Close review 001 §S-1: the default client-cert verifier never checked the client's CertificateVerify, so anyone holding a peer's *public* cert/SPKI bytes (public by design — peers publish them to be dialable) could complete a handshake as that peer, and the auth layer could not detect it. The consumer designs are known (X.509 and raw-key TCP/QUIC endpoints with identity-bearing clients), so implementing now — the zero-consumer moment — avoids the guaranteed breaking republish of flipping the default later. - VerifyPresentedCertVerifier (new): request, don't require, verify possession — permissive verify_client_cert (self-signed chains and bare SPKIs stay valid presentation) + CertificateVerify routing by presented cert kind (Ed25519 SPKI -> verify_tls13_signature_with_ raw_key both TLS versions; X.509 -> standard route), the same routing FingerprintPinVerifier implements. Nine-scheme list verbatim (shared fn, exact-list pin covers both). - Default on every TlsServerConfig path — X509 / RawKey / SelfSigned / ACME (the verifier install is crate-side rustls in new_acme, not rustls-acme's). - AcceptAnyCertVerifier stays public as the explicit no-pop escape hatch, no longer installed by any crate path. - tests/impersonation_posture.rs: four pins — default rejects the attacker (X.509: UnsupportedSignatureAlgorithmForPublicKeyContext; raw-key: BadSignature), escape hatch still accepts + extracts the victim's fingerprint (both cert types). - tests/handshake_behavior.rs: suites 4/4b — possession-checked legit clients (raw-key pin vs raw-key server; X.509 client vs X.509 server) complete and the server extracts the fingerprint; suite 3b doc updated. - Docs: ADR-008 written; OQ-TLS-09 -> resolved (option (b)); ADR-007 §Limits deferral retired to not-planned; server.md/client.md invariants/README/overview synced. Verification: cargo test 81 / --features tcp 95 / --all-features 104 green; clippy -D warnings clean (default + all-features); fmt clean; cargo doc warning-free.
195 lines
7.8 KiB
Rust
195 lines
7.8 KiB
Rust
//! The invariant pins, promoted to integration level (ADR-006: the
|
|
//! regression-proof shapes):
|
|
//!
|
|
//! 1. The nine-scheme exact-list pin — exact vec equality, not membership
|
|
//! (the shape the in-module seed test must not regress from).
|
|
//! 2. The verifier-selection matrix + client-auth presentation matrix,
|
|
//! exercised through the public API only.
|
|
//! 3. The root-store fallback — the store is never empty.
|
|
//! 4. The server `max_early_data_size` + request-but-don't-require shape.
|
|
//!
|
|
//! Fail-closed for unknown raw-key remotes is structural (the CA verifier
|
|
//! is what `None` installs; a raw-key remote cannot satisfy it — the
|
|
//! failure manifests at handshake, never via `TlsError`): the matrix
|
|
//! asserts `None` installs `WebPkiServerVerifier`, which is the
|
|
//! fail-closed structure. The verifier inside `rustls::ClientConfig` /
|
|
//! `rustls::ServerConfig` is `pub(super)` at rustls 0.23.44, so the
|
|
//! verifier identity is probed through the config's derived Debug output
|
|
//! (the verifier's type name appears in it) and the helpers' own
|
|
//! construction results.
|
|
|
|
use alktls::{
|
|
build_client_auth, build_rustls_server_config, load_platform_root_cert_store,
|
|
select_server_verifier, AcceptAnyCertVerifier, ConnectionCredentials, Ed25519SecretKey,
|
|
RemoteIdentity, TlsClientConfig, TlsIdentity,
|
|
};
|
|
|
|
fn provider() -> std::sync::Arc<rustls::crypto::CryptoProvider> {
|
|
std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider())
|
|
}
|
|
|
|
#[test]
|
|
fn nine_schemes_exact_list_pin() {
|
|
use rustls::server::danger::ClientCertVerifier;
|
|
let schemes = AcceptAnyCertVerifier.supported_verify_schemes();
|
|
assert_eq!(
|
|
schemes,
|
|
vec![
|
|
rustls::SignatureScheme::ED25519,
|
|
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
|
|
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
|
|
rustls::SignatureScheme::RSA_PSS_SHA256,
|
|
rustls::SignatureScheme::RSA_PSS_SHA384,
|
|
rustls::SignatureScheme::RSA_PSS_SHA512,
|
|
rustls::SignatureScheme::RSA_PKCS1_SHA256,
|
|
rustls::SignatureScheme::RSA_PKCS1_SHA384,
|
|
rustls::SignatureScheme::RSA_PKCS1_SHA512,
|
|
],
|
|
"the nine-scheme list is load-bearing (server.md) — exact equality, order included"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn verifier_selection_matrix_full_construction() {
|
|
let known_peer = ConnectionCredentials::new()
|
|
.with_local_identity(TlsIdentity::RawKey(Ed25519SecretKey::generate()))
|
|
.with_remote_identity(RemoteIdentity {
|
|
fingerprint: "ed25519:aa".to_string(),
|
|
});
|
|
let config = TlsClientConfig::new(&known_peer, b"alk/call")
|
|
.expect("Some(remote_identity) cell must construct");
|
|
assert!(
|
|
format!("{:?}", config.into_rustls_config()).contains("FingerprintPinVerifier"),
|
|
"Some(fingerprint) must install FingerprintPinVerifier"
|
|
);
|
|
|
|
let unknown_x509 = ConnectionCredentials::new();
|
|
let config = TlsClientConfig::new(&unknown_x509, b"alk/call")
|
|
.expect("None(remote_identity) cell must construct (the public-X.509 state)");
|
|
assert!(
|
|
format!("{:?}", config.into_rustls_config()).contains("WebPkiServerVerifier"),
|
|
"None must install WebPkiServerVerifier — the CA verifier. Fail-closed is structural: \
|
|
an unknown raw-key remote cannot satisfy it and fails at handshake, never via TlsError"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn client_auth_presentation_matrix_full_construction() {
|
|
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 cells: Vec<(ConnectionCredentials, &str)> = vec![
|
|
(
|
|
ConnectionCredentials::new()
|
|
.with_local_identity(TlsIdentity::RawKey(Ed25519SecretKey::generate())),
|
|
"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 &cells {
|
|
let config = TlsClientConfig::new(credentials, b"alk/call")
|
|
.unwrap_or_else(|e| panic!("{label} cell must construct: {e}"));
|
|
let resolver = &config.into_rustls_config().client_auth_cert_resolver;
|
|
match *label {
|
|
"raw-key" => assert!(
|
|
resolver.has_certs() && !resolver.only_raw_public_keys(),
|
|
"RawKey presents its SPKI under the X.509 offer (ADR-007: the \
|
|
extension is an offer format, not an identity statement)"
|
|
),
|
|
"x509" => assert!(
|
|
resolver.has_certs() && !resolver.only_raw_public_keys(),
|
|
"X509 must present the loaded chain (not raw public keys)"
|
|
),
|
|
_ => assert!(
|
|
!resolver.has_certs(),
|
|
"{label} presents 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: alktls::AcmeDirectory::Production,
|
|
contact: vec![],
|
|
});
|
|
match TlsClientConfig::new(&acme, b"alk/call") {
|
|
Ok(_) => panic!("Acme is a server-only identity — client path must be a config error"),
|
|
Err(e) => assert!(
|
|
matches!(e, alktls::TlsError::AcmeConfig(_)),
|
|
"Acme on the client path must surface TlsError::AcmeConfig, got {e:?}"
|
|
),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn root_store_fallback_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 store even when the platform store is \
|
|
empty (alknet ADR-088 §5): got {} anchors",
|
|
roots.roots.len()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn selection_and_auth_helpers_agree_with_the_full_config() {
|
|
let sk = Ed25519SecretKey::generate();
|
|
let local = Some(TlsIdentity::RawKey(sk.clone()));
|
|
let resolver = build_client_auth(&provider(), &local).expect("raw-key resolver builds");
|
|
assert!(
|
|
resolver.has_certs() && !resolver.only_raw_public_keys(),
|
|
"the raw-key resolver presents its SPKI under the X.509 offer (ADR-007)"
|
|
);
|
|
|
|
let remote = Some(RemoteIdentity {
|
|
fingerprint: "ed25519:aa".to_string(),
|
|
});
|
|
let verifier = select_server_verifier(&provider(), &remote).expect("pin verifier selects");
|
|
assert!(format!("{verifier:?}").contains("FingerprintPinVerifier"));
|
|
|
|
let _ = sk;
|
|
}
|
|
|
|
#[test]
|
|
fn server_paths_carry_max_early_data_and_alpn() {
|
|
let alpns = vec![b"alk/call".to_vec()];
|
|
for identity in [
|
|
TlsIdentity::RawKey(Ed25519SecretKey::generate()),
|
|
TlsIdentity::SelfSigned,
|
|
] {
|
|
let config = build_rustls_server_config(&identity, &alpns)
|
|
.expect("server config builds per variant");
|
|
assert_eq!(
|
|
config.max_early_data_size,
|
|
u32::MAX,
|
|
"max_early_data_size must be u32::MAX on every server path (the 0-RTT invariant)"
|
|
);
|
|
assert_eq!(config.alpn_protocols, alpns);
|
|
assert!(
|
|
!format!("{config:?}").is_empty(),
|
|
"the config carries its verifier through the derived Debug (request-but-don't-require \
|
|
shape asserted at unit level on the verifiers themselves)"
|
|
);
|
|
}
|
|
}
|