Skip to main content

ddns/core/
signature.rs

1use std::time::{SystemTime, UNIX_EPOCH};
2
3use base64::Engine;
4use dhttp_identity::identity::{LocalAuthority, SignError as AuthoritySignError};
5use ring::digest::{SHA256, digest};
6use rustls::{SignatureScheme, pki_types::SubjectPublicKeyInfoDer};
7use snafu::Snafu;
8
9use crate::core::parser::sigin;
10
11pub const CONTENT_DIGEST_HEADER: &str = "content-digest";
12pub const SIGNATURE_INPUT_HEADER: &str = "signature-input";
13pub const SIGNATURE_HEADER: &str = "signature";
14pub const SIGNATURE_LABEL: &str = "dns";
15
16const DIGEST_PREFIX: &str = "sha-256=:";
17const SIGNATURE_PREFIX: &str = "dns=:";
18const SIGNATURE_INPUT_PREFIX: &str = "dns=(\"content-digest\")";
19
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct SignatureFields {
22    pub content_digest: Vec<u8>,
23    pub signature_input: Vec<u8>,
24    pub signature: Vec<u8>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28struct ParsedSignatureInput<'a> {
29    signature_params: &'a str,
30    alg: &'a str,
31    keyid: &'a str,
32}
33
34#[derive(Debug, Snafu)]
35#[snafu(module)]
36pub enum SignatureFieldsError {
37    #[snafu(display("missing publisher certificate"))]
38    MissingCertificate,
39    #[snafu(display("unsupported signature scheme {scheme:?}"))]
40    UnsupportedScheme { scheme: SignatureScheme },
41    #[snafu(display("unsupported signature algorithm {alg}"))]
42    UnsupportedAlgorithm { alg: String },
43    #[snafu(display("invalid {field} field"))]
44    InvalidField { field: &'static str },
45    #[snafu(display("invalid signature field utf-8"))]
46    InvalidUtf8 { source: std::str::Utf8Error },
47    #[snafu(display("invalid base64"))]
48    InvalidBase64 { source: base64::DecodeError },
49    #[snafu(display("content digest mismatch"))]
50    DigestMismatch,
51    #[snafu(display("signature keyid does not match publisher certificate"))]
52    KeyIdMismatch,
53    #[snafu(display("failed to sign DNS packet"))]
54    Sign { source: AuthoritySignError },
55    #[snafu(display("invalid certificate: {details}"))]
56    InvalidCertificate { details: String },
57    #[snafu(display("signature verification failed"))]
58    Verify { source: sigin::VerifyError },
59}
60
61impl SignatureFields {
62    pub fn empty() -> Self {
63        Self::default()
64    }
65
66    pub fn is_empty(&self) -> bool {
67        self.content_digest.is_empty()
68            && self.signature_input.is_empty()
69            && self.signature.is_empty()
70    }
71
72    pub async fn sign(
73        dns_bytes: &[u8],
74        authority: &(impl LocalAuthority + ?Sized),
75    ) -> Result<Self, SignatureFieldsError> {
76        let cert = authority
77            .cert_chain()
78            .first()
79            .ok_or(SignatureFieldsError::MissingCertificate)?;
80        let keyid = keyid_for_cert(cert.as_ref());
81        let content_digest = content_digest_value(dns_bytes);
82        let created = unix_now_secs();
83
84        let scheme = sigin::canonical_scheme_for_spki(authority.public_key()).ok_or(
85            SignatureFieldsError::UnsupportedScheme {
86                scheme: SignatureScheme::Unknown(0),
87            },
88        )?;
89        let alg = sigin::alg_name_for_scheme(scheme)
90            .ok_or(SignatureFieldsError::UnsupportedScheme { scheme })?;
91        let signature_input = signature_input_value(created, &keyid, alg);
92        let signature_base = signature_base(&content_digest, &signature_input)?;
93        let signature = authority
94            .sign(signature_base.as_bytes())
95            .await
96            .map_err(|source| SignatureFieldsError::Sign { source })?;
97        let signature = signature_value(&signature);
98
99        Ok(Self {
100            content_digest: content_digest.into_bytes(),
101            signature_input: signature_input.into_bytes(),
102            signature: signature.into_bytes(),
103        })
104    }
105
106    pub fn verify(&self, dns_bytes: &[u8], cert_der: &[u8]) -> Result<bool, SignatureFieldsError> {
107        if self.is_empty() {
108            return Ok(false);
109        }
110
111        let content_digest = field_str(&self.content_digest)?;
112        verify_content_digest(content_digest, dns_bytes)?;
113
114        let signature_input = field_str(&self.signature_input)?;
115        let parsed_input = parse_signature_input(signature_input)?;
116        let expected_keyid = keyid_for_cert(cert_der);
117        if parsed_input.keyid != expected_keyid {
118            return Ok(false);
119        }
120
121        let scheme = sigin::scheme_for_alg_name(parsed_input.alg).ok_or_else(|| {
122            SignatureFieldsError::UnsupportedAlgorithm {
123                alg: parsed_input.alg.to_string(),
124            }
125        })?;
126        let signature = parse_signature(field_str(&self.signature)?)?;
127        let signature_base = signature_base(content_digest, signature_input)?;
128
129        let (_, cert) = x509_parser::parse_x509_certificate(cert_der).map_err(|e| {
130            SignatureFieldsError::InvalidCertificate {
131                details: e.to_string(),
132            }
133        })?;
134        let spki = SubjectPublicKeyInfoDer::from(cert.tbs_certificate.subject_pki.raw);
135        sigin::verify(spki, scheme, signature_base.as_bytes(), &signature)
136            .map_err(|source| SignatureFieldsError::Verify { source })
137    }
138}
139
140pub fn content_digest_value(dns_bytes: &[u8]) -> String {
141    let digest = digest(&SHA256, dns_bytes);
142    let b64 = base64::engine::general_purpose::STANDARD.encode(digest.as_ref());
143    format!("{DIGEST_PREFIX}{b64}:")
144}
145
146pub fn cert_fingerprint_hex(cert_der: &[u8]) -> String {
147    digest(&SHA256, cert_der)
148        .as_ref()
149        .iter()
150        .map(|b| format!("{b:02x}"))
151        .collect()
152}
153
154pub fn keyid_for_cert(cert_der: &[u8]) -> String {
155    format!("sha256:{}", cert_fingerprint_hex(cert_der))
156}
157
158fn unix_now_secs() -> u64 {
159    SystemTime::now()
160        .duration_since(UNIX_EPOCH)
161        .map(|d| d.as_secs())
162        .unwrap_or(0)
163}
164
165fn signature_input_value(created: u64, keyid: &str, alg: &str) -> String {
166    format!(
167        "{SIGNATURE_LABEL}=(\"content-digest\");created={created};keyid=\"{keyid}\";alg=\"{alg}\""
168    )
169}
170
171fn signature_value(signature: &[u8]) -> String {
172    let b64 = base64::engine::general_purpose::STANDARD.encode(signature);
173    format!("{SIGNATURE_PREFIX}{b64}:")
174}
175
176fn signature_base(
177    content_digest: &str,
178    signature_input: &str,
179) -> Result<String, SignatureFieldsError> {
180    let parsed = parse_signature_input(signature_input)?;
181    Ok(format!(
182        "\"content-digest\": {content_digest}\n\"@signature-params\": {}",
183        parsed.signature_params
184    ))
185}
186
187fn field_str(field: &[u8]) -> Result<&str, SignatureFieldsError> {
188    std::str::from_utf8(field).map_err(|source| SignatureFieldsError::InvalidUtf8 { source })
189}
190
191fn verify_content_digest(
192    content_digest: &str,
193    dns_bytes: &[u8],
194) -> Result<(), SignatureFieldsError> {
195    let encoded = content_digest
196        .strip_prefix(DIGEST_PREFIX)
197        .and_then(|rest| rest.strip_suffix(':'))
198        .ok_or(SignatureFieldsError::InvalidField {
199            field: CONTENT_DIGEST_HEADER,
200        })?;
201    let decoded = base64::engine::general_purpose::STANDARD
202        .decode(encoded)
203        .map_err(|source| SignatureFieldsError::InvalidBase64 { source })?;
204    if decoded.as_slice() != digest(&SHA256, dns_bytes).as_ref() {
205        return Err(SignatureFieldsError::DigestMismatch);
206    }
207    Ok(())
208}
209
210fn parse_signature(input: &str) -> Result<Vec<u8>, SignatureFieldsError> {
211    let encoded = input
212        .strip_prefix(SIGNATURE_PREFIX)
213        .and_then(|rest| rest.strip_suffix(':'))
214        .ok_or(SignatureFieldsError::InvalidField {
215            field: SIGNATURE_HEADER,
216        })?;
217    base64::engine::general_purpose::STANDARD
218        .decode(encoded)
219        .map_err(|source| SignatureFieldsError::InvalidBase64 { source })
220}
221
222fn parse_signature_input(input: &str) -> Result<ParsedSignatureInput<'_>, SignatureFieldsError> {
223    if !input.starts_with(SIGNATURE_INPUT_PREFIX) {
224        return Err(SignatureFieldsError::InvalidField {
225            field: SIGNATURE_INPUT_HEADER,
226        });
227    }
228
229    let signature_params =
230        input
231            .strip_prefix("dns=")
232            .ok_or(SignatureFieldsError::InvalidField {
233                field: SIGNATURE_INPUT_HEADER,
234            })?;
235    let params = signature_params
236        .strip_prefix("(\"content-digest\")")
237        .ok_or(SignatureFieldsError::InvalidField {
238            field: SIGNATURE_INPUT_HEADER,
239        })?;
240
241    let mut created = None;
242    let mut keyid = None;
243    let mut alg = None;
244
245    for param in params.split(';').filter(|part| !part.is_empty()) {
246        if let Some(value) = param.strip_prefix("created=") {
247            created = value.parse::<u64>().ok();
248        } else if let Some(value) = param.strip_prefix("keyid=") {
249            keyid = unquote(value);
250        } else if let Some(value) = param.strip_prefix("alg=") {
251            alg = unquote(value);
252        } else {
253            return Err(SignatureFieldsError::InvalidField {
254                field: SIGNATURE_INPUT_HEADER,
255            });
256        }
257    }
258
259    if created.is_none() {
260        return Err(SignatureFieldsError::InvalidField {
261            field: SIGNATURE_INPUT_HEADER,
262        });
263    }
264
265    let keyid = keyid.ok_or(SignatureFieldsError::InvalidField {
266        field: SIGNATURE_INPUT_HEADER,
267    })?;
268    let alg = alg.ok_or(SignatureFieldsError::InvalidField {
269        field: SIGNATURE_INPUT_HEADER,
270    })?;
271
272    Ok(ParsedSignatureInput {
273        signature_params,
274        alg,
275        keyid,
276    })
277}
278
279fn unquote(value: &str) -> Option<&str> {
280    value.strip_prefix('"')?.strip_suffix('"')
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn content_digest_uses_sha256_dictionary_value() {
289        let value = content_digest_value(b"dns");
290        assert!(value.starts_with("sha-256=:"));
291        assert!(value.ends_with(':'));
292        verify_content_digest(&value, b"dns").unwrap();
293        assert!(matches!(
294            verify_content_digest(&value, b"changed"),
295            Err(SignatureFieldsError::DigestMismatch)
296        ));
297    }
298
299    #[test]
300    fn signature_input_requires_alg_and_keyid() {
301        let input = "dns=(\"content-digest\");created=1;keyid=\"sha256:abc\";alg=\"ed25519\"";
302        let parsed = parse_signature_input(input).unwrap();
303        assert_eq!(parsed.keyid, "sha256:abc");
304        assert_eq!(parsed.alg, "ed25519");
305
306        assert!(parse_signature_input("dns=(\"content-digest\");created=1").is_err());
307        assert!(parse_signature_input("dns=(\"date\");created=1;alg=\"ed25519\"").is_err());
308    }
309
310    #[test]
311    fn alg_names_are_explicitly_mapped() {
312        assert_eq!(
313            sigin::scheme_for_alg_name("ed25519"),
314            Some(SignatureScheme::ED25519)
315        );
316        assert_eq!(
317            sigin::alg_name_for_scheme(SignatureScheme::ECDSA_NISTP256_SHA256),
318            Some("ecdsa-p256-sha256")
319        );
320        assert_eq!(sigin::scheme_for_alg_name("unknown"), None);
321    }
322}