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 bytes::BufMut;
12use nom::{IResult, bytes::streaming::take, number::streaming::be_u32};
13
14use crate::core::signature::SignatureFields;
15
16/// One DNS + certificate pair inside a [`MultiResponse`].
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ResponseRecord {
19    /// RFC 9421/9530-style publisher signature fields. Empty for unsigned
20    /// OpenMulti or static seed records.
21    pub signature_fields: SignatureFields,
22    /// Serialised DNS packet bytes.
23    pub dns: Vec<u8>,
24    /// DER-encoded leaf certificate of the publisher, or empty when unavailable.
25    pub cert: Vec<u8>,
26}
27
28impl ResponseRecord {
29    pub fn new(signature_fields: SignatureFields, dns: Vec<u8>, cert: Vec<u8>) -> Self {
30        Self {
31            signature_fields,
32            dns,
33            cert,
34        }
35    }
36
37    pub fn unsigned(dns: Vec<u8>, cert: Vec<u8>) -> Self {
38        Self::new(SignatureFields::empty(), dns, cert)
39    }
40
41    /// SHA-256 fingerprint of the publisher certificate as lowercase hex.
42    /// Returns `None` when the cert field is empty.
43    pub fn cert_fingerprint_hex(&self) -> Option<String> {
44        if self.cert.is_empty() {
45            return None;
46        }
47        use ring::digest::{SHA256, digest};
48        let digest = digest(&SHA256, &self.cert);
49        Some(digest.as_ref().iter().map(|b| format!("{b:02x}")).collect())
50    }
51}
52
53/// HTTP response body carrying zero or more DNS records.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct MultiResponse {
56    pub records: Vec<ResponseRecord>,
57}
58
59impl MultiResponse {
60    pub fn new(iter: impl IntoIterator<Item = ResponseRecord>) -> Self {
61        Self {
62            records: iter.into_iter().collect(),
63        }
64    }
65
66    pub fn encoding_size(&self) -> usize {
67        4 + self
68            .records
69            .iter()
70            .map(|record| {
71                4 + record.signature_fields.content_digest.len()
72                    + 4
73                    + record.signature_fields.signature_input.len()
74                    + 4
75                    + record.signature_fields.signature.len()
76                    + 4
77                    + record.dns.len()
78                    + 4
79                    + record.cert.len()
80            })
81            .sum::<usize>()
82    }
83
84    pub fn encode(&self) -> Vec<u8> {
85        let mut buf = Vec::with_capacity(self.encoding_size());
86        buf.put_multi_response(self);
87        buf
88    }
89}
90
91pub trait WriteMultiResponse {
92    fn put_multi_response(&mut self, response: &MultiResponse);
93}
94
95impl<B: BufMut> WriteMultiResponse for B {
96    fn put_multi_response(&mut self, response: &MultiResponse) {
97        self.put_u32(response.records.len() as u32);
98        for record in &response.records {
99            put_field(self, &record.signature_fields.content_digest);
100            put_field(self, &record.signature_fields.signature_input);
101            put_field(self, &record.signature_fields.signature);
102            put_field(self, &record.dns);
103            put_field(self, &record.cert);
104        }
105    }
106}
107
108fn put_field<B: BufMut>(buf: &mut B, value: &[u8]) {
109    buf.put_u32(value.len() as u32);
110    buf.put_slice(value);
111}
112
113pub fn be_multi_response(input: &[u8]) -> IResult<&[u8], MultiResponse> {
114    let (mut input, count) = be_u32(input)?;
115    let mut records = Vec::with_capacity(count as usize);
116    for _ in 0..count {
117        let (rest, content_digest) = be_field(input)?;
118        let (rest, signature_input) = be_field(rest)?;
119        let (rest, signature) = be_field(rest)?;
120        let (rest, dns) = be_field(rest)?;
121        let (rest, cert) = be_field(rest)?;
122        records.push(ResponseRecord::new(
123            SignatureFields {
124                content_digest,
125                signature_input,
126                signature,
127            },
128            dns,
129            cert,
130        ));
131        input = rest;
132    }
133    Ok((input, MultiResponse { records }))
134}
135
136fn be_field(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
137    let (input, len) = be_u32(input)?;
138    let (input, value) = take(len as usize)(input)?;
139    Ok((input, value.to_vec()))
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn multi_response_roundtrips() {
148        let response = MultiResponse::new([
149            ResponseRecord::new(
150                SignatureFields {
151                    content_digest: b"sha-256=:abc:".to_vec(),
152                    signature_input:
153                        b"dns=(\"content-digest\");created=1;keyid=\"sha256:abc\";alg=\"ed25519\""
154                            .to_vec(),
155                    signature: b"dns=:sig:".to_vec(),
156                },
157                vec![1, 2, 3],
158                vec![4, 5],
159            ),
160            ResponseRecord::unsigned(vec![6, 7, 8, 9], Vec::new()),
161        ]);
162        let encoded = response.encode();
163        let (remain, decoded) = be_multi_response(&encoded).unwrap();
164        assert!(remain.is_empty());
165        assert_eq!(decoded, response);
166    }
167}