use std::sync::{Arc, Mutex};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::server::danger::{ClientCertVerified, ClientCertVerifier};
use rustls::{DigitallySignedStruct, DistinguishedName, Error as TlsError, SignatureScheme};
use webpki::{anchor_from_trusted_cert, EndEntityCert, KeyUsage};
use crate::binding::{evaluate, verify_binding_from_leaf_cert, BindingPolicy};
use crate::ca::embedded_ca_cert_der;
use crate::error::Result;
use crate::identity::{peer_id_from_leaf_cert_der, PeerId};
#[derive(Debug, Default, Clone)]
pub struct CapturedPeerId(pub Arc<Mutex<Option<PeerId>>>);
impl CapturedPeerId {
pub fn get(&self) -> Option<PeerId> {
*self.0.lock().unwrap()
}
}
#[derive(Debug, Default, Clone)]
pub struct CapturedBlsPub(pub Arc<Mutex<Option<[u8; 48]>>>);
impl CapturedBlsPub {
pub fn get(&self) -> Option<[u8; 48]> {
*self.0.lock().unwrap()
}
}
const CHAIN_SIG_ALGS: &[&dyn webpki::types::SignatureVerificationAlgorithm] = &[
webpki::ring::ECDSA_P256_SHA256,
webpki::ring::ECDSA_P256_SHA384,
webpki::ring::ECDSA_P384_SHA256,
webpki::ring::ECDSA_P384_SHA384,
];
fn verify_chain_to_dig_ca(
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
now: UnixTime,
usage: KeyUsage,
) -> std::result::Result<(), TlsError> {
let ca_der =
embedded_ca_cert_der().map_err(|e| TlsError::General(format!("DigNetwork CA: {e}")))?;
let anchor = anchor_from_trusted_cert(&ca_der).map_err(|e| {
TlsError::General(format!("DigNetwork CA is not a valid trust anchor: {e}"))
})?;
let ee = EndEntityCert::try_from(end_entity)
.map_err(|e| TlsError::General(format!("peer leaf is not a valid certificate: {e}")))?;
ee.verify_for_usage(
CHAIN_SIG_ALGS,
&[anchor],
intermediates,
now,
usage,
None,
None,
)
.map_err(|e| {
TlsError::General(format!(
"peer leaf does not chain to the DigNetwork CA: {e}"
))
})?;
Ok(())
}
fn pin_and_bind(
end_entity: &CertificateDer<'_>,
expected: Option<PeerId>,
captured: &CapturedPeerId,
binding_policy: BindingPolicy,
captured_bls: &CapturedBlsPub,
) -> std::result::Result<PeerId, TlsError> {
let derived = peer_id_from_leaf_cert_der(end_entity.as_ref()).ok_or_else(|| {
TlsError::General("peer leaf certificate could not be parsed as X.509".to_string())
})?;
*captured.0.lock().unwrap() = Some(derived);
if let Some(expected) = expected {
if derived != expected {
return Err(TlsError::General(format!(
"peer_id mismatch: expected {expected}, got {derived}"
)));
}
}
if binding_policy != BindingPolicy::Off {
let outcome = verify_binding_from_leaf_cert(end_entity.as_ref());
match evaluate(&outcome, binding_policy) {
Ok(bls_pub) => *captured_bls.0.lock().unwrap() = bls_pub,
Err(reason) => {
return Err(TlsError::General(format!(
"peer {derived} rejected by cert BLS binding policy: {reason}"
)))
}
}
}
Ok(derived)
}
fn default_signature_schemes() -> Vec<SignatureScheme> {
rustls::crypto::ring::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
fn verify_tls12(
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, TlsError> {
rustls::crypto::verify_tls12_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn verify_tls13(
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, TlsError> {
rustls::crypto::verify_tls13_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
#[derive(Debug)]
pub struct DigServerCertVerifier {
expected: Option<PeerId>,
captured: CapturedPeerId,
binding_policy: BindingPolicy,
captured_bls: CapturedBlsPub,
schemes: Vec<SignatureScheme>,
require_ca_chain: bool,
}
impl DigServerCertVerifier {
pub fn new(
expected: Option<PeerId>,
captured: CapturedPeerId,
binding_policy: BindingPolicy,
captured_bls: CapturedBlsPub,
) -> Self {
Self {
expected,
captured,
binding_policy,
captured_bls,
schemes: default_signature_schemes(),
require_ca_chain: true,
}
}
pub fn new_spki_pinned(
expected: Option<PeerId>,
captured: CapturedPeerId,
binding_policy: BindingPolicy,
captured_bls: CapturedBlsPub,
) -> Self {
Self {
expected,
captured,
binding_policy,
captured_bls,
schemes: default_signature_schemes(),
require_ca_chain: false,
}
}
}
impl ServerCertVerifier for DigServerCertVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
now: UnixTime,
) -> std::result::Result<ServerCertVerified, TlsError> {
if self.require_ca_chain {
verify_chain_to_dig_ca(end_entity, intermediates, now, KeyUsage::server_auth())?;
}
pin_and_bind(
end_entity,
self.expected,
&self.captured,
self.binding_policy,
&self.captured_bls,
)?;
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, TlsError> {
verify_tls12(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, TlsError> {
verify_tls13(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.schemes.clone()
}
}
#[derive(Debug)]
pub struct DigClientCertVerifier {
expected: Option<PeerId>,
captured: CapturedPeerId,
binding_policy: BindingPolicy,
captured_bls: CapturedBlsPub,
schemes: Vec<SignatureScheme>,
root_hints: Vec<DistinguishedName>,
require_ca_chain: bool,
}
impl DigClientCertVerifier {
pub fn new(
expected: Option<PeerId>,
captured: CapturedPeerId,
binding_policy: BindingPolicy,
captured_bls: CapturedBlsPub,
) -> Self {
Self {
expected,
captured,
binding_policy,
captured_bls,
schemes: default_signature_schemes(),
root_hints: Vec::new(),
require_ca_chain: true,
}
}
pub fn new_spki_pinned(
expected: Option<PeerId>,
captured: CapturedPeerId,
binding_policy: BindingPolicy,
captured_bls: CapturedBlsPub,
) -> Self {
Self {
expected,
captured,
binding_policy,
captured_bls,
schemes: default_signature_schemes(),
root_hints: Vec::new(),
require_ca_chain: false,
}
}
}
impl ClientCertVerifier for DigClientCertVerifier {
fn offer_client_auth(&self) -> bool {
true
}
fn client_auth_mandatory(&self) -> bool {
true
}
fn root_hint_subjects(&self) -> &[DistinguishedName] {
&self.root_hints
}
fn verify_client_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
now: UnixTime,
) -> std::result::Result<ClientCertVerified, TlsError> {
if self.require_ca_chain {
verify_chain_to_dig_ca(end_entity, intermediates, now, KeyUsage::client_auth())?;
}
pin_and_bind(
end_entity,
self.expected,
&self.captured,
self.binding_policy,
&self.captured_bls,
)?;
Ok(ClientCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, TlsError> {
verify_tls12(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> std::result::Result<HandshakeSignatureValid, TlsError> {
verify_tls13(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.schemes.clone()
}
}
pub fn dig_ca_trust_anchor_der() -> Result<CertificateDer<'static>> {
embedded_ca_cert_der()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bls::{public_key_bytes, SecretKey};
use crate::ca::DigCa;
use crate::node_cert::NodeCert;
use rcgen::{
CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, Ia5String, KeyPair,
KeyUsagePurpose, SanType, PKCS_ECDSA_P256_SHA256,
};
use sha2::{Digest, Sha256};
use time::OffsetDateTime;
fn bls_sk(label: &str) -> SecretKey {
let seed: [u8; 32] = Sha256::digest(label.as_bytes()).into();
SecretKey::from_seed(&seed)
}
fn unbound_dig_ca_leaf() -> CertificateDer<'static> {
let ca = DigCa::embedded().expect("embedded CA");
let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
let mut params = CertificateParams::new(Vec::<String>::new()).unwrap();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "peer.dig");
params.distinguished_name = dn;
params.subject_alt_names = vec![SanType::DnsName(
Ia5String::try_from("peer.dig".to_string()).unwrap(),
)];
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
params.extended_key_usages = vec![
ExtendedKeyUsagePurpose::ServerAuth,
ExtendedKeyUsagePurpose::ClientAuth,
];
let cert = params.signed_by(&leaf_key, &ca.cert, &ca.key).unwrap();
CertificateDer::from(cert.der().to_vec())
}
#[test]
fn required_rejects_ca_signed_unbound_leaf() {
let leaf = unbound_dig_ca_leaf();
let v = DigClientCertVerifier::new(
None,
CapturedPeerId::default(),
BindingPolicy::Required,
CapturedBlsPub::default(),
);
let err = v
.verify_client_cert(&leaf, &[], UnixTime::now())
.expect_err("Required rejects an unbound leaf");
assert!(
format!("{err}").contains("BLS binding"),
"rejected on the binding, not the chain"
);
}
#[test]
fn required_accepts_bound_leaf_and_captures_identity() {
let sk = bls_sk("verify/bound");
let node = NodeCert::generate_signed(&sk).expect("node cert");
let leaf = CertificateDer::from(node.cert_der().to_vec());
let captured = CapturedPeerId::default();
let captured_bls = CapturedBlsPub::default();
let v = DigClientCertVerifier::new(
None,
captured.clone(),
BindingPolicy::Required,
captured_bls.clone(),
);
v.verify_client_cert(&leaf, &[], UnixTime::now())
.expect("a bound DIG-CA leaf verifies");
assert_eq!(captured.get(), Some(node.peer_id()));
assert_eq!(captured_bls.get(), Some(public_key_bytes(&sk)));
}
fn self_signed_bound_leaf(label: &str) -> (CertificateDer<'static>, PeerId, [u8; 48]) {
let sk = bls_sk(label);
let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
let mut params = CertificateParams::new(Vec::<String>::new()).unwrap();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "peer.dig");
params.distinguished_name = dn;
params.subject_alt_names = vec![SanType::DnsName(
Ia5String::try_from("peer.dig".to_string()).unwrap(),
)];
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
params.extended_key_usages = vec![
ExtendedKeyUsagePurpose::ServerAuth,
ExtendedKeyUsagePurpose::ClientAuth,
];
crate::binding::attach_binding(&mut params, &leaf_key, &sk);
let cert = params.self_signed(&leaf_key).unwrap();
let der = CertificateDer::from(cert.der().to_vec());
let peer_id = peer_id_from_leaf_cert_der(der.as_ref()).unwrap();
(der, peer_id, public_key_bytes(&sk))
}
fn self_signed_unbound_leaf() -> (CertificateDer<'static>, PeerId) {
let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
let mut params = CertificateParams::new(Vec::<String>::new()).unwrap();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "peer.dig");
params.distinguished_name = dn;
params.subject_alt_names = vec![SanType::DnsName(
Ia5String::try_from("peer.dig".to_string()).unwrap(),
)];
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
params.extended_key_usages = vec![
ExtendedKeyUsagePurpose::ServerAuth,
ExtendedKeyUsagePurpose::ClientAuth,
];
let cert = params.self_signed(&leaf_key).unwrap();
let der = CertificateDer::from(cert.der().to_vec());
let peer_id = peer_id_from_leaf_cert_der(der.as_ref()).unwrap();
(der, peer_id)
}
#[test]
fn spki_pinned_accepts_self_signed_leaf() {
let (leaf, peer_id, _bls) = self_signed_bound_leaf("verify/spki-self-signed");
let captured = CapturedPeerId::default();
let v = DigServerCertVerifier::new_spki_pinned(
None,
captured.clone(),
BindingPolicy::Opportunistic,
CapturedBlsPub::default(),
);
let name = ServerName::try_from("peer.dig").unwrap();
v.verify_server_cert(&leaf, &[], &name, &[], UnixTime::now())
.expect("SPKI-pinned mode accepts a self-signed leaf");
assert_eq!(captured.get(), Some(peer_id));
let ca_v = DigServerCertVerifier::new(
None,
CapturedPeerId::default(),
BindingPolicy::Opportunistic,
CapturedBlsPub::default(),
);
let err = ca_v
.verify_server_cert(&leaf, &[], &name, &[], UnixTime::now())
.expect_err("CA-requiring mode rejects a self-signed leaf");
assert!(
format!("{err}").contains("DigNetwork CA"),
"rejected on the chain, not something else: {err}"
);
}
#[test]
fn spki_pinned_rejects_wrong_peer_id() {
let (leaf, _peer_id, _bls) = self_signed_bound_leaf("verify/spki-wrong-pin");
let wrong = PeerId::from_bytes([0x22u8; 32]);
let v = DigServerCertVerifier::new_spki_pinned(
Some(wrong),
CapturedPeerId::default(),
BindingPolicy::Opportunistic,
CapturedBlsPub::default(),
);
let name = ServerName::try_from("peer.dig").unwrap();
let err = v
.verify_server_cert(&leaf, &[], &name, &[], UnixTime::now())
.expect_err("a wrong-peer_id pin is rejected even in SPKI-pinned mode");
assert!(
format!("{err}").contains("peer_id mismatch"),
"rejected on the pin: {err}"
);
}
#[test]
fn spki_pinned_live_case_unbound_self_signed_under_opportunistic() {
let (leaf, peer_id) = self_signed_unbound_leaf();
let captured = CapturedPeerId::default();
let captured_bls = CapturedBlsPub::default();
let opp = DigClientCertVerifier::new_spki_pinned(
None,
captured.clone(),
BindingPolicy::Opportunistic,
captured_bls.clone(),
);
opp.verify_client_cert(&leaf, &[], UnixTime::now())
.expect("Opportunistic accepts an unbound self-signed leaf");
assert_eq!(captured.get(), Some(peer_id));
assert_eq!(captured_bls.get(), None);
let req = DigClientCertVerifier::new_spki_pinned(
None,
CapturedPeerId::default(),
BindingPolicy::Required,
CapturedBlsPub::default(),
);
let err = req
.verify_client_cert(&leaf, &[], UnixTime::now())
.expect_err("Required rejects an unbound leaf");
assert!(
format!("{err}").contains("BLS binding"),
"rejected on the binding, not the chain: {err}"
);
}
#[test]
fn foreign_ca_leaf_fails_chain() {
let foreign = crate::ca::generate_dig_ca(OffsetDateTime::now_utc()).unwrap();
let foreign_ca = DigCa::from_pem(&foreign.cert_pem, &foreign.key_pem).unwrap();
let node = NodeCert::generate_signed_by(
&foreign_ca,
&bls_sk("verify/foreign"),
OffsetDateTime::now_utc(),
)
.unwrap();
let leaf = CertificateDer::from(node.cert_der().to_vec());
let v = DigServerCertVerifier::new(
None,
CapturedPeerId::default(),
BindingPolicy::Off,
CapturedBlsPub::default(),
);
let name = ServerName::try_from("peer.dig").unwrap();
let err = v
.verify_server_cert(&leaf, &[], &name, &[], UnixTime::now())
.expect_err("foreign CA leaf is rejected");
assert!(format!("{err}").contains("DigNetwork CA"));
}
}