Skip to main content

ddns/core/
wire.rs

1/// HTTP multi-record response wire format shared between server and all clients.
2///
3/// Wire layout (big-endian, contiguous):
4/// ```text
5/// +-----------+  (repeated `count` times)
6/// | count     |  +-----------+------+-----------+------+
7/// | u32 BE    |  | dns_len   | dns  | cert_len  | cert |
8/// +-----------+  | u32 BE    | ...  | u32 BE    | ...  |
9///                +-----------+------+-----------+------+
10/// ```
11use std::borrow::Borrow;
12
13use bytes::BufMut;
14use dhttp_identity::certificate::CertificateChainKey;
15use nom::{IResult, bytes::streaming::take, number::streaming::be_u32};
16use rustls::pki_types::CertificateDer;
17
18use crate::core::signature::SignatureFields;
19
20/// One DNS + certificate pair inside a [`MultiResponse`].
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ResponseRecord {
23    /// RFC 9421/9530-style publisher signature fields. Empty for unsigned
24    /// OpenMulti or static seed records.
25    pub signature_fields: SignatureFields,
26    /// Serialised DNS packet bytes.
27    pub dns: Vec<u8>,
28    /// DER-encoded leaf certificate of the publisher, or empty when unavailable.
29    pub cert: Vec<u8>,
30}
31
32impl ResponseRecord {
33    pub fn new(signature_fields: SignatureFields, dns: Vec<u8>, cert: Vec<u8>) -> Self {
34        Self {
35            signature_fields,
36            dns,
37            cert,
38        }
39    }
40
41    pub fn unsigned(dns: Vec<u8>, cert: Vec<u8>) -> Self {
42        Self::new(SignatureFields::empty(), dns, cert)
43    }
44
45    /// SHA-256 fingerprint of the publisher certificate as lowercase hex.
46    /// Returns `None` when the cert field is empty.
47    pub fn cert_fingerprint_hex(&self) -> Option<String> {
48        if self.cert.is_empty() {
49            return None;
50        }
51        use ring::digest::{SHA256, digest};
52        let digest = digest(&SHA256, &self.cert);
53        Some(digest.as_ref().iter().map(|b| format!("{b:02x}")).collect())
54    }
55
56    pub fn publisher_certificate_chain_key(&self) -> Option<CertificateChainKey> {
57        if self.cert.is_empty() {
58            return None;
59        }
60
61        let cert = CertificateDer::from(self.cert.clone());
62        dhttp_identity::identity::extract_dhttp_subject_key_identifier(std::slice::from_ref(&cert))
63            .ok()
64            .map(|ski| ski.chain().clone())
65    }
66}
67
68/// HTTP response body carrying zero or more DNS records.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct MultiResponse {
71    pub records: Vec<ResponseRecord>,
72}
73
74impl MultiResponse {
75    pub fn new(iter: impl IntoIterator<Item = ResponseRecord>) -> Self {
76        Self {
77            records: iter.into_iter().collect(),
78        }
79    }
80
81    pub fn encoding_size(&self) -> usize {
82        4 + self
83            .records
84            .iter()
85            .map(|record| {
86                4 + record.signature_fields.content_digest.len()
87                    + 4
88                    + record.signature_fields.signature_input.len()
89                    + 4
90                    + record.signature_fields.signature.len()
91                    + 4
92                    + record.dns.len()
93                    + 4
94                    + record.cert.len()
95            })
96            .sum::<usize>()
97    }
98
99    pub fn encode(&self) -> Vec<u8> {
100        let mut buf = Vec::with_capacity(self.encoding_size());
101        buf.put_multi_response(self);
102        buf
103    }
104
105    pub fn encode_records<I, R>(records: I) -> Vec<u8>
106    where
107        I: IntoIterator<Item = R>,
108        R: Borrow<ResponseRecord>,
109    {
110        let mut buf = Vec::new();
111        buf.put_u32(0);
112
113        let mut count = 0u32;
114        for record in records {
115            count = count
116                .checked_add(1)
117                .expect("multi response record count exceeds u32 range");
118            put_response_record(&mut buf, record.borrow());
119        }
120
121        buf[..4].copy_from_slice(&count.to_be_bytes());
122        buf
123    }
124}
125
126pub trait WriteMultiResponse {
127    fn put_multi_response(&mut self, response: &MultiResponse);
128}
129
130impl<B: BufMut> WriteMultiResponse for B {
131    fn put_multi_response(&mut self, response: &MultiResponse) {
132        self.put_u32(response.records.len() as u32);
133        for record in &response.records {
134            put_response_record(self, record);
135        }
136    }
137}
138
139fn put_response_record<B: BufMut>(buf: &mut B, record: &ResponseRecord) {
140    put_field(buf, &record.signature_fields.content_digest);
141    put_field(buf, &record.signature_fields.signature_input);
142    put_field(buf, &record.signature_fields.signature);
143    put_field(buf, &record.dns);
144    put_field(buf, &record.cert);
145}
146
147fn put_field<B: BufMut>(buf: &mut B, value: &[u8]) {
148    buf.put_u32(value.len() as u32);
149    buf.put_slice(value);
150}
151
152pub fn be_multi_response(input: &[u8]) -> IResult<&[u8], MultiResponse> {
153    let (mut input, count) = be_u32(input)?;
154    let mut records = Vec::with_capacity(count as usize);
155    for _ in 0..count {
156        let (rest, content_digest) = be_field(input)?;
157        let (rest, signature_input) = be_field(rest)?;
158        let (rest, signature) = be_field(rest)?;
159        let (rest, dns) = be_field(rest)?;
160        let (rest, cert) = be_field(rest)?;
161        records.push(ResponseRecord::new(
162            SignatureFields {
163                content_digest,
164                signature_input,
165                signature,
166            },
167            dns,
168            cert,
169        ));
170        input = rest;
171    }
172    Ok((input, MultiResponse { records }))
173}
174
175fn be_field(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
176    let (input, len) = be_u32(input)?;
177    let (input, value) = take(len as usize)(input)?;
178    Ok((input, value.to_vec()))
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn multi_response_roundtrips() {
187        let response = MultiResponse::new([
188            ResponseRecord::new(
189                SignatureFields {
190                    content_digest: b"sha-256=:abc:".to_vec(),
191                    signature_input:
192                        b"dns=(\"content-digest\");created=1;keyid=\"sha256:abc\";alg=\"ed25519\""
193                            .to_vec(),
194                    signature: b"dns=:sig:".to_vec(),
195                },
196                vec![1, 2, 3],
197                vec![4, 5],
198            ),
199            ResponseRecord::unsigned(vec![6, 7, 8, 9], Vec::new()),
200        ]);
201        let encoded = response.encode();
202        let (remain, decoded) = be_multi_response(&encoded).unwrap();
203        assert!(remain.is_empty());
204        assert_eq!(decoded, response);
205    }
206
207    #[test]
208    fn encode_records_matches_multi_response_encoding_for_owned_records() {
209        let records = [
210            ResponseRecord::unsigned(vec![1, 2, 3], vec![4, 5]),
211            ResponseRecord::new(
212                SignatureFields {
213                    content_digest: b"sha-256=:abc:".to_vec(),
214                    signature_input: b"dns=(\"content-digest\")".to_vec(),
215                    signature: b"dns=:sig:".to_vec(),
216                },
217                vec![6, 7],
218                Vec::new(),
219            ),
220        ];
221        let response = MultiResponse::new(records.clone());
222
223        assert_eq!(MultiResponse::encode_records(records), response.encode());
224    }
225
226    #[test]
227    fn encode_records_matches_multi_response_encoding_for_borrowed_records() {
228        let response = MultiResponse::new([
229            ResponseRecord::unsigned(vec![1, 2, 3], vec![4, 5]),
230            ResponseRecord::unsigned(vec![6, 7], Vec::new()),
231        ]);
232
233        assert_eq!(
234            MultiResponse::encode_records(response.records.iter()),
235            response.encode()
236        );
237    }
238}