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}