generation 3: port server + client — the two big ports
port-server (src/server.rs, 614 lines): - TlsServerConfig with ADR-004 accessors: for_noq(&self) (borrow + inner-clone + NoqWrap via #[from]), for_tcp_tls(&self) adopted (infallible TlsAcceptor), rustls_config(&self) adopted - acme_handle renamed per ADR-006; the defensive Acme arm returns TlsError::AcmeConfig (unreachable! eliminated) - error sites remapped to typed variants (Rustls/SelfSigned/AcmeConfig) - ACME path verbatim: DirCache + directory + contacts + resolver + acme-tls/1 + spawned event loop (tracing lines ported), returns immediately, detached handle - 14 tests incl. the nine-scheme exact-list pin, ACME spawn/return/ALPN assertion (blackhole URL — no network I/O), no-feature AcmeConfig, for_tcp_tls/rustls_config round-trips port-client (src/credentials.rs + src/client.rs): - ConnectionCredentials/RemoteIdentity wholesale with load-bearing Option-semantics docs (None = public-X.509 state, Some = pin) - verifier selection matrix + client-auth presentation matrix test-pinned at unit level; enable_early_data=true pinned; root-store fallback asserted non-empty - FingerprintPinVerifier: pin match/mismatch + raw-key signature routing (verify_tls13_signature_with_raw_key) asserted - resolvers/verifier made pub for the re-export block Cargo.toml (two required deltas): - noq feature gains aws-lc-rs: lockfile resolves noq-proto 1.3.0 where ServerConfig::with_crypto is #[cfg(any(aws-lc-rs, ring))]; ADR-003's TOML block was written against the 1.2 API. Amendment note recorded in tasks/port-server.md for the review-impl sync - acme = [dep:rustls-acme, dep:futures] — ADR-006 already gates the futures dep on acme; the scaffold omitted it lib.rs: re-export block complete (all eight modules) Verification: cargo test 68, --all-features 75, --features noq 72, clippy -D warnings, fmt --check, doc --no-deps (0 warnings) — green
This commit is contained in:
Generated
+1
@@ -996,6 +996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c1e5b6fe668491eca022f745a0a9402585626c73a7b839b3424ace15d6a9c8f"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aws-lc-rs",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"enum-assoc",
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ name = "alktls"
|
||||
default = []
|
||||
noq = ["dep:noq", "dep:noq-proto"]
|
||||
tcp = ["dep:tokio-rustls"]
|
||||
acme = ["dep:rustls-acme"]
|
||||
acme = ["dep:rustls-acme", "dep:futures"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", default-features = false, features = ["rt", "sync", "macros"] }
|
||||
@@ -35,7 +35,7 @@ tracing = "0.1"
|
||||
thiserror = "2"
|
||||
|
||||
futures = { version = "0.3", optional = true }
|
||||
noq = { version = "1.2", optional = true, default-features = false, features = ["rustls"] }
|
||||
noq = { version = "1.2", optional = true, default-features = false, features = ["rustls", "aws-lc-rs"] }
|
||||
noq-proto = { version = "1.2", optional = true, default-features = false }
|
||||
tokio-rustls = { version = "0.26", optional = true }
|
||||
rustls-acme = { version = "0.12", optional = true, features = ["aws-lc-rs"] }
|
||||
|
||||
+796
@@ -2,3 +2,799 @@
|
||||
//! [`FingerprintPinVerifier`], [`RawKeyClientCertResolver`],
|
||||
//! [`NoClientCertResolver`], [`select_server_verifier`],
|
||||
//! [`build_client_auth`], [`load_platform_root_cert_store`].
|
||||
|
||||
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;
|
||||
|
||||
/// Client-side TLS configuration, transport-agnostic.
|
||||
/// Wraps a `rustls::ClientConfig` built from `ConnectionCredentials`.
|
||||
///
|
||||
/// Built per dial from a [`ConnectionCredentials`] + ALPN and consumed by
|
||||
/// its accessors — a `TlsClientConfig` is not reused across dials (ADR-004).
|
||||
#[allow(dead_code)]
|
||||
pub struct TlsClientConfig {
|
||||
pub(crate) rustls_config: rustls::ClientConfig,
|
||||
}
|
||||
|
||||
impl TlsClientConfig {
|
||||
/// Build a client config from `ConnectionCredentials` and an ALPN.
|
||||
/// Selects the server cert verifier by `remote_identity` presence
|
||||
/// (alknet ADR-034 §3): `Some` → fingerprint pin, `None` → CA
|
||||
/// verification.
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert to a `noq::ClientConfig` for QUIC transport. Consumes —
|
||||
/// one dial = one config build (ADR-004).
|
||||
#[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)))
|
||||
}
|
||||
|
||||
/// Consume the config and return the inner `rustls::ClientConfig`.
|
||||
/// Used by the TCP+TLS dial to build a `TlsConnector`. Consumes —
|
||||
/// zero-cost, no clone behind the scenes (ADR-004).
|
||||
pub fn into_rustls_config(self) -> rustls::ClientConfig {
|
||||
self.rustls_config
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the client-auth cert resolver that presents the local node's TLS
|
||||
/// identity. For `TlsIdentity::RawKey` the Ed25519 key is presented as an RFC
|
||||
/// 7250 raw public key client cert (`only_raw_public_keys() == true`) — the
|
||||
/// client-side equivalent of the server's `RawKeyCertResolver`. For X.509 the
|
||||
/// cert chain + key are loaded from disk. `None` (no `local_identity` configured)
|
||||
/// resolves to no client cert (the server gets nothing to fingerprint).
|
||||
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(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the server cert verifier by `remote_identity` presence (alknet
|
||||
/// ADR-034 §3).
|
||||
///
|
||||
/// - `Some(fingerprint)` → known peer → `FingerprintPinVerifier` (fingerprint
|
||||
/// match). The fingerprint IS the trust anchor.
|
||||
/// - `None` → no prior knowledge of the remote → `WebPkiServerVerifier` (CA
|
||||
/// verification) for X.509 remotes. For Ed25519 raw-key remotes the
|
||||
/// `WebPkiServerVerifier` fails closed at handshake time (raw-key remotes
|
||||
/// have no CA to fall back to — alknet ADR-034 §2 assumption 1). `None` is
|
||||
/// the public-X.509-endpoint state, not "skip verification."
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the platform's trusted root certificates into a `RootCertStore` for
|
||||
/// `WebPkiServerVerifier` (the `None` + X.509 CA-verification path). Falls back
|
||||
/// to the built-in `webpki-roots` if the platform store is empty (e.g. in a
|
||||
/// container with no system CA bundle) — alknet ADR-088 §5.
|
||||
pub fn load_platform_root_cert_store() -> Result<rustls::RootCertStore, TlsError> {
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
let result = rustls_native_certs::load_native_certs();
|
||||
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)
|
||||
}
|
||||
|
||||
/// Client cert resolver that presents a single RFC 7250 raw public key (or
|
||||
/// X.509 cert chain). For raw keys `only_raw_public_keys()` returns `true` so
|
||||
/// rustls negotiates the RFC 7250 ClientCertificateType extension.
|
||||
pub struct RawKeyClientCertResolver {
|
||||
key: Arc<rustls::sign::CertifiedKey>,
|
||||
raw_public_keys: bool,
|
||||
}
|
||||
|
||||
impl RawKeyClientCertResolver {
|
||||
pub fn new(key: Arc<rustls::sign::CertifiedKey>) -> Self {
|
||||
let raw_public_keys = key.cert.len() == 1 && is_ed25519_spki(&key.cert[0]);
|
||||
Self {
|
||||
key,
|
||||
raw_public_keys,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_ed25519_spki(cert_der: &rustls::pki_types::CertificateDer<'_>) -> bool {
|
||||
extract_ed25519_raw_key_from_spki(cert_der.as_ref()).is_some()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// Client cert resolver that presents no client cert (the `local_identity: None`
|
||||
/// or `SelfSigned` path). The server gets nothing to fingerprint — the
|
||||
/// fingerprint → peer-id resolution path is not activated for this connection.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// `ServerCertVerifier` that pins a specific fingerprint (alknet ADR-034 §3, the
|
||||
/// known-peer path). For `ed25519:<hex>` remotes the raw Ed25519 pub key is
|
||||
/// extracted from the presented cert and matched against the pinned
|
||||
/// fingerprint; for `SHA256:<hex>` remotes the cert DER is hashed and matched
|
||||
/// against the pinned fingerprint. No match → verification failure (the
|
||||
/// connection is rejected). The fingerprint IS the trust anchor — there is no
|
||||
/// CA verification and no name verification, only the fingerprint pin.
|
||||
///
|
||||
/// Handshake signatures are still verified (using the aws-lc-rs default
|
||||
/// signature verification algorithms) so that a stolen-but-stale fingerprint
|
||||
/// can't be replayed with a forged signature: the presenter must prove
|
||||
/// possession of the private key corresponding to the pinned public key.
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[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")
|
||||
}
|
||||
|
||||
#[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 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.only_raw_public_keys(),
|
||||
"RawKey client auth resolver must present raw public keys (RFC 7250)"
|
||||
);
|
||||
assert!(
|
||||
resolver.has_certs(),
|
||||
"RawKey client auth resolver must report it has a cert to present"
|
||||
);
|
||||
}
|
||||
|
||||
#[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"
|
||||
);
|
||||
}
|
||||
|
||||
#[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 a raw public key (RFC 7250)"
|
||||
);
|
||||
}
|
||||
"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 = match TlsClientConfig::new(&acme, b"alk/call") {
|
||||
Ok(_) => panic!("Acme client auth must fail config construction"),
|
||||
Err(e) => e,
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+107
-1
@@ -1,3 +1,109 @@
|
||||
//! Transport-level credential bundle for outbound connections:
|
||||
//! [`ConnectionCredentials`], [`RemoteIdentity`] (ADR-005, moved from
|
||||
//! alknet-core `credentials.rs`).
|
||||
//! alknet-core `credentials.rs`; alknet ADR-091's semantics).
|
||||
//!
|
||||
//! [`ConnectionCredentials`] carries the two dimensions the client config
|
||||
//! consumes: the local node's identity and the expected remote identity.
|
||||
//! It is transport-agnostic — consumed by this crate's
|
||||
//! [`TlsClientConfig`](crate::TlsClientConfig) (TLS setup) and by the dial
|
||||
//! seam in the consumers.
|
||||
|
||||
use crate::identity::TlsIdentity;
|
||||
|
||||
/// Expected identity of the remote node (alknet ADR-017 §7, extended by
|
||||
/// alknet ADR-034 §2; ADR-005 moved the type here).
|
||||
///
|
||||
/// Carries a fingerprint string the assembly layer derives when the local
|
||||
/// node knows the remote (the known-peer case → fingerprint pin).
|
||||
///
|
||||
/// `remote_identity: None` is the **public X.509 endpoint** case: the local
|
||||
/// node has no prior knowledge of the remote, so there is no fingerprint to
|
||||
/// pin. Combined with an X.509 remote, `None` selects CA verification
|
||||
/// ([`WebPkiServerVerifier`] per the verifier-selection rule in alknet
|
||||
/// ADR-034 §3). Combined with an Ed25519 raw-key remote, `None` fails closed
|
||||
/// at handshake (raw-key remotes are always known peers — no CA to fall
|
||||
/// back to).
|
||||
///
|
||||
/// The `Option` is therefore load-bearing, not cosmetic:
|
||||
/// `Some(fingerprint)` means "pin this" (known peer), `None` means "trust
|
||||
/// the CA or fail" (unknown remote). An implementer must not default
|
||||
/// `remote_identity` to a placeholder value to "satisfy" the field — `None`
|
||||
/// is a real state that drives verifier selection.
|
||||
///
|
||||
/// [`WebPkiServerVerifier`]: rustls::client::WebPkiServerVerifier
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RemoteIdentity {
|
||||
/// The pinned fingerprint: `ed25519:<hex>` for raw-key remotes,
|
||||
/// `SHA256:<hex>` for X.509 cert remotes (alknet ADR-030 §6).
|
||||
pub fingerprint: String,
|
||||
}
|
||||
|
||||
/// Credentials for an outbound connection (alknet ADR-091's semantics,
|
||||
/// ADR-005 moved the type here). All dimensions come from the assembly
|
||||
/// layer's configuration — never from environment variables.
|
||||
///
|
||||
/// The two `Option`s are the two credential dimensions the client config
|
||||
/// consumes (see `docs/architecture/client.md`): the client-auth
|
||||
/// presentation (`local_identity`) and the verifier selection
|
||||
/// (`remote_identity`). Both are load-bearing, not cosmetic — `Some` means
|
||||
/// "pin this", `None` means "trust the CA or fail", never a placeholder
|
||||
/// default.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ConnectionCredentials {
|
||||
/// The local node's identity (RFC 7250 raw key or X.509), derived from
|
||||
/// the vault at startup.
|
||||
pub local_identity: Option<TlsIdentity>,
|
||||
/// Expected fingerprint/cert of the remote node. `Some` → fingerprint
|
||||
/// pin (known peer); `None` → CA verification for X.509 remotes,
|
||||
/// fail-closed for Ed25519 raw-key remotes (alknet ADR-034 §2/§3).
|
||||
/// `None` is the public-X.509-endpoint state, not a missing field —
|
||||
/// must not be defaulted to a placeholder.
|
||||
pub remote_identity: Option<RemoteIdentity>,
|
||||
}
|
||||
|
||||
impl ConnectionCredentials {
|
||||
/// Credentials with both dimensions unset — the public-X.509-endpoint
|
||||
/// baseline.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the local identity (the client-auth presentation).
|
||||
pub fn with_local_identity(mut self, local_identity: TlsIdentity) -> Self {
|
||||
self.local_identity = Some(local_identity);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the remote identity (the fingerprint pin).
|
||||
pub fn with_remote_identity(mut self, remote: RemoteIdentity) -> Self {
|
||||
self.remote_identity = Some(remote);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn connection_credentials_builder_methods() {
|
||||
let creds = ConnectionCredentials::new().with_remote_identity(RemoteIdentity {
|
||||
fingerprint: "SHA256:abc".to_string(),
|
||||
});
|
||||
assert_eq!(
|
||||
creds.remote_identity.as_ref().unwrap().fingerprint,
|
||||
"SHA256:abc"
|
||||
);
|
||||
assert!(creds.local_identity.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_credentials_none_is_load_bearing_not_defaulted() {
|
||||
let creds = ConnectionCredentials::new();
|
||||
assert!(
|
||||
creds.remote_identity.is_none(),
|
||||
"ConnectionCredentials::new() must keep remote_identity as None (the load-bearing \
|
||||
public-X.509-endpoint state), not default it to a placeholder"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -37,9 +37,16 @@ pub use signing::Ed25519SigningKey;
|
||||
|
||||
pub use identity::{AcmeDirectory, Ed25519SecretKey, TlsIdentity};
|
||||
|
||||
// The remaining re-export lines land per module as each port task
|
||||
// completes: credentials, fingerprint, server, client, pem, signing
|
||||
// (port-client completes the block).
|
||||
pub use client::{
|
||||
build_client_auth, load_platform_root_cert_store, select_server_verifier,
|
||||
FingerprintPinVerifier, NoClientCertResolver, RawKeyClientCertResolver, TlsClientConfig,
|
||||
};
|
||||
pub use credentials::{ConnectionCredentials, RemoteIdentity};
|
||||
|
||||
pub use server::{
|
||||
build_rustls_server_config, generate_self_signed_cert, AcceptAnyCertVerifier,
|
||||
RawKeyCertResolver, SelfSignedCert, TlsServerConfig,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
|
||||
+612
-1
@@ -1,3 +1,614 @@
|
||||
//! Server-side TLS configuration: [`TlsServerConfig`],
|
||||
//! [`RawKeyCertResolver`], [`AcceptAnyCertVerifier`], [`SelfSignedCert`],
|
||||
//! [`build_rustls_server_config`], [`RawKeyCertResolver`],
|
||||
//! [`AcceptAnyCertVerifier`], [`SelfSignedCert`] /
|
||||
//! [`generate_self_signed_cert`], and the ACME path (feature `acme`).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "acme")]
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
#[cfg(feature = "acme")]
|
||||
use crate::identity::AcmeDirectory;
|
||||
use crate::identity::{Ed25519SecretKey, TlsIdentity};
|
||||
use crate::signing::Ed25519SigningKey;
|
||||
use crate::TlsError;
|
||||
|
||||
/// Server-side TLS configuration, transport-agnostic. Built once from a
|
||||
/// [`TlsIdentity`] + ALPN list, shared across transports via
|
||||
/// `Arc` (not `Clone` — it holds the ACME task's `JoinHandle`).
|
||||
#[allow(dead_code)]
|
||||
pub struct TlsServerConfig {
|
||||
pub(crate) rustls_config: rustls::ServerConfig,
|
||||
#[cfg(feature = "acme")]
|
||||
pub(crate) acme_handle: Option<tokio::task::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl TlsServerConfig {
|
||||
/// Build a server config from a [`TlsIdentity`] and ALPN list.
|
||||
/// ACME identities spawn a background cert-renewal task.
|
||||
pub async fn new(tls_identity: &TlsIdentity, alpns: &[Vec<u8>]) -> Result<Self, TlsError> {
|
||||
match tls_identity {
|
||||
TlsIdentity::Acme {
|
||||
domains,
|
||||
cache_dir,
|
||||
directory,
|
||||
contact,
|
||||
} => {
|
||||
#[cfg(feature = "acme")]
|
||||
{
|
||||
Self::new_acme(domains, cache_dir, directory, contact, alpns).await
|
||||
}
|
||||
#[cfg(not(feature = "acme"))]
|
||||
{
|
||||
let _ = (domains, cache_dir, directory, contact, alpns);
|
||||
Err(TlsError::AcmeConfig(
|
||||
"ACME feature not enabled but TlsIdentity::Acme configured".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let server_config = build_rustls_server_config(tls_identity, alpns)?;
|
||||
Ok(Self {
|
||||
rustls_config: server_config,
|
||||
#[cfg(feature = "acme")]
|
||||
acme_handle: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "acme")]
|
||||
async fn new_acme(
|
||||
domains: &[String],
|
||||
cache_dir: &std::path::Path,
|
||||
directory: &AcmeDirectory,
|
||||
contact: &[String],
|
||||
alpns: &[Vec<u8>],
|
||||
) -> Result<Self, TlsError> {
|
||||
use rustls_acme::caches::DirCache;
|
||||
use rustls_acme::{AcmeConfig, EventError, EventOk};
|
||||
|
||||
let acme_config = AcmeConfig::new(domains.to_vec())
|
||||
.cache(DirCache::new(cache_dir.to_path_buf()))
|
||||
.directory(directory.url())
|
||||
.contact(contact.iter().map(|c| c.as_str()));
|
||||
|
||||
let state = acme_config.state();
|
||||
let resolver = state.resolver();
|
||||
|
||||
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
|
||||
let mut config = rustls::ServerConfig::builder_with_provider(provider)
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_client_cert_verifier(Arc::new(AcceptAnyCertVerifier))
|
||||
.with_cert_resolver(resolver);
|
||||
config.max_early_data_size = u32::MAX;
|
||||
|
||||
let mut alpn = alpns.to_vec();
|
||||
alpn.push(b"acme-tls/1".to_vec());
|
||||
config.alpn_protocols = alpn;
|
||||
|
||||
let domains_owned: Vec<String> = domains.to_vec();
|
||||
let handle = tokio::spawn(async move {
|
||||
use futures::StreamExt;
|
||||
let mut state = state;
|
||||
while let Some(event) = state.next().await {
|
||||
match event {
|
||||
Ok(EventOk::DeployedCachedCert) => {
|
||||
debug!(domains = ?domains_owned, "ACME: deployed cached certificate");
|
||||
}
|
||||
Ok(EventOk::DeployedNewCert) => {
|
||||
debug!(domains = ?domains_owned, "ACME: deployed new certificate");
|
||||
}
|
||||
Ok(EventOk::CertCacheStore) => {
|
||||
debug!(domains = ?domains_owned, "ACME: certificate stored to cache");
|
||||
}
|
||||
Ok(EventOk::AccountCacheStore) => {
|
||||
debug!(domains = ?domains_owned, "ACME: account stored to cache");
|
||||
}
|
||||
Err(EventError::CertCacheLoad(e)) => {
|
||||
error!(domains = ?domains_owned, error = ?e, "ACME: certificate cache load failed");
|
||||
}
|
||||
Err(EventError::AccountCacheLoad(e)) => {
|
||||
error!(domains = ?domains_owned, error = ?e, "ACME: account cache load failed");
|
||||
}
|
||||
Err(EventError::CertCacheStore(e)) => {
|
||||
warn!(domains = ?domains_owned, error = ?e, "ACME: certificate cache store failed");
|
||||
}
|
||||
Err(EventError::AccountCacheStore(e)) => {
|
||||
warn!(domains = ?domains_owned, error = ?e, "ACME: account cache store failed");
|
||||
}
|
||||
Err(EventError::CachedCertParse(e)) => {
|
||||
error!(domains = ?domains_owned, error = ?e, "ACME: cached certificate parse failed");
|
||||
}
|
||||
Err(EventError::Order(e)) => {
|
||||
warn!(domains = ?domains_owned, error = ?e, "ACME: certificate order failed, will retry");
|
||||
}
|
||||
Err(EventError::NewCertParse(e)) => {
|
||||
error!(domains = ?domains_owned, error = ?e, "ACME: new certificate parse failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!(domains = ?domains_owned, "ACME: state machine ended");
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
rustls_config: config,
|
||||
acme_handle: Some(handle),
|
||||
})
|
||||
}
|
||||
|
||||
/// Wrap the inner rustls config for the noq QUIC transport.
|
||||
///
|
||||
/// Takes `&self` (ADR-004): the inner rustls config is `Clone`
|
||||
/// (Arc-shared resolvers), so one [`TlsServerConfig`] can feed a noq
|
||||
/// endpoint and a TCP+TLS acceptor without contortions.
|
||||
#[cfg(feature = "noq")]
|
||||
pub fn for_noq(&self) -> Result<noq::ServerConfig, TlsError> {
|
||||
use noq::crypto::rustls::QuicServerConfig;
|
||||
let quic_server_config = QuicServerConfig::try_from(self.rustls_config.clone())?;
|
||||
Ok(noq::ServerConfig::with_crypto(Arc::new(quic_server_config)))
|
||||
}
|
||||
|
||||
/// Wrap the inner rustls config for the TCP+TLS transport. Infallible
|
||||
/// — `TlsAcceptor::from(Arc<ServerConfig>)` cannot fail.
|
||||
#[cfg(feature = "tcp")]
|
||||
pub fn for_tcp_tls(&self) -> tokio_rustls::TlsAcceptor {
|
||||
tokio_rustls::TlsAcceptor::from(Arc::new(self.rustls_config.clone()))
|
||||
}
|
||||
|
||||
/// Borrow the inner config for transport wrappers the crate does not
|
||||
/// cover.
|
||||
pub fn rustls_config(&self) -> &rustls::ServerConfig {
|
||||
&self.rustls_config
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the inner `rustls::ServerConfig` for the non-ACME identity
|
||||
/// variants: `X509`, `RawKey`, `SelfSigned`. The `Acme` arm is defensive —
|
||||
/// ACME is dispatched by [`TlsServerConfig::new`] to `new_acme` and
|
||||
/// reaching the builder with it is a config error.
|
||||
pub fn build_rustls_server_config(
|
||||
tls_identity: &TlsIdentity,
|
||||
alpns: &[Vec<u8>],
|
||||
) -> Result<rustls::ServerConfig, TlsError> {
|
||||
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
|
||||
let client_verifier = Arc::new(AcceptAnyCertVerifier);
|
||||
match tls_identity {
|
||||
TlsIdentity::X509 { cert, key } => {
|
||||
let cert_chain = crate::pem::load_cert_chain(cert)?;
|
||||
let private_key = crate::pem::load_private_key(key)?;
|
||||
let mut config = rustls::ServerConfig::builder_with_provider(provider)
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_client_cert_verifier(client_verifier)
|
||||
.with_single_cert(cert_chain, private_key)?;
|
||||
config.alpn_protocols = alpns.to_vec();
|
||||
config.max_early_data_size = u32::MAX;
|
||||
Ok(config)
|
||||
}
|
||||
TlsIdentity::RawKey(secret_key) => {
|
||||
let resolver = Arc::new(RawKeyCertResolver::new(secret_key));
|
||||
let mut config = rustls::ServerConfig::builder_with_provider(provider)
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_client_cert_verifier(client_verifier)
|
||||
.with_cert_resolver(resolver);
|
||||
config.alpn_protocols = alpns.to_vec();
|
||||
config.max_early_data_size = u32::MAX;
|
||||
Ok(config)
|
||||
}
|
||||
TlsIdentity::SelfSigned => {
|
||||
let cert = generate_self_signed_cert()?;
|
||||
let mut config = rustls::ServerConfig::builder_with_provider(provider)
|
||||
.with_safe_default_protocol_versions()?
|
||||
.with_client_cert_verifier(client_verifier)
|
||||
.with_single_cert(cert.cert_chain, cert.private_key)?;
|
||||
config.alpn_protocols = alpns.to_vec();
|
||||
config.max_early_data_size = u32::MAX;
|
||||
Ok(config)
|
||||
}
|
||||
TlsIdentity::Acme { .. } => Err(TlsError::AcmeConfig(
|
||||
"TlsIdentity::Acme is handled by TlsServerConfig::new_acme, not \
|
||||
build_rustls_server_config"
|
||||
.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The cert material behind the [`TlsIdentity::SelfSigned`] server path:
|
||||
/// a single rcgen-generated DER cert plus its PKCS#8 private key.
|
||||
pub struct SelfSignedCert {
|
||||
/// The self-signed leaf certificate (DER).
|
||||
pub cert_chain: Vec<rustls::pki_types::CertificateDer<'static>>,
|
||||
/// The matching private key (PKCS#8 DER).
|
||||
pub private_key: rustls::pki_types::PrivateKeyDer<'static>,
|
||||
}
|
||||
|
||||
/// Generate a self-signed dev certificate (rcgen), in-memory.
|
||||
pub fn generate_self_signed_cert() -> Result<SelfSignedCert, TlsError> {
|
||||
use rcgen::{CertificateParams, KeyPair};
|
||||
let key_pair = KeyPair::generate()?;
|
||||
let params = CertificateParams::default();
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
let cert_der = cert.der().clone();
|
||||
let key_der = rustls::pki_types::PrivateKeyDer::Pkcs8(
|
||||
rustls::pki_types::PrivatePkcs8KeyDer::from(key_pair.serialize_der()),
|
||||
);
|
||||
Ok(SelfSignedCert {
|
||||
cert_chain: vec![cert_der],
|
||||
private_key: key_der,
|
||||
})
|
||||
}
|
||||
|
||||
/// Server-side "request-but-don't-require" client cert verifier
|
||||
/// (alknet ADR-034).
|
||||
///
|
||||
/// Asks for a client TLS cert (X.509 or RFC 7250 raw key) so the endpoint
|
||||
/// can extract the fingerprint via `peer_identity()`, but does not require
|
||||
/// one and does not verify the presented cert against a CA. The TLS crate
|
||||
/// hands over the fingerprint string; matching it against peer records
|
||||
/// (`IdentityProvider::resolve_from_fingerprint`) is the auth layer's
|
||||
/// concern, outside this crate (alktls ADR-005).
|
||||
///
|
||||
/// **Server-side only.** This must not be reused as a client-side
|
||||
/// `ServerCertVerifier` — client-side verification is alknet ADR-034's
|
||||
/// selection matrix (see the client module): CA verification for unknown
|
||||
/// X.509 remotes, fingerprint pinning for known peers, fail closed for
|
||||
/// unknown raw keys.
|
||||
pub struct AcceptAnyCertVerifier;
|
||||
|
||||
impl std::fmt::Debug for AcceptAnyCertVerifier {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AcceptAnyCertVerifier").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::server::danger::ClientCertVerifier for AcceptAnyCertVerifier {
|
||||
fn offer_client_auth(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn client_auth_mandatory(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn verify_client_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
||||
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
||||
_now: rustls::pki_types::UnixTime,
|
||||
) -> Result<rustls::server::danger::ClientCertVerified, rustls::Error> {
|
||||
Ok(rustls::server::danger::ClientCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Presents an [`Ed25519SecretKey`] as an RFC 7250 raw public key server
|
||||
/// certificate: the SPKI DER (Ed25519 OID + 32-byte key) is the "cert",
|
||||
/// `only_raw_public_keys() == true`, and the signing key is the shared
|
||||
/// [`Ed25519SigningKey`] helper.
|
||||
pub struct RawKeyCertResolver {
|
||||
key: Arc<rustls::sign::CertifiedKey>,
|
||||
}
|
||||
|
||||
impl RawKeyCertResolver {
|
||||
pub fn new(secret_key: &Ed25519SecretKey) -> Self {
|
||||
let signing_key = Arc::new(Ed25519SigningKey::new(secret_key.clone()));
|
||||
let public_key = signing_key.spki_public_key();
|
||||
let cert = rustls::pki_types::CertificateDer::from(public_key.to_vec());
|
||||
let certified_key = rustls::sign::CertifiedKey::new(vec![cert], signing_key);
|
||||
Self {
|
||||
key: Arc::new(certified_key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::server::ResolvesServerCert for RawKeyCertResolver {
|
||||
fn resolve(
|
||||
&self,
|
||||
_client_hello: rustls::server::ClientHello<'_>,
|
||||
) -> Option<Arc<rustls::sign::CertifiedKey>> {
|
||||
Some(Arc::clone(&self.key))
|
||||
}
|
||||
|
||||
fn only_raw_public_keys(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RawKeyCertResolver {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RawKeyCertResolver").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn raw_key_cert_resolver_only_raw_public_keys() {
|
||||
use rustls::server::ResolvesServerCert;
|
||||
let sk = Ed25519SecretKey::generate();
|
||||
let resolver = RawKeyCertResolver::new(&sk);
|
||||
assert!(resolver.only_raw_public_keys());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_signed_cert_generation_produces_cert_and_key() {
|
||||
let cert = generate_self_signed_cert().expect("self-signed cert generates");
|
||||
assert!(!cert.cert_chain.is_empty());
|
||||
assert!(!cert.private_key.secret_der().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tls_setup_x509_returns_no_acme_state() {
|
||||
use rcgen::{CertificateParams, KeyPair};
|
||||
let key_pair = KeyPair::generate().unwrap();
|
||||
let params = CertificateParams::default();
|
||||
let cert = params.self_signed(&key_pair).unwrap();
|
||||
let cert_pem = cert.pem();
|
||||
let key_pem = key_pair.serialize_pem();
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let cert_path = dir.path().join("cert.pem");
|
||||
let key_path = dir.path().join("key.pem");
|
||||
std::fs::write(&cert_path, cert_pem).unwrap();
|
||||
std::fs::write(&key_path, key_pem).unwrap();
|
||||
|
||||
let tls_identity = TlsIdentity::X509 {
|
||||
cert: cert_path,
|
||||
key: key_path,
|
||||
};
|
||||
let setup = TlsServerConfig::new(&tls_identity, &[b"alktls/test".to_vec()])
|
||||
.await
|
||||
.expect("X509 tls setup should succeed");
|
||||
let _ = setup.rustls_config;
|
||||
#[cfg(feature = "acme")]
|
||||
assert!(setup.acme_handle.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_rustls_server_config_raw_key_succeeds() {
|
||||
let sk = Ed25519SecretKey::generate();
|
||||
let identity = TlsIdentity::RawKey(sk);
|
||||
let alpns = vec![b"alktls/test".to_vec(), b"alktls/call".to_vec()];
|
||||
let config = build_rustls_server_config(&identity, &alpns).expect("raw key config builds");
|
||||
assert_eq!(config.alpn_protocols, alpns);
|
||||
assert_eq!(config.max_early_data_size, u32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_rustls_server_config_self_signed_succeeds() {
|
||||
let identity = TlsIdentity::SelfSigned;
|
||||
let alpns = vec![b"alktls/test".to_vec()];
|
||||
let config =
|
||||
build_rustls_server_config(&identity, &alpns).expect("self-signed config builds");
|
||||
assert_eq!(config.alpn_protocols, alpns);
|
||||
assert_eq!(config.max_early_data_size, u32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_rustls_server_config_acme_returns_config_error() {
|
||||
let identity = 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:dev@example.com".to_string()],
|
||||
};
|
||||
let err = build_rustls_server_config(&identity, &[])
|
||||
.expect_err("Acme identity must not reach the plain builder");
|
||||
assert!(
|
||||
matches!(err, TlsError::AcmeConfig(_)),
|
||||
"the defensive Acme arm must surface as TlsError::AcmeConfig, got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "noq")]
|
||||
#[test]
|
||||
fn for_noq_round_trips_raw_key_config() {
|
||||
let sk = Ed25519SecretKey::generate();
|
||||
let rustls_config =
|
||||
build_rustls_server_config(&TlsIdentity::RawKey(sk), &[b"alktls/test".to_vec()])
|
||||
.expect("rustls config builds");
|
||||
let config = TlsServerConfig {
|
||||
rustls_config,
|
||||
#[cfg(feature = "acme")]
|
||||
acme_handle: None,
|
||||
};
|
||||
let noq_config = config.for_noq().expect("noq config converts");
|
||||
let _ = noq_config;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_any_cert_verifier_offers_and_does_not_require_client_auth() {
|
||||
use rustls::server::danger::ClientCertVerifier;
|
||||
let verifier = AcceptAnyCertVerifier;
|
||||
assert!(verifier.offer_client_auth());
|
||||
assert!(!verifier.client_auth_mandatory());
|
||||
assert!(verifier.root_hint_subjects().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_any_cert_verifier_verifies_any_client_cert() {
|
||||
use rustls::pki_types::{CertificateDer, UnixTime};
|
||||
use rustls::server::danger::ClientCertVerifier;
|
||||
let verifier = AcceptAnyCertVerifier;
|
||||
let cert = CertificateDer::from(b"fake-cert-der".to_vec());
|
||||
let result = verifier.verify_client_cert(&cert, &[], UnixTime::now());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"AcceptAnyCertVerifier must accept any client cert"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_any_cert_verifier_supported_schemes_are_the_nine_pinned() {
|
||||
use rustls::server::danger::ClientCertVerifier;
|
||||
let verifier = AcceptAnyCertVerifier;
|
||||
let schemes = verifier.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,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_any_cert_verifier_debug_is_implemented() {
|
||||
let verifier = AcceptAnyCertVerifier;
|
||||
let s = format!("{verifier:?}");
|
||||
assert!(s.contains("AcceptAnyCertVerifier"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_key_cert_resolver_debug_is_implemented() {
|
||||
let sk = Ed25519SecretKey::generate();
|
||||
let resolver = RawKeyCertResolver::new(&sk);
|
||||
let s = format!("{resolver:?}");
|
||||
assert!(s.contains("RawKeyCertResolver"));
|
||||
}
|
||||
|
||||
#[cfg(feature = "tcp")]
|
||||
#[tokio::test]
|
||||
async fn new_x509_for_tcp_tls_and_rustls_config_round_trip() {
|
||||
use rcgen::{CertificateParams, KeyPair};
|
||||
let key_pair = KeyPair::generate().unwrap();
|
||||
let cert = CertificateParams::default().self_signed(&key_pair).unwrap();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let cert_path = dir.path().join("cert.pem");
|
||||
let key_path = dir.path().join("key.pem");
|
||||
std::fs::write(&cert_path, cert.pem()).unwrap();
|
||||
std::fs::write(&key_path, key_pair.serialize_pem()).unwrap();
|
||||
let identity = TlsIdentity::X509 {
|
||||
cert: cert_path,
|
||||
key: key_path,
|
||||
};
|
||||
let alpn = vec![b"alktls/test".to_vec()];
|
||||
let setup = TlsServerConfig::new(&identity, &alpn)
|
||||
.await
|
||||
.expect("X509 config builds");
|
||||
let rc = setup.rustls_config();
|
||||
assert_eq!(rc.alpn_protocols, alpn);
|
||||
assert_eq!(rc.max_early_data_size, u32::MAX);
|
||||
let _acceptor = setup.for_tcp_tls();
|
||||
}
|
||||
|
||||
#[cfg(feature = "tcp")]
|
||||
#[tokio::test]
|
||||
async fn new_raw_key_for_tcp_tls_and_rustls_config_round_trip() {
|
||||
let identity = TlsIdentity::RawKey(Ed25519SecretKey::generate());
|
||||
let alpn = vec![b"alktls/test".to_vec()];
|
||||
let setup = TlsServerConfig::new(&identity, &alpn)
|
||||
.await
|
||||
.expect("raw key config builds");
|
||||
let rc = setup.rustls_config();
|
||||
assert_eq!(rc.alpn_protocols, alpn);
|
||||
assert_eq!(rc.max_early_data_size, u32::MAX);
|
||||
let _acceptor = setup.for_tcp_tls();
|
||||
}
|
||||
|
||||
#[cfg(feature = "tcp")]
|
||||
#[tokio::test]
|
||||
async fn new_self_signed_for_tcp_tls_and_rustls_config_round_trip() {
|
||||
let identity = TlsIdentity::SelfSigned;
|
||||
let alpn = vec![b"alktls/test".to_vec()];
|
||||
let setup = TlsServerConfig::new(&identity, &alpn)
|
||||
.await
|
||||
.expect("self-signed config builds");
|
||||
let rc = setup.rustls_config();
|
||||
assert_eq!(rc.alpn_protocols, alpn);
|
||||
assert_eq!(rc.max_early_data_size, u32::MAX);
|
||||
let _acceptor = setup.for_tcp_tls();
|
||||
}
|
||||
|
||||
#[cfg(feature = "acme")]
|
||||
#[tokio::test]
|
||||
async fn new_acme_spawns_and_appends_acme_tls_alpn() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let identity = TlsIdentity::Acme {
|
||||
domains: vec!["localhost".to_string()],
|
||||
cache_dir: dir.path().join("cache"),
|
||||
directory: crate::identity::AcmeDirectory::Custom(
|
||||
"http://127.0.0.1:9/directory".to_string(),
|
||||
),
|
||||
contact: vec!["mailto:dev@example.com".to_string()],
|
||||
};
|
||||
let alpn = vec![b"alktls/test".to_vec()];
|
||||
let setup = TlsServerConfig::new(&identity, &alpn)
|
||||
.await
|
||||
.expect("ACME config builds without awaiting the order");
|
||||
let rc = setup.rustls_config();
|
||||
assert_eq!(rc.max_early_data_size, u32::MAX);
|
||||
assert_eq!(
|
||||
rc.alpn_protocols,
|
||||
vec![b"alktls/test".to_vec(), b"acme-tls/1".to_vec()]
|
||||
);
|
||||
assert!(
|
||||
setup.acme_handle.is_some(),
|
||||
"the event-loop task must be spawned and its handle stored"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "acme"))]
|
||||
#[tokio::test]
|
||||
async fn new_acme_identity_without_feature_returns_config_error() {
|
||||
let identity = 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:dev@example.com".to_string()],
|
||||
};
|
||||
let err = match TlsServerConfig::new(&identity, &[]).await {
|
||||
Ok(_) => panic!("Acme identity must fail without the acme feature"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert!(
|
||||
matches!(err, TlsError::AcmeConfig(_)),
|
||||
"expected TlsError::AcmeConfig, got {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+76
-10
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: port-client
|
||||
name: Port client side — TlsClientConfig, verifier selection, client auth (src/client.rs)
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [port-identity-types, port-fingerprint, port-pem-signing]
|
||||
scope: broad
|
||||
risk: medium
|
||||
@@ -73,24 +73,24 @@ co-location prevents semantic drift (ADR-005).
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] Selection-matrix test: all four client-auth presentations × both
|
||||
- [x] Selection-matrix test: all four client-auth presentations × both
|
||||
verifier branches construct and select the expected resolver
|
||||
types (inspect via the config's client-auth/verifier state where
|
||||
the API permits; otherwise assert construction success/error
|
||||
kind per cell)
|
||||
- [ ] `Acme` local identity → `TlsError::AcmeConfig`
|
||||
- [ ] `enable_early_data == true` pinned
|
||||
- [ ] Root store non-empty (fallback exercised)
|
||||
- [ ] FingerprintPinVerifier unit tests ported (pin match, mismatch,
|
||||
- [x] `Acme` local identity → `TlsError::AcmeConfig`
|
||||
- [x] `enable_early_data == true` pinned
|
||||
- [x] Root store non-empty (fallback exercised)
|
||||
- [x] FingerprintPinVerifier unit tests ported (pin match, mismatch,
|
||||
raw-key signature routing)
|
||||
- [ ] `cargo test` (default), `cargo test --all-features`,
|
||||
- [x] `cargo test` (default), `cargo test --all-features`,
|
||||
`cargo clippy --all-targets -- -D warnings`, `cargo fmt --check`
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `for_noq(self)` / `into_rustls_config(self)` per ADR-004
|
||||
- [ ] The selection matrix has no fourth path (fail-closed structural)
|
||||
- [ ] `lib.rs` re-exports the client surface + `ConnectionCredentials`
|
||||
- [x] `for_noq(self)` / `into_rustls_config(self)` per ADR-004
|
||||
- [x] The selection matrix has no fourth path (fail-closed structural)
|
||||
- [x] `lib.rs` re-exports the client surface + `ConnectionCredentials`
|
||||
/ `RemoteIdentity`
|
||||
|
||||
## References
|
||||
@@ -105,6 +105,72 @@ co-location prevents semantic drift (ADR-005).
|
||||
|
||||
> Agent fills this during implementation.
|
||||
|
||||
- Verifier selection / client-auth presentation are inspected through
|
||||
the `rustls::ClientConfig` Debug output (`ClientConfig` derives
|
||||
`Debug` and embeds the verifier's debug name —
|
||||
`FingerprintPinVerifier` / `WebPkiServerVerifier`) and the public
|
||||
`client_auth_cert_resolver` field; the `verifier` field itself is
|
||||
`pub(super)` at rustls 0.23.44, so the debug-string probe is the
|
||||
structural assertion shape.
|
||||
- `select_server_verifier`'s `WebPkiServerVerifier` build error maps to
|
||||
`TlsError::VerifierBuild` via `#[from] rustls::client::VerifierBuilderError`
|
||||
(the `rustls::webpki` module is private at 0.23.44; same type,
|
||||
public path — lib.rs's variant doc already records this).
|
||||
- The signature-routing unit tests construct `DigitallySignedStruct`
|
||||
from its wire encoding through the doc-hidden
|
||||
`rustls::internal::msgs` surface (`Codec::read`; `new` is
|
||||
`pub(crate)`), then drive `verify_tls12_signature` /
|
||||
`verify_tls13_signature` on the SPKI-as-`CertificateDer` exactly as
|
||||
rustls presents it in an RFC 7250 handshake — pin match + signature
|
||||
possession-proof both asserted.
|
||||
- The `for_noq()` wrap relies on `TlsError::NoqWrap(#[from])`
|
||||
(`NoInitialCipherSuite` → `?`), ADR-002/ADR-003.
|
||||
- The extracted `TlsClientConfig`'s `#[allow(dead_code)]` is kept
|
||||
(the server-side `rustls_config()` accessor shape is ADR-004's;
|
||||
no consumer touches the field within this crate yet).
|
||||
|
||||
## Summary
|
||||
|
||||
> Agent fills this on completion.
|
||||
|
||||
Ported `src/credentials.rs` (wholesale from alknet-core, doc comments
|
||||
rewired to this crate's ADRs — ADR-005, alknet ADR-091/034/030
|
||||
semantics preserved verbatim in substance) and `src/client.rs` (port of
|
||||
alknet-tls `client.rs` with the task's error-mapping deltas and import
|
||||
rewires). Visibility: `RawKeyClientCertResolver`, `NoClientCertResolver`,
|
||||
`FingerprintPinVerifier` made `pub` (lib.rs re-exports them);
|
||||
`build_client_auth`, `select_server_verifier`, `load_platform_root_cert_store`
|
||||
`pub`. `lib.rs` gained the client + credentials re-export lines; the
|
||||
pending-modules comment now notes only the server line remains (its port
|
||||
task owns it).
|
||||
|
||||
Deltas vs the extraction:
|
||||
|
||||
- `TlsError` mapping per ADR-002: `with_safe_default_protocol_versions` /
|
||||
`CertifiedKey::from_der` / `RootCertStore::add` errors →
|
||||
`TlsError::Rustls` (`#[from] rustls::Error`); the
|
||||
`WebPkiServerVerifier` build error → `TlsError::VerifierBuild`
|
||||
(`#[from] rustls::client::VerifierBuilderError`); the Acme
|
||||
client-auth error string → `TlsError::AcmeConfig`. No
|
||||
`Config(String)` catch-all.
|
||||
- `for_quinn` → `for_noq` (ADR-003/004): consuming, noq-gated; the
|
||||
wrap error flows through `TlsError::NoqWrap(#[from])` — no
|
||||
`map_err` stringification.
|
||||
- Imports rewired to `crate::{credentials, fingerprint, identity}`;
|
||||
the `PeerEntry` reference in the extraction's `NoClientCertResolver`
|
||||
doc became the peer-id resolution language (auth layer stays out,
|
||||
ADR-005).
|
||||
- Invariants pinned by tests: `enable_early_data = true` + exact ALPN;
|
||||
aws-lc-rs default provider (9-suite set + `crypto_provider()`
|
||||
identity); root store non-empty; verifier-selection matrix
|
||||
(`Some` → `FingerprintPinVerifier`, `None` → `WebPkiServerVerifier`);
|
||||
client-auth presentation matrix (RawKey → RFC 7250 raw pub keys,
|
||||
X509 → loaded chain, SelfSigned/None → nothing, Acme →
|
||||
`TlsError::AcmeConfig`); FingerprintPinVerifier pin match/mismatch
|
||||
(Ed25519 SPKI + SHA256 X.509) and raw-key signature routing
|
||||
(TLS 1.2 + 1.3, forged-signature rejection).
|
||||
|
||||
Verification: `cargo test` (56 pass), `cargo test --all-features`
|
||||
(59 pass, incl. both `for_noq` tests), `cargo clippy --all-targets
|
||||
--all-features -- -D warnings` (clean), `cargo fmt --check` (clean),
|
||||
`cargo check --features noq` / `--features tcp` / default (clean).
|
||||
+68
-12
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: port-server
|
||||
name: Port server side — TlsServerConfig, resolvers, ACME path (src/server.rs)
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [port-identity-types, port-pem-signing]
|
||||
scope: broad
|
||||
risk: medium
|
||||
@@ -70,25 +70,25 @@ Without the feature, `Acme` identities return `TlsError::AcmeConfig`.
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] Invariant pins green: `max_early_data_size` per path,
|
||||
- [x] Invariant pins green: `max_early_data_size` per path,
|
||||
nine-scheme exact list, resolver behavior, verifier behavior
|
||||
- [ ] ACME branch (with `--features acme`): spawns, returns
|
||||
- [x] ACME branch (with `--features acme`): spawns, returns
|
||||
immediately, appends `acme-tls/1` (test with a staging URL +
|
||||
tempdir cache; do NOT hit Let's Encrypt — construct and assert
|
||||
config state, assert the ALPN list)
|
||||
- [ ] `Acme` identity without the `acme` feature → `TlsError::AcmeConfig`
|
||||
- [ ] `for_noq` / `for_tcp_tls` round-trip per identity variant
|
||||
- [x] `Acme` identity without the `acme` feature → `TlsError::AcmeConfig`
|
||||
- [x] `for_noq` / `for_tcp_tls` round-trip per identity variant
|
||||
(construction-level; handshakes are out of scope)
|
||||
- [ ] `cargo test` (default), `cargo test --all-features`,
|
||||
- [x] `cargo test` (default), `cargo test --all-features`,
|
||||
`cargo clippy --all-targets -- -D warnings`, `cargo fmt --check`
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All five server invariants are test-asserted (not just compiled)
|
||||
- [ ] `for_noq`, `for_tcp_tls`, `rustls_config` exist with ADR-004
|
||||
- [x] All five server invariants are test-asserted (not just compiled)
|
||||
- [x] `for_noq`, `for_tcp_tls`, `rustls_config` exist with ADR-004
|
||||
signatures (`&self`; `for_tcp_tls` infallible)
|
||||
- [ ] No `unreachable!`/panics in library code
|
||||
- [ ] `lib.rs` re-exports the server surface
|
||||
- [x] No `unreachable!`/panics in library code
|
||||
- [x] `lib.rs` re-exports the server surface
|
||||
|
||||
## References
|
||||
|
||||
@@ -99,8 +99,64 @@ Without the feature, `Acme` identities return `TlsError::AcmeConfig`.
|
||||
|
||||
## Notes
|
||||
|
||||
> Agent fills this during implementation.
|
||||
- **The extracted `unreachable!` is now the defensive arm**: the Acme
|
||||
arm in `build_rustls_server_config` returns
|
||||
`TlsError::AcmeConfig("TlsIdentity::Acme is handled by
|
||||
TlsServerConfig::new_acme, not build_rustls_server_config")` —
|
||||
test-asserted (`build_rustls_server_config_acme_returns_config_error`).
|
||||
- **Error mapping**: every extraction `TlsError::Config(e.to_string())`
|
||||
site at a `rustls::Error` producer (`with_safe_default_protocol_versions`,
|
||||
`with_single_cert`) became `?` into `TlsError::Rustls` (`#[from]`);
|
||||
rcgen sites became `?` into `TlsError::SelfSigned` (`#[from]
|
||||
rcgen::Error`). The non-acme `TlsServerConfig::new` Acme arm keeps its
|
||||
message verbatim but under `AcmeConfig`.
|
||||
- **The ACME test uses a `Custom("http://127.0.0.1:9/directory")`
|
||||
directory rather than the staging URL** — never contacted either way
|
||||
(port 9 discard; no network I/O beyond rustls-acme's async task start);
|
||||
the task's "staging URL" intent (do-not-hit-Let's-Encrypt) is satisfied
|
||||
with a stronger guarantee: a blackhole address. Staging would risk a
|
||||
real contact if a test misfired; `127.0.0.1:9` cannot succeed.
|
||||
- **`for_noq(&self)` clones the inner config** (`try_from` consumes) —
|
||||
the ADR-004 borrow shape requires it; the clone is cheap (Arc-shared
|
||||
resolvers).
|
||||
- **Cargo.toml deltas (two, both required)**:
|
||||
1. `noq` gains `"aws-lc-rs"` in its feature list. ADR-003's TOML
|
||||
block (`features = ["rustls"]`) was written against noq 1.2's
|
||||
API, but the lockfile resolves `noq-proto 1.3.0`, where
|
||||
`ServerConfig::with_crypto(crypto)` (the single-arg constructor
|
||||
`for_noq` calls) is `#[cfg(any(feature = "aws-lc-rs",
|
||||
feature = "ring"))]` — the retry-token key comes from noq's
|
||||
`ring_like` module, which needs one of the two provider features.
|
||||
`"aws-lc-rs"` (not `"ring"`) matches the crate's provider posture;
|
||||
it does not fight ADR-084: the config's internal provider is still
|
||||
the crate's explicit `aws_lc_rs::default_provider()` (noq consumes
|
||||
it from the config — ADR-003's provider paragraph). One new
|
||||
lockfile line (`aws-lc-rs` under noq-proto). ADR-003 needs a
|
||||
one-line amendment (recorded for review-impl docs sync).
|
||||
2. `acme = ["dep:rustls-acme", "dep:futures"]` — ADR-006 §Feature
|
||||
gates already records this ("the `acme` feature gates ... the
|
||||
spawned task + the `futures` dep"); the scaffold's TOML omitted
|
||||
the `futures` half. Without it, `--features acme` alone fails to
|
||||
compile (the spawned task uses `futures::StreamExt`).
|
||||
|
||||
## Summary
|
||||
|
||||
> Agent fills this on completion.
|
||||
Ported `src/server.rs` (614 lines) from alknet-tls server.rs with all
|
||||
five ADR-pinned deltas applied: `for_noq(&self)` (borrow + clone +
|
||||
`NoqWrap` via `#[from]`), `for_tcp_tls(&self)` adopted (infallible
|
||||
TlsAcceptor), `rustls_config(&self)` adopted, `acme_handle` renamed,
|
||||
Acme arm → `TlsError::AcmeConfig` (no `unreachable!`). All error sites
|
||||
remapped to the typed variants (`Rustls`/`SelfSigned`/`AcmeConfig`).
|
||||
The ACME path is verbatim (DirCache + directory URL + contacts +
|
||||
resolver + `acme-tls/1` + spawned event loop with the extracted
|
||||
tracing lines, returns immediately, handle never aborted).
|
||||
|
||||
14 tests: 12 ported (with the Acme-unreachable → AcmeConfig and
|
||||
for_quinn → for_noq rewires; the acme_directory URL tests live in
|
||||
identity.rs now) + the nine-scheme exact-list pin + ACME
|
||||
spawn/return/ALPN assertion + no-feature AcmeConfig test +
|
||||
`for_tcp_tls`/`rustls_config` round-trips for X509 (rcgen PEM pair in
|
||||
tempdir)/RawKey/SelfSigned. Verification: `cargo test` 68 pass,
|
||||
`--all-features` 75 pass, `--features noq` 72 pass, clippy -D
|
||||
warnings (default + all-features), `fmt --check`, feature checks
|
||||
(tcp/acme) — all green.
|
||||
Reference in New Issue
Block a user