use rustls::pki_types::{CertificateDer, UnixTime};
use crate::dht::{self, Record};
#[derive(Debug, PartialEq, Eq)]
pub enum CertChainError {
BadSignature,
Absent,
Undecodable,
KeyMismatch,
Untrusted,
OrgMismatch,
}
impl std::fmt::Display for CertChainError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
CertChainError::BadSignature => "advertisement signature does not verify",
CertChainError::Absent => "advertisement carries no cert_chain",
CertChainError::Undecodable => "cert_chain is not a decodable PEM certificate bundle",
CertChainError::KeyMismatch => {
"leaf cert public key does not match the advertisement's signer"
}
CertChainError::Untrusted => "cert chain does not validate to the trusted realm CA",
CertChainError::OrgMismatch => "leaf cert organization does not match the expected org",
};
write!(f, "cert_chain: {msg}")
}
}
impl std::error::Error for CertChainError {}
pub fn verify_advertisement_cert_chain(
realm_ca_pem: &[u8],
rec: &Record,
expected_org: &str,
) -> Result<(), CertChainError> {
dht::verify(rec).map_err(|_| CertChainError::BadSignature)?;
let adv = dht::read_procedure_advertisement(rec).map_err(|_| CertChainError::BadSignature)?;
let Some(chain_pem) = adv.cert_chain else {
return Err(CertChainError::Absent);
};
let chain_der = decode_cert_chain(&chain_pem)?;
let leaf_der = &chain_der[0];
let leaf_key =
crate::cert::ed25519_pubkey_from_cert(leaf_der).map_err(|_| CertChainError::KeyMismatch)?;
if leaf_key != rec.key {
return Err(CertChainError::KeyMismatch);
}
validate_cert_path(realm_ca_pem, &chain_der)?;
let leaf_org = leaf_organization(leaf_der).ok_or(CertChainError::OrgMismatch)?;
if leaf_org != expected_org {
return Err(CertChainError::OrgMismatch);
}
Ok(())
}
fn decode_cert_chain(cert_chain_pem: &[u8]) -> Result<Vec<Vec<u8>>, CertChainError> {
let ders: Vec<Vec<u8>> = x509_parser::pem::Pem::iter_from_buffer(cert_chain_pem)
.filter_map(Result::ok)
.filter(|pem| pem.label == "CERTIFICATE")
.map(|pem| pem.contents)
.collect();
if ders.is_empty() {
return Err(CertChainError::Undecodable);
}
Ok(ders)
}
fn leaf_organization(der: &[u8]) -> Option<String> {
let (_, cert) = x509_parser::parse_x509_certificate(der).ok()?;
let org = cert
.subject()
.iter_organization()
.next()
.and_then(|attr| attr.as_str().ok())
.map(str::to_owned);
org
}
fn validate_cert_path(realm_ca_pem: &[u8], chain: &[Vec<u8>]) -> Result<(), CertChainError> {
let anchor_ders = decode_cert_chain(realm_ca_pem).map_err(|_| CertChainError::Untrusted)?;
let anchor_der = CertificateDer::from(anchor_ders[0].clone());
let anchor =
webpki::anchor_from_trusted_cert(&anchor_der).map_err(|_| CertChainError::Untrusted)?;
let leaf_der = CertificateDer::from(chain[0].clone());
let end_entity =
webpki::EndEntityCert::try_from(&leaf_der).map_err(|_| CertChainError::Untrusted)?;
let intermediates: Vec<CertificateDer> = chain[1..]
.iter()
.map(|der| CertificateDer::from(der.clone()))
.collect();
end_entity
.verify_for_usage(
&[webpki::ring::ED25519],
std::slice::from_ref(&anchor),
&intermediates,
UnixTime::now(),
webpki::KeyUsage::server_auth(),
None,
None,
)
.map_err(|_| CertChainError::Untrusted)?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair as RcgenKeyPair};
use super::*;
use crate::dht;
use crate::identity::KeyPair;
fn test_ca() -> (Vec<u8>, rcgen::Issuer<'static, RcgenKeyPair>) {
let key_pair = RcgenKeyPair::generate_for(&rcgen::PKCS_ED25519).expect("ca keygen");
let mut params = CertificateParams::new(Vec::<String>::new()).expect("ca params");
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "Test Realm CA");
dn.push(DnType::OrganizationName, "Test Realm CA");
params.distinguished_name = dn;
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
params.not_before = time::OffsetDateTime::now_utc() - time::Duration::hours(1);
params.not_after = time::OffsetDateTime::now_utc() + time::Duration::hours(24);
let cert = params.self_signed(&key_pair).expect("ca self-sign");
let pem = cert.pem().into_bytes();
(pem, rcgen::Issuer::new(params, key_pair))
}
fn test_leaf(
ca_issuer: &rcgen::Issuer<'static, RcgenKeyPair>,
advertiser_pub: [u8; 32],
org: &str,
not_after: time::OffsetDateTime,
) -> Vec<u8> {
let subject_spki = rcgen::SubjectPublicKeyInfo::from_der(&ed25519_spki_der(advertiser_pub))
.expect("advertiser SPKI");
let mut params = CertificateParams::new(Vec::<String>::new()).expect("leaf params");
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "test-service");
dn.push(DnType::OrganizationName, org);
params.distinguished_name = dn;
params.not_before = time::OffsetDateTime::now_utc() - time::Duration::hours(1);
params.not_after = not_after;
let cert = params
.signed_by(&subject_spki, ca_issuer)
.expect("leaf signed_by");
cert.der().to_vec()
}
fn ed25519_spki_der(pubkey: [u8; 32]) -> Vec<u8> {
let mut der = vec![
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, ];
der.extend_from_slice(&pubkey);
der
}
fn pem_bundle(ders: &[Vec<u8>]) -> Vec<u8> {
let mut out = Vec::new();
for der in ders {
let b64 = base64_std_encode(der);
out.extend_from_slice(b"-----BEGIN CERTIFICATE-----\n");
for chunk in b64.as_bytes().chunks(64) {
out.extend_from_slice(chunk);
out.push(b'\n');
}
out.extend_from_slice(b"-----END CERTIFICATE-----\n");
}
out
}
fn base64_std_encode(data: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(data)
}
fn advertiser_and_station() -> (KeyPair, KeyPair) {
(KeyPair::generate(), KeyPair::generate())
}
#[test]
fn valid_chain_verifies_and_authorizes() {
let (ca_pem, ca_issuer) = test_ca();
let (advertiser, station) = advertiser_and_station();
let leaf_der = test_leaf(
&ca_issuer,
advertiser.node_id(),
"acme-corp",
time::OffsetDateTime::now_utc() + time::Duration::hours(1),
);
let rec = dht::new_procedure_advertisement_with_cert_chain(
advertiser.node_id(),
"0000/acme-corp/widget.build_v1",
station.node_id(),
Duration::from_secs(3600),
pem_bundle(&[leaf_der]),
);
let rec = dht::sign(rec, &advertiser);
assert_eq!(
verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
Ok(())
);
}
#[test]
fn absent_chain_is_reported_distinctly() {
let (advertiser, station) = advertiser_and_station();
let rec = dht::new_procedure_advertisement(
advertiser.node_id(),
"0000/acme-corp/widget.build_v1",
station.node_id(),
Duration::from_secs(3600),
);
let rec = dht::sign(rec, &advertiser);
let (ca_pem, _) = test_ca();
assert_eq!(
verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
Err(CertChainError::Absent)
);
}
#[test]
fn bad_envelope_signature_is_checked_before_the_chain() {
let (ca_pem, ca_issuer) = test_ca();
let (advertiser, station) = advertiser_and_station();
let leaf_der = test_leaf(
&ca_issuer,
advertiser.node_id(),
"acme-corp",
time::OffsetDateTime::now_utc() + time::Duration::hours(1),
);
let rec = dht::new_procedure_advertisement_with_cert_chain(
advertiser.node_id(),
"0000/acme-corp/widget.build_v1",
station.node_id(),
Duration::from_secs(3600),
pem_bundle(&[leaf_der]),
);
let mut rec = dht::sign(rec, &advertiser);
rec.signature[0] ^= 0xFF;
assert_eq!(
verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
Err(CertChainError::BadSignature)
);
}
#[test]
fn leaf_key_not_matching_the_signer_is_rejected() {
let (ca_pem, ca_issuer) = test_ca();
let (advertiser, station) = advertiser_and_station();
let other = KeyPair::generate();
let leaf_der = test_leaf(
&ca_issuer,
other.node_id(),
"acme-corp",
time::OffsetDateTime::now_utc() + time::Duration::hours(1),
);
let rec = dht::new_procedure_advertisement_with_cert_chain(
advertiser.node_id(),
"0000/acme-corp/widget.build_v1",
station.node_id(),
Duration::from_secs(3600),
pem_bundle(&[leaf_der]),
);
let rec = dht::sign(rec, &advertiser);
assert_eq!(
verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
Err(CertChainError::KeyMismatch)
);
}
#[test]
fn wrong_org_is_rejected_after_a_valid_chain() {
let (ca_pem, ca_issuer) = test_ca();
let (advertiser, station) = advertiser_and_station();
let leaf_der = test_leaf(
&ca_issuer,
advertiser.node_id(),
"acme-corp",
time::OffsetDateTime::now_utc() + time::Duration::hours(1),
);
let rec = dht::new_procedure_advertisement_with_cert_chain(
advertiser.node_id(),
"0000/other-org/widget.build_v1",
station.node_id(),
Duration::from_secs(3600),
pem_bundle(&[leaf_der]),
);
let rec = dht::sign(rec, &advertiser);
assert_eq!(
verify_advertisement_cert_chain(&ca_pem, &rec, "other-org"),
Err(CertChainError::OrgMismatch)
);
}
#[test]
fn expired_leaf_is_untrusted() {
let (ca_pem, ca_issuer) = test_ca();
let (advertiser, station) = advertiser_and_station();
let leaf_der = test_leaf(
&ca_issuer,
advertiser.node_id(),
"acme-corp",
time::OffsetDateTime::now_utc() - time::Duration::hours(1),
);
let rec = dht::new_procedure_advertisement_with_cert_chain(
advertiser.node_id(),
"0000/acme-corp/widget.build_v1",
station.node_id(),
Duration::from_secs(3600),
pem_bundle(&[leaf_der]),
);
let rec = dht::sign(rec, &advertiser);
assert_eq!(
verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
Err(CertChainError::Untrusted)
);
}
#[test]
fn chain_signed_by_a_different_ca_is_untrusted() {
let (_, ca_issuer) = test_ca();
let (other_ca_pem, _) = test_ca();
let (advertiser, station) = advertiser_and_station();
let leaf_der = test_leaf(
&ca_issuer,
advertiser.node_id(),
"acme-corp",
time::OffsetDateTime::now_utc() + time::Duration::hours(1),
);
let rec = dht::new_procedure_advertisement_with_cert_chain(
advertiser.node_id(),
"0000/acme-corp/widget.build_v1",
station.node_id(),
Duration::from_secs(3600),
pem_bundle(&[leaf_der]),
);
let rec = dht::sign(rec, &advertiser);
assert_eq!(
verify_advertisement_cert_chain(&other_ca_pem, &rec, "acme-corp"),
Err(CertChainError::Untrusted)
);
}
#[test]
fn undecodable_chain_is_reported_distinctly() {
let (ca_pem, _) = test_ca();
let (advertiser, station) = advertiser_and_station();
let rec = dht::new_procedure_advertisement_with_cert_chain(
advertiser.node_id(),
"0000/acme-corp/widget.build_v1",
station.node_id(),
Duration::from_secs(3600),
b"not a pem cert bundle".to_vec(),
);
let rec = dht::sign(rec, &advertiser);
assert_eq!(
verify_advertisement_cert_chain(&ca_pem, &rec, "acme-corp"),
Err(CertChainError::Undecodable)
);
}
}