use std::sync::Arc;
use crate::credentials::{ConnectionCredentials, RemoteIdentity};
use crate::fingerprint::{extract_ed25519_raw_key_from_spki, fingerprint_from_cert_der};
use crate::identity::TlsIdentity;
use crate::TlsError;
#[allow(dead_code)]
pub struct TlsClientConfig {
pub(crate) rustls_config: rustls::ClientConfig,
}
impl TlsClientConfig {
pub fn new(credentials: &ConnectionCredentials, alpn: &[u8]) -> Result<Self, TlsError> {
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
let client_auth = build_client_auth(&provider, &credentials.local_identity)?;
let verifier = select_server_verifier(&provider, &credentials.remote_identity)?;
let mut config = rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.map_err(TlsError::from)?
.dangerous()
.with_custom_certificate_verifier(verifier)
.with_client_cert_resolver(client_auth);
config.alpn_protocols = vec![alpn.to_vec()];
config.enable_early_data = true;
Ok(Self {
rustls_config: config,
})
}
#[cfg(feature = "noq")]
pub fn for_noq(self) -> Result<noq::ClientConfig, TlsError> {
let quic_config = noq::crypto::rustls::QuicClientConfig::try_from(self.rustls_config)?;
Ok(noq::ClientConfig::new(Arc::new(quic_config)))
}
pub fn into_rustls_config(self) -> rustls::ClientConfig {
self.rustls_config
}
}
pub fn build_client_auth(
provider: &Arc<rustls::crypto::CryptoProvider>,
tls_identity: &Option<TlsIdentity>,
) -> Result<Arc<dyn rustls::client::ResolvesClientCert>, TlsError> {
match tls_identity {
Some(TlsIdentity::RawKey(secret_key)) => {
let signing_key = Arc::new(crate::signing::Ed25519SigningKey::new(secret_key.clone()));
let spki = signing_key.spki_public_key();
let cert = rustls::pki_types::CertificateDer::from(spki.to_vec());
let certified_key = Arc::new(rustls::sign::CertifiedKey::new(vec![cert], signing_key));
Ok(Arc::new(RawKeyClientCertResolver::new(certified_key)))
}
Some(TlsIdentity::X509 { cert, key }) => {
let cert_chain = crate::pem::load_cert_chain(cert)?;
let key_der = crate::pem::load_private_key(key)?;
let certified_key =
rustls::sign::CertifiedKey::from_der(cert_chain, key_der, provider)?;
Ok(Arc::new(RawKeyClientCertResolver::new(Arc::new(
certified_key,
))))
}
Some(TlsIdentity::SelfSigned) | None => Ok(Arc::new(NoClientCertResolver)),
Some(TlsIdentity::Acme { .. }) => Err(TlsError::AcmeConfig(
"ACME TLS identity is server-only; cannot be used for client auth".to_string(),
)),
}
}
pub fn select_server_verifier(
provider: &Arc<rustls::crypto::CryptoProvider>,
remote_identity: &Option<RemoteIdentity>,
) -> Result<Arc<dyn rustls::client::danger::ServerCertVerifier>, TlsError> {
match remote_identity {
Some(ri) => Ok(Arc::new(FingerprintPinVerifier::new(
ri.fingerprint.clone(),
provider.signature_verification_algorithms,
))),
None => {
let roots = load_platform_root_cert_store()?;
let verifier = rustls::client::WebPkiServerVerifier::builder_with_provider(
Arc::new(roots),
Arc::clone(provider),
)
.build()?;
Ok(verifier)
}
}
}
pub fn load_platform_root_cert_store() -> Result<rustls::RootCertStore, TlsError> {
fill_root_store_from_native(&rustls_native_certs::load_native_certs())
}
fn fill_root_store_from_native(
result: &rustls_native_certs::CertificateResult,
) -> Result<rustls::RootCertStore, TlsError> {
let mut roots = rustls::RootCertStore::empty();
for err in &result.errors {
tracing::warn!(error = ?err, "failed to load a native root cert");
}
for cert in &result.certs {
roots.add(cert.clone())?;
}
if roots.is_empty() {
tracing::info!("platform root cert store is empty, falling back to webpki-roots");
for anchor in webpki_roots::TLS_SERVER_ROOTS.iter() {
roots.roots.push(anchor.to_owned());
}
}
Ok(roots)
}
pub struct RawKeyClientCertResolver {
key: Arc<rustls::sign::CertifiedKey>,
raw_public_keys: bool,
}
impl RawKeyClientCertResolver {
pub fn new(key: Arc<rustls::sign::CertifiedKey>) -> Self {
Self {
key,
raw_public_keys: false,
}
}
}
impl std::fmt::Debug for RawKeyClientCertResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RawKeyClientCertResolver")
.field("raw_public_keys", &self.raw_public_keys)
.finish()
}
}
impl rustls::client::ResolvesClientCert for RawKeyClientCertResolver {
fn resolve(
&self,
_root_hint_subjects: &[&[u8]],
_sigschemes: &[rustls::SignatureScheme],
) -> Option<Arc<rustls::sign::CertifiedKey>> {
Some(Arc::clone(&self.key))
}
fn only_raw_public_keys(&self) -> bool {
self.raw_public_keys
}
fn has_certs(&self) -> bool {
true
}
}
pub struct NoClientCertResolver;
impl std::fmt::Debug for NoClientCertResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NoClientCertResolver").finish()
}
}
impl rustls::client::ResolvesClientCert for NoClientCertResolver {
fn resolve(
&self,
_root_hint_subjects: &[&[u8]],
_sigschemes: &[rustls::SignatureScheme],
) -> Option<Arc<rustls::sign::CertifiedKey>> {
None
}
fn has_certs(&self) -> bool {
false
}
}
pub struct FingerprintPinVerifier {
fingerprint: String,
supported: rustls::crypto::WebPkiSupportedAlgorithms,
}
impl FingerprintPinVerifier {
pub fn new(fingerprint: String, supported: rustls::crypto::WebPkiSupportedAlgorithms) -> Self {
Self {
fingerprint,
supported,
}
}
}
impl std::fmt::Debug for FingerprintPinVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FingerprintPinVerifier")
.field("fingerprint", &self.fingerprint)
.finish()
}
}
impl rustls::client::danger::ServerCertVerifier for FingerprintPinVerifier {
fn verify_server_cert(
&self,
end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
let presented =
fingerprint_from_cert_der(end_entity.as_ref()).ok_or(rustls::Error::General(
"fingerprint pin: failed to compute fingerprint from presented cert".to_string(),
))?;
if presented == self.fingerprint {
Ok(rustls::client::danger::ServerCertVerified::assertion())
} else {
Err(rustls::Error::General(format!(
"fingerprint pin mismatch: expected {} got {}",
self.fingerprint, presented
)))
}
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
if extract_ed25519_raw_key_from_spki(cert.as_ref()).is_some() {
let spki = rustls::pki_types::SubjectPublicKeyInfoDer::from(cert.as_ref().to_vec());
rustls::crypto::verify_tls13_signature_with_raw_key(
message,
&spki,
dss,
&self.supported,
)
} else {
rustls::crypto::verify_tls12_signature(message, cert, dss, &self.supported)
}
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
if extract_ed25519_raw_key_from_spki(cert.as_ref()).is_some() {
let spki = rustls::pki_types::SubjectPublicKeyInfoDer::from(cert.as_ref().to_vec());
rustls::crypto::verify_tls13_signature_with_raw_key(
message,
&spki,
dss,
&self.supported,
)
} else {
rustls::crypto::verify_tls13_signature(message, cert, dss, &self.supported)
}
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.supported.supported_schemes()
}
fn requires_raw_public_keys(&self) -> bool {
self.fingerprint.starts_with("ed25519:")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::Ed25519SecretKey;
fn build_ed25519_spki_der(raw_key: &[u8; 32]) -> Vec<u8> {
let spki = rustls::sign::public_key_to_spki(&rustls::pki_types::alg_id::ED25519, raw_key);
spki.to_vec()
}
fn build_x509_cert_der() -> rustls::pki_types::CertificateDer<'static> {
let key_pair = rcgen::KeyPair::generate().expect("key gen");
let params = rcgen::CertificateParams::default();
let cert = params.self_signed(&key_pair).expect("self-signed cert");
cert.der().clone()
}
fn aws_lc_rs_provider() -> Arc<rustls::crypto::CryptoProvider> {
Arc::new(rustls::crypto::aws_lc_rs::default_provider())
}
fn verify_pin(
verifier: &FingerprintPinVerifier,
cert_der: rustls::pki_types::CertificateDer<'_>,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
use rustls::client::danger::ServerCertVerifier;
let server_name: rustls::pki_types::ServerName<'static> =
"alktls".try_into().expect("server name");
verifier.verify_server_cert(
&cert_der,
&[],
&server_name,
&[],
rustls::pki_types::UnixTime::now(),
)
}
fn dss_with_ed25519_scheme(signature: Vec<u8>) -> rustls::DigitallySignedStruct {
use rustls::internal::msgs::codec::{Codec, Reader};
let mut encoded = Vec::new();
rustls::SignatureScheme::ED25519.encode(&mut encoded);
(signature.len() as u16).encode(&mut encoded);
encoded.extend_from_slice(&signature);
rustls::DigitallySignedStruct::read(&mut Reader::init(&encoded))
.expect("DigitallySignedStruct decodes from its wire encoding")
}
fn dss_with_scheme(
scheme: rustls::SignatureScheme,
signature: Vec<u8>,
) -> rustls::DigitallySignedStruct {
use rustls::internal::msgs::codec::{Codec, Reader};
let mut encoded = Vec::new();
scheme.encode(&mut encoded);
(signature.len() as u16).encode(&mut encoded);
encoded.extend_from_slice(&signature);
rustls::DigitallySignedStruct::read(&mut Reader::init(&encoded))
.expect("DigitallySignedStruct decodes from its wire encoding")
}
#[test]
fn fingerprint_pin_verifier_matches_correct_ed25519_fingerprint() {
let sk = Ed25519SecretKey::generate();
let raw_key = sk.public().to_bytes();
let spki_der = build_ed25519_spki_der(&raw_key);
let fingerprint = fingerprint_from_cert_der(&spki_der).expect("fingerprint");
let verifier = FingerprintPinVerifier::new(
fingerprint,
aws_lc_rs_provider().signature_verification_algorithms,
);
let cert = rustls::pki_types::CertificateDer::from(spki_der);
let result = verify_pin(&verifier, cert);
assert!(
result.is_ok(),
"FingerprintPinVerifier must accept a cert whose fingerprint matches the pin"
);
}
#[test]
fn fingerprint_pin_verifier_rejects_wrong_ed25519_fingerprint() {
let sk = Ed25519SecretKey::generate();
let raw_key = sk.public().to_bytes();
let spki_der = build_ed25519_spki_der(&raw_key);
let other_sk = Ed25519SecretKey::generate();
let other_fp = format!("ed25519:{}", hex::encode(other_sk.public().to_bytes()));
let verifier = FingerprintPinVerifier::new(
other_fp,
aws_lc_rs_provider().signature_verification_algorithms,
);
let cert = rustls::pki_types::CertificateDer::from(spki_der);
let result = verify_pin(&verifier, cert);
assert!(
result.is_err(),
"FingerprintPinVerifier must reject a cert whose fingerprint does not match the pin"
);
}
#[test]
fn fingerprint_pin_verifier_matches_correct_sha256_fingerprint() {
let cert_der = build_x509_cert_der();
let fingerprint = fingerprint_from_cert_der(cert_der.as_ref()).expect("fingerprint");
let verifier = FingerprintPinVerifier::new(
fingerprint,
aws_lc_rs_provider().signature_verification_algorithms,
);
let result = verify_pin(&verifier, cert_der);
assert!(
result.is_ok(),
"FingerprintPinVerifier must accept an X.509 cert whose SHA256 fingerprint matches"
);
}
#[test]
fn fingerprint_pin_verifier_rejects_wrong_sha256_fingerprint() {
let cert_der = build_x509_cert_der();
let verifier = FingerprintPinVerifier::new(
"SHA256:0000000000000000000000000000000000000000000000000000000000000000".to_string(),
aws_lc_rs_provider().signature_verification_algorithms,
);
let result = verify_pin(&verifier, cert_der);
assert!(
result.is_err(),
"FingerprintPinVerifier must reject an X.509 cert whose SHA256 does not match"
);
}
#[test]
fn fingerprint_pin_verifier_routes_ed25519_spki_tls13_signature_through_raw_key_path() {
use rustls::client::danger::ServerCertVerifier;
let sk = Ed25519SecretKey::generate();
let raw_key = sk.public().to_bytes();
let spki_der = build_ed25519_spki_der(&raw_key);
let supported = aws_lc_rs_provider().signature_verification_algorithms;
let verifier =
FingerprintPinVerifier::new(format!("ed25519:{}", hex::encode(raw_key)), supported);
let message = b"alktls tls13 raw-key signature routing";
let signature = sk.sign(message).to_bytes().to_vec();
let dss = dss_with_ed25519_scheme(signature);
let cert = rustls::pki_types::CertificateDer::from(spki_der.clone());
let result = verifier.verify_tls13_signature(message, &cert, &dss);
assert!(
result.is_ok(),
"TLS 1.3 signature for an Ed25519 SPKI cert must route through the raw-key path and verify, got: {result:?}"
);
let forged = dss_with_ed25519_scheme(vec![0u8; 64]);
let tampered = verifier.verify_tls13_signature(b"tampered", &cert, &forged);
assert!(
tampered.is_err(),
"a signature that does not verify must fail the handshake signature check"
);
}
#[test]
fn fingerprint_pin_verifier_verify_tls12_signature_accepts_ed25519_raw_key() {
use rustls::client::danger::ServerCertVerifier;
let sk = Ed25519SecretKey::generate();
let raw_key = sk.public().to_bytes();
let spki_der = build_ed25519_spki_der(&raw_key);
let supported = aws_lc_rs_provider().signature_verification_algorithms;
let verifier =
FingerprintPinVerifier::new(format!("ed25519:{}", hex::encode(raw_key)), supported);
let message = b"alktls tls12 raw-key signature routing";
let signature = sk.sign(message).to_bytes().to_vec();
let dss = dss_with_ed25519_scheme(signature);
let cert = rustls::pki_types::CertificateDer::from(spki_der.clone());
let result = verifier.verify_tls12_signature(message, &cert, &dss);
assert!(
result.is_ok(),
"TLS 1.2 signature for an Ed25519 SPKI cert must route through the raw-key path and verify, got: {result:?}"
);
}
#[test]
fn fingerprint_pin_verifier_verify_tls12_signature_routes_x509_through_standard_path() {
use rustls::client::danger::ServerCertVerifier;
let key_pair = rcgen::KeyPair::generate().expect("ECDSA P-256 key gen");
let cert = rcgen::CertificateParams::default()
.self_signed(&key_pair)
.expect("self-signed cert");
let cert_der = cert.der().clone();
let supported = aws_lc_rs_provider().signature_verification_algorithms;
let pin = fingerprint_from_cert_der(cert_der.as_ref()).expect("SHA256 pin");
let verifier = FingerprintPinVerifier::new(pin, supported);
let signing_key = rustls::crypto::aws_lc_rs::sign::any_ecdsa_type(
&rustls::pki_types::PrivateKeyDer::Pkcs8(rustls::pki_types::PrivatePkcs8KeyDer::from(
key_pair.serialize_der(),
)),
)
.expect("ECDSA signing key loads");
let message = b"alktls tls12 x509 signature routing";
let signature = signing_key
.choose_scheme(&[rustls::SignatureScheme::ECDSA_NISTP256_SHA256])
.expect("ECDSA_NISTP256_SHA256 must be offered")
.sign(message)
.expect("signing must succeed");
let dss = dss_with_scheme(rustls::SignatureScheme::ECDSA_NISTP256_SHA256, signature);
let result = verifier.verify_tls12_signature(message, &cert_der, &dss);
assert!(
result.is_ok(),
"TLS 1.2 signature for a non-Ed25519 (X.509 ECDSA) cert must route \
through the standard verification path and verify, got: {result:?}"
);
let forged = dss_with_scheme(
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
vec![0u8; 70],
);
let tampered = verifier.verify_tls12_signature(message, &cert_der, &forged);
assert!(
tampered.is_err(),
"a forged TLS 1.2 signature must fail the handshake signature check"
);
}
#[test]
fn select_server_verifier_returns_ca_verifier_for_none() {
let provider = aws_lc_rs_provider();
let remote_identity: Option<RemoteIdentity> = None;
let verifier = select_server_verifier(&provider, &remote_identity);
assert!(
verifier.is_ok(),
"select_server_verifier must succeed for None (CA path)"
);
let debug = format!("{:?}", verifier.unwrap());
assert!(
debug.contains("WebPkiServerVerifier"),
"None must select WebPkiServerVerifier (CA verification), got: {debug}"
);
}
#[test]
fn select_server_verifier_returns_fingerprint_pin_for_some() {
let provider = aws_lc_rs_provider();
let remote_identity = Some(RemoteIdentity {
fingerprint: "ed25519:abc".to_string(),
});
let verifier = select_server_verifier(&provider, &remote_identity);
assert!(
verifier.is_ok(),
"select_server_verifier must succeed for Some (fingerprint pin path)"
);
let debug = format!("{:?}", verifier.unwrap());
assert!(
debug.contains("FingerprintPinVerifier"),
"Some must select FingerprintPinVerifier, got: {debug}"
);
}
#[test]
fn build_client_auth_presents_ed25519_raw_key_without_error() {
let provider = aws_lc_rs_provider();
let sk = Ed25519SecretKey::generate();
let tls_identity = Some(TlsIdentity::RawKey(sk));
let resolver = build_client_auth(&provider, &tls_identity);
assert!(
resolver.is_ok(),
"build_client_auth must build a resolver for a RawKey identity"
);
let resolver = resolver.unwrap();
assert!(
resolver.has_certs(),
"RawKey client auth resolver must report it has a cert to present"
);
assert!(
!resolver.only_raw_public_keys(),
"the SPKI presentation goes out under the X.509 offer (ADR-007: \
a raw-only client cert type is rejected by AcceptAnyCertVerifier)"
);
}
#[test]
fn build_client_auth_x509_loads_chain_and_presents_certs() {
let dir = tempfile::tempdir().expect("tempdir");
let key_pair = rcgen::KeyPair::generate().expect("key gen");
let cert = rcgen::CertificateParams::default()
.self_signed(&key_pair)
.expect("self-signed cert");
let cert_path = dir.path().join("cert.pem");
let key_path = dir.path().join("key.pem");
std::fs::write(&cert_path, cert.pem()).expect("write cert");
std::fs::write(&key_path, key_pair.serialize_pem()).expect("write key");
let provider = aws_lc_rs_provider();
let tls_identity = Some(TlsIdentity::X509 {
cert: cert_path,
key: key_path,
});
let resolver = build_client_auth(&provider, &tls_identity);
assert!(
resolver.is_ok(),
"build_client_auth must build a resolver for an X509 identity"
);
let resolver = resolver.unwrap();
assert!(
resolver.has_certs(),
"X509 client auth resolver must report it has a cert to present"
);
assert!(
!resolver.only_raw_public_keys(),
"X509 client auth resolver must present X.509 certs, not raw public keys"
);
}
#[test]
fn build_client_auth_self_signed_resolves_to_no_client_cert() {
let provider = aws_lc_rs_provider();
let tls_identity = Some(TlsIdentity::SelfSigned);
let resolver = build_client_auth(&provider, &tls_identity)
.expect("build_client_auth must succeed for SelfSigned");
assert!(
!resolver.has_certs(),
"SelfSigned must present nothing (NoClientCertResolver, OQ-TLS-02)"
);
}
#[test]
fn build_client_auth_none_resolves_to_no_client_cert() {
let provider = aws_lc_rs_provider();
let tls_identity: Option<TlsIdentity> = None;
let resolver = build_client_auth(&provider, &tls_identity)
.expect("build_client_auth must succeed for None");
assert!(
!resolver.has_certs(),
"NoClientCertResolver must report no certs (no client cert presented)"
);
}
#[test]
fn build_client_auth_acme_is_a_config_error() {
let provider = aws_lc_rs_provider();
let tls_identity = Some(TlsIdentity::Acme {
domains: vec!["example.com".to_string()],
cache_dir: std::path::PathBuf::from("/tmp/alktls-acme-test"),
directory: crate::identity::AcmeDirectory::Staging,
contact: vec!["mailto:ops@example.com".to_string()],
});
let err = build_client_auth(&provider, &tls_identity)
.expect_err("Acme client auth must be a config error");
assert!(
matches!(err, TlsError::AcmeConfig(_)),
"Acme local identity must map to TlsError::AcmeConfig, got: {err:?}"
);
}
#[test]
fn tls_client_config_pins_enable_early_data_true() {
let sk = Ed25519SecretKey::generate();
let credentials = ConnectionCredentials::new()
.with_local_identity(TlsIdentity::RawKey(sk))
.with_remote_identity(RemoteIdentity {
fingerprint: "ed25519:deadbeef".to_string(),
});
let config = TlsClientConfig::new(&credentials, b"alk/call")
.expect("TlsClientConfig::new must build");
let rustls_config = config.into_rustls_config();
assert!(
rustls_config.enable_early_data,
"every client config must enable early data (the client half of the 0-RTT invariant, ADR-001)"
);
assert_eq!(
rustls_config.alpn_protocols,
vec![b"alk/call".to_vec()],
"the config must carry exactly the requested ALPN"
);
}
#[test]
fn tls_client_config_carries_aws_lc_rs_provider() {
let sk = Ed25519SecretKey::generate();
let credentials = ConnectionCredentials::new().with_local_identity(TlsIdentity::RawKey(sk));
let config = TlsClientConfig::new(&credentials, b"alk/call")
.expect("TlsClientConfig::new must build");
let rustls_config = config.into_rustls_config();
let suites: Vec<rustls::CipherSuite> = rustls_config
.crypto_provider()
.cipher_suites
.iter()
.map(|s| s.suite())
.collect();
assert_eq!(
suites.len(),
9,
"the aws-lc-rs default provider must be installed on every config (alknet ADR-084)"
);
assert!(
suites.contains(&rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256),
"the provider must be the aws-lc-rs default set (TLS 1.3 + TLS 1.2 suites), got: {suites:?}"
);
}
#[test]
fn load_platform_root_cert_store_is_never_empty() {
let roots = load_platform_root_cert_store().expect("root store must load");
assert!(
!roots.is_empty(),
"the webpki-roots fallback guarantees a non-empty root store (alknet ADR-088 ยง5)"
);
assert_eq!(
roots.len(),
roots.roots.len(),
"the store's anchor count must match its backing vec"
);
}
fn native_result(
certs: Vec<rustls::pki_types::CertificateDer<'static>>,
errors: usize,
) -> rustls_native_certs::CertificateResult {
let mut result = rustls_native_certs::CertificateResult::default();
result.certs = certs;
for i in 0..errors {
result.errors.push(rustls_native_certs::Error {
context: "test",
kind: rustls_native_certs::ErrorKind::Os(
std::io::Error::other(format!("native load failure {i}")).into(),
),
});
}
result
}
#[test]
fn empty_platform_store_deterministically_falls_back_to_webpki_roots() {
let empty = native_result(Vec::new(), 0);
let roots = fill_root_store_from_native(&empty).expect("fallback must not fail");
assert_eq!(
roots.len(),
webpki_roots::TLS_SERVER_ROOTS.len(),
"an empty platform store must deterministically pull in every \
webpki-roots anchor (alknet ADR-088 ยง5)"
);
assert!(!roots.is_empty());
}
#[test]
fn native_cert_errors_are_logged_and_valid_certs_still_load() {
let key_pair = rcgen::KeyPair::generate().expect("key gen");
let cert = rcgen::CertificateParams::default()
.self_signed(&key_pair)
.expect("self-signed cert");
let der = cert.der().to_owned();
let partial = native_result(vec![der], 2);
let roots = fill_root_store_from_native(&partial).expect("partial load must not fail");
assert_eq!(
roots.len(),
1,
"the valid cert must load despite the sibling errors"
);
}
#[test]
fn native_certs_fill_the_store_without_the_fallback() {
let key_pair = rcgen::KeyPair::generate().expect("key gen");
let cert = rcgen::CertificateParams::default()
.self_signed(&key_pair)
.expect("self-signed cert");
let der = cert.der().to_owned();
let full = native_result(vec![der], 0);
let roots = fill_root_store_from_native(&full).expect("native load must not fail");
assert_eq!(
roots.len(),
1,
"a non-empty platform store must be used verbatim โ no webpki-roots \
fallback on top"
);
}
#[test]
fn verifier_selection_matrix_over_client_config() {
let sk = Ed25519SecretKey::generate();
let pinned = ConnectionCredentials::new()
.with_local_identity(TlsIdentity::RawKey(sk.clone()))
.with_remote_identity(RemoteIdentity {
fingerprint: "ed25519:deadbeef".to_string(),
});
let config =
TlsClientConfig::new(&pinned, b"alk/call").expect("pinned path must construct");
let debug = format!("{:?}", config.rustls_config);
assert!(
debug.contains("FingerprintPinVerifier"),
"Some(remote_identity) must install FingerprintPinVerifier, got: {debug}"
);
let ca_path = ConnectionCredentials::new().with_local_identity(TlsIdentity::RawKey(sk));
let config = TlsClientConfig::new(&ca_path, b"alk/call").expect("CA path must construct");
let debug = format!("{:?}", config.rustls_config);
assert!(
debug.contains("WebPkiServerVerifier"),
"None(remote_identity) must install WebPkiServerVerifier, got: {debug}"
);
}
#[test]
fn client_auth_presentation_matrix_over_client_config() {
let sk = Ed25519SecretKey::generate();
let dir = tempfile::tempdir().expect("tempdir");
let key_pair = rcgen::KeyPair::generate().expect("key gen");
let cert = rcgen::CertificateParams::default()
.self_signed(&key_pair)
.expect("self-signed cert");
let cert_path = dir.path().join("cert.pem");
let key_path = dir.path().join("key.pem");
std::fs::write(&cert_path, cert.pem()).expect("write cert");
std::fs::write(&key_path, key_pair.serialize_pem()).expect("write key");
let matrix: Vec<(ConnectionCredentials, &str)> = vec![
(
ConnectionCredentials::new().with_local_identity(TlsIdentity::RawKey(sk.clone())),
"raw-key",
),
(
ConnectionCredentials::new().with_local_identity(TlsIdentity::X509 {
cert: cert_path,
key: key_path,
}),
"x509",
),
(
ConnectionCredentials::new().with_local_identity(TlsIdentity::SelfSigned),
"self-signed",
),
(ConnectionCredentials::new(), "none"),
];
for (credentials, label) in matrix {
let config = TlsClientConfig::new(&credentials, b"alk/call")
.unwrap_or_else(|e| panic!("{label} presentation must construct: {e}"));
let resolver = &config.rustls_config.client_auth_cert_resolver;
match label {
"raw-key" => {
assert!(
resolver.has_certs() && !resolver.only_raw_public_keys(),
"RawKey must present its SPKI under the X.509 offer (ADR-007)"
);
}
"x509" => {
assert!(
resolver.has_certs() && !resolver.only_raw_public_keys(),
"X509 must present the loaded cert chain"
);
}
_ => {
assert!(
!resolver.has_certs(),
"{label} must present nothing (NoClientCertResolver)"
);
}
}
}
let acme = ConnectionCredentials::new().with_local_identity(TlsIdentity::Acme {
domains: vec!["example.com".to_string()],
cache_dir: dir.path().to_path_buf(),
directory: crate::identity::AcmeDirectory::Production,
contact: vec![],
});
let err = TlsClientConfig::new(&acme, b"alk/call")
.err()
.expect("Acme client auth must fail config construction");
assert!(
matches!(err, TlsError::AcmeConfig(_)),
"Acme local identity must map to TlsError::AcmeConfig, got: {err:?}"
);
}
#[cfg(feature = "noq")]
#[test]
fn build_noq_client_config_with_raw_key_identity_builds_without_error() {
let sk = Ed25519SecretKey::generate();
let credentials = ConnectionCredentials::new()
.with_local_identity(TlsIdentity::RawKey(sk))
.with_remote_identity(RemoteIdentity {
fingerprint: "ed25519:deadbeef".to_string(),
});
let config = TlsClientConfig::new(&credentials, b"alk/call")
.expect("TlsClientConfig::new must build");
let noq_config = config.for_noq().expect("for_noq must convert");
let _ = noq_config;
}
#[cfg(feature = "noq")]
#[test]
fn build_noq_client_config_with_no_remote_identity_builds_without_error() {
let sk = Ed25519SecretKey::generate();
let credentials = ConnectionCredentials::new().with_local_identity(TlsIdentity::RawKey(sk));
let config = TlsClientConfig::new(&credentials, b"alk/call")
.expect("TlsClientConfig::new must build for CA-verification path");
let noq_config = config.for_noq().expect("for_noq must convert");
let _ = noq_config;
}
}