1use rustls::pki_types::{CertificateDer, UnixTime};
29
30use crate::dht::{self, Record};
31
32#[derive(Debug, PartialEq, Eq)]
39pub enum CertChainError {
40 BadSignature,
48 Absent,
53 Undecodable,
56 KeyMismatch,
60 Untrusted,
63 OrgMismatch,
67}
68
69impl std::fmt::Display for CertChainError {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 let msg = match self {
72 CertChainError::BadSignature => "advertisement signature does not verify",
73 CertChainError::Absent => "advertisement carries no cert_chain",
74 CertChainError::Undecodable => "cert_chain is not a decodable PEM certificate bundle",
75 CertChainError::KeyMismatch => {
76 "leaf cert public key does not match the advertisement's signer"
77 }
78 CertChainError::Untrusted => "cert chain does not validate to the trusted realm CA",
79 CertChainError::OrgMismatch => "leaf cert organization does not match the expected org",
80 };
81 write!(f, "cert_chain: {msg}")
82 }
83}
84
85impl std::error::Error for CertChainError {}
86
87pub fn verify_advertisement_cert_chain(
102 realm_ca_pem: &[u8],
103 rec: &Record,
104 expected_org: &str,
105) -> Result<(), CertChainError> {
106 dht::verify(rec).map_err(|_| CertChainError::BadSignature)?;
107 let adv = dht::read_procedure_advertisement(rec).map_err(|_| CertChainError::BadSignature)?;
108 let Some(chain_pem) = adv.cert_chain else {
109 return Err(CertChainError::Absent);
110 };
111
112 let chain_der = decode_cert_chain(&chain_pem)?;
113 let leaf_der = &chain_der[0];
114
115 let leaf_key =
116 crate::cert::ed25519_pubkey_from_cert(leaf_der).map_err(|_| CertChainError::KeyMismatch)?;
117 if leaf_key != rec.key {
118 return Err(CertChainError::KeyMismatch);
119 }
120
121 validate_cert_path(realm_ca_pem, &chain_der)?;
122
123 let leaf_org = leaf_organization(leaf_der).ok_or(CertChainError::OrgMismatch)?;
124 if leaf_org != expected_org {
125 return Err(CertChainError::OrgMismatch);
126 }
127 Ok(())
128}
129
130fn decode_cert_chain(cert_chain_pem: &[u8]) -> Result<Vec<Vec<u8>>, CertChainError> {
134 let ders: Vec<Vec<u8>> = x509_parser::pem::Pem::iter_from_buffer(cert_chain_pem)
135 .filter_map(Result::ok)
136 .filter(|pem| pem.label == "CERTIFICATE")
137 .map(|pem| pem.contents)
138 .collect();
139 if ders.is_empty() {
140 return Err(CertChainError::Undecodable);
141 }
142 Ok(ders)
143}
144
145fn leaf_organization(der: &[u8]) -> Option<String> {
148 let (_, cert) = x509_parser::parse_x509_certificate(der).ok()?;
149 let org = cert
150 .subject()
151 .iter_organization()
152 .next()
153 .and_then(|attr| attr.as_str().ok())
154 .map(str::to_owned);
155 org
156}
157
158fn validate_cert_path(realm_ca_pem: &[u8], chain: &[Vec<u8>]) -> Result<(), CertChainError> {
164 let anchor_ders = decode_cert_chain(realm_ca_pem).map_err(|_| CertChainError::Untrusted)?;
165 let anchor_der = CertificateDer::from(anchor_ders[0].clone());
166 let anchor =
167 webpki::anchor_from_trusted_cert(&anchor_der).map_err(|_| CertChainError::Untrusted)?;
168
169 let leaf_der = CertificateDer::from(chain[0].clone());
170 let end_entity =
171 webpki::EndEntityCert::try_from(&leaf_der).map_err(|_| CertChainError::Untrusted)?;
172 let intermediates: Vec<CertificateDer> = chain[1..]
173 .iter()
174 .map(|der| CertificateDer::from(der.clone()))
175 .collect();
176
177 end_entity
182 .verify_for_usage(
183 &[webpki::ring::ED25519],
184 std::slice::from_ref(&anchor),
185 &intermediates,
186 UnixTime::now(),
187 webpki::KeyUsage::server_auth(),
188 None,
189 None,
190 )
191 .map_err(|_| CertChainError::Untrusted)?;
192 Ok(())
193}
194
195#[cfg(test)]
196mod tests {
197 use std::time::Duration;
198
199 use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair as RcgenKeyPair};
200
201 use super::*;
202 use crate::dht;
203 use crate::identity::KeyPair;
204
205 fn test_ca() -> (Vec<u8>, rcgen::Issuer<'static, RcgenKeyPair>) {
206 let key_pair = RcgenKeyPair::generate_for(&rcgen::PKCS_ED25519).expect("ca keygen");
207 let mut params = CertificateParams::new(Vec::<String>::new()).expect("ca params");
208 let mut dn = DistinguishedName::new();
209 dn.push(DnType::CommonName, "Test Realm CA");
210 dn.push(DnType::OrganizationName, "Test Realm CA");
211 params.distinguished_name = dn;
212 params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
213 params.not_before = time::OffsetDateTime::now_utc() - time::Duration::hours(1);
214 params.not_after = time::OffsetDateTime::now_utc() + time::Duration::hours(24);
215 let cert = params.self_signed(&key_pair).expect("ca self-sign");
216 let pem = cert.pem().into_bytes();
217 (pem, rcgen::Issuer::new(params, key_pair))
218 }
219
220 fn test_leaf(
221 ca_issuer: &rcgen::Issuer<'static, RcgenKeyPair>,
222 advertiser_pub: [u8; 32],
223 org: &str,
224 not_after: time::OffsetDateTime,
225 ) -> Vec<u8> {
226 let subject_spki = rcgen::SubjectPublicKeyInfo::from_der(&ed25519_spki_der(advertiser_pub))
227 .expect("advertiser SPKI");
228 let mut params = CertificateParams::new(Vec::<String>::new()).expect("leaf params");
229 let mut dn = DistinguishedName::new();
230 dn.push(DnType::CommonName, "test-service");
231 dn.push(DnType::OrganizationName, org);
232 params.distinguished_name = dn;
233 params.not_before = time::OffsetDateTime::now_utc() - time::Duration::hours(1);
234 params.not_after = not_after;
235 let cert = params
236 .signed_by(&subject_spki, ca_issuer)
237 .expect("leaf signed_by");
238 cert.der().to_vec()
239 }
240
241 fn ed25519_spki_der(pubkey: [u8; 32]) -> Vec<u8> {
247 let mut der = vec![
248 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, ];
253 der.extend_from_slice(&pubkey);
254 der
255 }
256
257 fn pem_bundle(ders: &[Vec<u8>]) -> Vec<u8> {
258 let mut out = Vec::new();
259 for der in ders {
260 let b64 = base64_std_encode(der);
261 out.extend_from_slice(b"-----BEGIN CERTIFICATE-----\n");
262 for chunk in b64.as_bytes().chunks(64) {
263 out.extend_from_slice(chunk);
264 out.push(b'\n');
265 }
266 out.extend_from_slice(b"-----END CERTIFICATE-----\n");
267 }
268 out
269 }
270
271 fn base64_std_encode(data: &[u8]) -> String {
272 use base64::Engine;
273 base64::engine::general_purpose::STANDARD.encode(data)
274 }
275
276 fn advertiser_and_station() -> (KeyPair, KeyPair) {
277 (KeyPair::generate(), KeyPair::generate())
278 }
279
280 #[test]
281 fn valid_chain_verifies_and_authorizes() {
282 let (ca_pem, ca_issuer) = test_ca();
283 let (advertiser, station) = advertiser_and_station();
284 let leaf_der = test_leaf(
285 &ca_issuer,
286 advertiser.node_id(),
287 "acme-corp",
288 time::OffsetDateTime::now_utc() + time::Duration::hours(1),
289 );
290
291 let rec = dht::new_procedure_advertisement_with_cert_chain(
292 advertiser.node_id(),
293 "0000/acme-corp/widget.build_v1",
294 station.node_id(),
295 Duration::from_secs(3600),
296 pem_bundle(&[leaf_der]),
297 );
298 let rec = dht::sign(rec, &advertiser);
299
300 assert_eq!(
301 verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
302 Ok(())
303 );
304 }
305
306 #[test]
307 fn absent_chain_is_reported_distinctly() {
308 let (advertiser, station) = advertiser_and_station();
309 let rec = dht::new_procedure_advertisement(
310 advertiser.node_id(),
311 "0000/acme-corp/widget.build_v1",
312 station.node_id(),
313 Duration::from_secs(3600),
314 );
315 let rec = dht::sign(rec, &advertiser);
316 let (ca_pem, _) = test_ca();
317
318 assert_eq!(
319 verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
320 Err(CertChainError::Absent)
321 );
322 }
323
324 #[test]
325 fn bad_envelope_signature_is_checked_before_the_chain() {
326 let (ca_pem, ca_issuer) = test_ca();
327 let (advertiser, station) = advertiser_and_station();
328 let leaf_der = test_leaf(
329 &ca_issuer,
330 advertiser.node_id(),
331 "acme-corp",
332 time::OffsetDateTime::now_utc() + time::Duration::hours(1),
333 );
334 let rec = dht::new_procedure_advertisement_with_cert_chain(
335 advertiser.node_id(),
336 "0000/acme-corp/widget.build_v1",
337 station.node_id(),
338 Duration::from_secs(3600),
339 pem_bundle(&[leaf_der]),
340 );
341 let mut rec = dht::sign(rec, &advertiser);
342 rec.signature[0] ^= 0xFF;
343
344 assert_eq!(
345 verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
346 Err(CertChainError::BadSignature)
347 );
348 }
349
350 #[test]
351 fn leaf_key_not_matching_the_signer_is_rejected() {
352 let (ca_pem, ca_issuer) = test_ca();
353 let (advertiser, station) = advertiser_and_station();
354 let other = KeyPair::generate();
355 let leaf_der = test_leaf(
358 &ca_issuer,
359 other.node_id(),
360 "acme-corp",
361 time::OffsetDateTime::now_utc() + time::Duration::hours(1),
362 );
363 let rec = dht::new_procedure_advertisement_with_cert_chain(
364 advertiser.node_id(),
365 "0000/acme-corp/widget.build_v1",
366 station.node_id(),
367 Duration::from_secs(3600),
368 pem_bundle(&[leaf_der]),
369 );
370 let rec = dht::sign(rec, &advertiser);
371
372 assert_eq!(
373 verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
374 Err(CertChainError::KeyMismatch)
375 );
376 }
377
378 #[test]
379 fn wrong_org_is_rejected_after_a_valid_chain() {
380 let (ca_pem, ca_issuer) = test_ca();
381 let (advertiser, station) = advertiser_and_station();
382 let leaf_der = test_leaf(
383 &ca_issuer,
384 advertiser.node_id(),
385 "acme-corp",
386 time::OffsetDateTime::now_utc() + time::Duration::hours(1),
387 );
388 let rec = dht::new_procedure_advertisement_with_cert_chain(
389 advertiser.node_id(),
390 "0000/other-org/widget.build_v1",
391 station.node_id(),
392 Duration::from_secs(3600),
393 pem_bundle(&[leaf_der]),
394 );
395 let rec = dht::sign(rec, &advertiser);
396
397 assert_eq!(
398 verify_advertisement_cert_chain(&ca_pem, &rec, "other-org"),
399 Err(CertChainError::OrgMismatch)
400 );
401 }
402
403 #[test]
404 fn expired_leaf_is_untrusted() {
405 let (ca_pem, ca_issuer) = test_ca();
406 let (advertiser, station) = advertiser_and_station();
407 let leaf_der = test_leaf(
408 &ca_issuer,
409 advertiser.node_id(),
410 "acme-corp",
411 time::OffsetDateTime::now_utc() - time::Duration::hours(1),
412 );
413 let rec = dht::new_procedure_advertisement_with_cert_chain(
414 advertiser.node_id(),
415 "0000/acme-corp/widget.build_v1",
416 station.node_id(),
417 Duration::from_secs(3600),
418 pem_bundle(&[leaf_der]),
419 );
420 let rec = dht::sign(rec, &advertiser);
421
422 assert_eq!(
423 verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
424 Err(CertChainError::Untrusted)
425 );
426 }
427
428 #[test]
429 fn chain_signed_by_a_different_ca_is_untrusted() {
430 let (_, ca_issuer) = test_ca();
431 let (other_ca_pem, _) = test_ca();
432 let (advertiser, station) = advertiser_and_station();
433 let leaf_der = test_leaf(
434 &ca_issuer,
435 advertiser.node_id(),
436 "acme-corp",
437 time::OffsetDateTime::now_utc() + time::Duration::hours(1),
438 );
439 let rec = dht::new_procedure_advertisement_with_cert_chain(
440 advertiser.node_id(),
441 "0000/acme-corp/widget.build_v1",
442 station.node_id(),
443 Duration::from_secs(3600),
444 pem_bundle(&[leaf_der]),
445 );
446 let rec = dht::sign(rec, &advertiser);
447
448 assert_eq!(
449 verify_advertisement_cert_chain(&other_ca_pem, &rec, "acme-corp"),
450 Err(CertChainError::Untrusted)
451 );
452 }
453
454 #[test]
455 fn undecodable_chain_is_reported_distinctly() {
456 let (ca_pem, _) = test_ca();
457 let (advertiser, station) = advertiser_and_station();
458 let rec = dht::new_procedure_advertisement_with_cert_chain(
459 advertiser.node_id(),
460 "0000/acme-corp/widget.build_v1",
461 station.node_id(),
462 Duration::from_secs(3600),
463 b"not a pem cert bundle".to_vec(),
464 );
465 let rec = dht::sign(rec, &advertiser);
466
467 assert_eq!(
468 verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
469 Err(CertChainError::Undecodable)
470 );
471 }
472}