//! `CallClient`: the outbound connection opener (ADR-017 §1). //! //! Opens a QUIC connection to a remote node on ALPN `alknet/call`, performs //! credential setup, and produces a [`CallConnection`] running the shared //! dispatch loop (delegated to [`crate::protocol::dispatch::Dispatcher`]). //! `CallClient` is the connection-establishment half; `CallAdapter`'s accept //! path is the inbound half; both produce a `CallConnection` and hand it to //! the same `Dispatcher::run_loop` (ADR-017 §1). //! //! After establishment the connection is symmetric (ADR-017 §2): both sides //! can send and receive `call.requested`. The `CallClient` is both a caller //! (initiates outgoing calls via `CallConnection::call()`/`subscribe()`/ //! `abort()`) and a callee (dispatches incoming calls against its registry). //! //! See `docs/architecture/crates/call/client-and-adapters.md` for the spec. use std::net::SocketAddr; use std::sync::Arc; use alknet_core::auth::IdentityProvider; use alknet_core::config::TlsIdentity; use alknet_core::types::Connection; use crate::protocol::connection::CallConnection; use crate::protocol::dispatch::Dispatcher; use crate::registry::registration::OperationRegistry; /// Expected identity of the remote node (ADR-017 §7, extended by ADR-034 §2). /// Carries a fingerprint string the assembly layer derives from `Capabilities` /// when the local node has a `PeerEntry` for the remote (the known-peer case → /// fingerprint pin). /// /// `remote_identity: None` is the **public X.509 endpoint** case: the local /// node has no `PeerEntry` for the remote, so there is no fingerprint to pin. /// Combined with an X.509 transport, `None` selects CA verification /// (`WebPkiServerVerifier`) per the verifier-selection rule in ADR-034 §3. /// Combined with an Ed25519 raw-key transport, `None` fails closed (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. #[derive(Debug, Clone)] pub struct RemoteIdentity { pub fingerprint: String, } /// Credentials for an outbound `alknet/call` connection (ADR-017 §7). All /// three dimensions come from `Capabilities` (ADR-014), never from environment /// variables — see the No-Env-Vars Invariant in /// `docs/architecture/crates/call/client-and-adapters.md`. #[derive(Debug, Clone, Default)] pub struct CallCredentials { /// The local node's TLS identity (RFC 7250 raw key or X.509), derived /// from the vault at startup. pub tls_identity: Option, /// Opaque call-protocol-level auth token, decrypted from the vault. pub auth_token: Option, /// Expected fingerprint/cert of the remote node, stored as a capability. /// `Some` → fingerprint pin (known peer with a `PeerEntry`); `None` → CA /// verification for X.509 remotes, fail-closed for Ed25519 raw-key remotes /// (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, } impl CallCredentials { pub fn new() -> Self { Self::default() } pub fn with_tls_identity(mut self, tls_identity: TlsIdentity) -> Self { self.tls_identity = Some(tls_identity); self } pub fn with_auth_token(mut self, token: alknet_core::auth::AuthToken) -> Self { self.auth_token = Some(token); self } pub fn with_remote_identity(mut self, remote: RemoteIdentity) -> Self { self.remote_identity = Some(remote); self } } /// Errors produced by [`CallClient::connect`]. #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum ClientError { #[error("transport error: {message}")] Transport { message: String }, #[error("tls setup error: {message}")] TlsSetup { message: String }, #[error("connection closed")] ConnectionClosed, } /// Outbound `alknet/call` connection opener (the #1 gap, ADR-017 §1). /// /// Peer authorization flows through the existing `AccessControl::check` gate /// in `OperationRegistry::invoke` (ADR-029 §3) — no parallel `remote_safe`/ /// `trusted_peer` gate. pub struct CallClient { registry: Arc, identity_provider: Arc, } impl CallClient { pub fn new( registry: Arc, identity_provider: Arc, ) -> Self { Self { registry, identity_provider, } } pub fn registry(&self) -> &Arc { &self.registry } pub fn identity_provider(&self) -> &Arc { &self.identity_provider } /// Open a QUIC connection to `addr` on ALPN `alknet/call`, perform /// credential handshake, and return a `CallConnection` running the shared /// dispatch loop. Credentials come from `Capabilities` (ADR-014), not env /// vars — the no-env-vars invariant. /// /// The dispatch loop runs on a spawned task; the returned `CallConnection` /// is live until the remote closes the connection or the caller drops it. /// The caller can immediately use `call()`/`subscribe()`/`abort()` on the /// returned connection, and the remote peer can call back into this /// `CallClient`'s registry (connection symmetry, ADR-017 §2). #[cfg(feature = "quinn")] pub async fn connect( &self, addr: SocketAddr, credentials: CallCredentials, ) -> Result { let alpn = b"alknet/call".to_vec(); let client_config = build_quinn_client_config(&credentials, &alpn) .map_err(|e| ClientError::TlsSetup { message: e })?; let bind_addr: SocketAddr = "0.0.0.0:0".parse().expect("valid bind addr"); let endpoint = quinn::Endpoint::client(bind_addr).map_err(|e| ClientError::Transport { message: e.to_string(), })?; let connection = endpoint .connect_with(client_config, addr, "alknet") .map_err(|e| ClientError::Transport { message: e.to_string(), })? .await .map_err(|e| ClientError::Transport { message: e.to_string(), })?; let connection = Connection::from_quinn_with_alpn(connection, alpn); Ok(self.spawn_dispatch(connection)) } /// Run the shared dispatch loop over a pre-established `Connection`. The /// `CallClient` spawns the dispatcher task and returns a live /// `CallConnection` the caller can use immediately. Used by `connect()` /// (after the QUIC dial completes) and by integration tests that wire a /// mock/loopback `Connection` directly. pub fn spawn_dispatch(&self, connection: Connection) -> CallConnection { let call_connection = Arc::new(CallConnection::new(connection)); let dispatcher = Dispatcher::new( Arc::clone(&self.registry), Arc::clone(&self.identity_provider), ); let run_conn = Arc::clone(&call_connection); tokio::spawn(async move { dispatcher.run_loop(run_conn).await; }); (*call_connection).clone() } } #[cfg(feature = "quinn")] fn build_quinn_client_config( credentials: &CallCredentials, alpn: &[u8], ) -> Result { let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider()); let client_auth = build_client_auth(&provider, &credentials.tls_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(|e| e.to_string())? .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(quinn::ClientConfig::new(Arc::new( quinn::crypto::rustls::QuicClientConfig::try_from(config).map_err(|e| e.to_string())?, ))) } /// 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 `tls_identity` configured) /// resolves to no client cert (the server gets nothing to fingerprint). #[cfg(feature = "quinn")] fn build_client_auth( provider: &Arc, tls_identity: &Option, ) -> Result, String> { match tls_identity { Some(TlsIdentity::RawKey(secret_key)) => { let signing_key = Arc::new(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 = load_cert_chain(cert).map_err(|e| e.to_string())?; let key_der = load_private_key(key).map_err(|e| e.to_string())?; let certified_key = rustls::sign::CertifiedKey::from_der(cert_chain, key_der, provider) .map_err(|e| e.to_string())?; Ok(Arc::new(RawKeyClientCertResolver::new(Arc::new( certified_key, )))) } Some(TlsIdentity::SelfSigned) | None => Ok(Arc::new(NoClientCertResolver)), Some(TlsIdentity::Acme { .. }) => { Err("ACME TLS identity is server-only; cannot be used for client auth".to_string()) } } } /// Select the server cert verifier by `remote_identity` presence (ADR-034 §3). /// /// - `Some(fingerprint)` → known peer → `FingerprintPinVerifier` (fingerprint /// match). The fingerprint IS the trust anchor. /// - `None` → no `PeerEntry` for 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 — ADR-034 §2 assumption 1). `None` is the /// public-X.509-endpoint state, not "skip verification." #[cfg(feature = "quinn")] fn select_server_verifier( provider: &Arc, remote_identity: &Option, ) -> Result, String> { 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() .map_err(|e| e.to_string())?; 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 aws-lc-rs built-in `webpki-roots` if the platform store is empty /// (e.g. in a container with no system CA bundle). #[cfg(feature = "quinn")] fn load_platform_root_cert_store() -> Result { 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()) .map_err(|e| format!("failed to add native root cert: {e}"))?; } Ok(roots) } #[cfg(feature = "quinn")] fn load_cert_chain( path: &std::path::Path, ) -> Result>, String> { let bytes = std::fs::read(path).map_err(|e| e.to_string())?; let mut reader = std::io::BufReader::new(bytes.as_slice()); rustls_pemfile::certs(&mut reader) .collect::, _>>() .map_err(|e| e.to_string()) } #[cfg(feature = "quinn")] fn load_private_key( path: &std::path::Path, ) -> Result, String> { let bytes = std::fs::read(path).map_err(|e| e.to_string())?; let mut reader = std::io::BufReader::new(bytes.as_slice()); match rustls_pemfile::private_key(&mut reader) { Ok(Some(key)) => Ok(key), Ok(None) => Err("no private key found in file".to_string()), Err(e) => Err(e.to_string()), } } /// 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. #[cfg(feature = "quinn")] struct RawKeyClientCertResolver { key: Arc, raw_public_keys: bool, } #[cfg(feature = "quinn")] impl RawKeyClientCertResolver { fn new(key: Arc) -> Self { let raw_public_keys = key.cert.len() == 1 && is_ed25519_spki(&key.cert[0]); Self { key, raw_public_keys, } } } #[cfg(feature = "quinn")] fn is_ed25519_spki(cert_der: &rustls::pki_types::CertificateDer<'_>) -> bool { alknet_core::fingerprint::extract_ed25519_raw_key_from_spki(cert_der.as_ref()).is_some() } #[cfg(feature = "quinn")] 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() } } #[cfg(feature = "quinn")] impl rustls::client::ResolvesClientCert for RawKeyClientCertResolver { fn resolve( &self, _root_hint_subjects: &[&[u8]], _sigschemes: &[rustls::SignatureScheme], ) -> Option> { 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 `tls_identity: None` /// or `SelfSigned` path). The server gets nothing to fingerprint — the /// `PeerEntry` fingerprint → `peer_id` resolution path is not activated for /// this connection. #[cfg(feature = "quinn")] struct NoClientCertResolver; #[cfg(feature = "quinn")] impl std::fmt::Debug for NoClientCertResolver { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("NoClientCertResolver").finish() } } #[cfg(feature = "quinn")] impl rustls::client::ResolvesClientCert for NoClientCertResolver { fn resolve( &self, _root_hint_subjects: &[&[u8]], _sigschemes: &[rustls::SignatureScheme], ) -> Option> { None } fn has_certs(&self) -> bool { false } } /// `ServerCertVerifier` that pins a specific fingerprint (ADR-034 §3, the /// known-peer path). For `ed25519:` remotes the raw Ed25519 pub key is /// extracted from the presented cert and matched against the pinned /// fingerprint; for `SHA256:` 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. #[cfg(feature = "quinn")] struct FingerprintPinVerifier { fingerprint: String, supported: rustls::crypto::WebPkiSupportedAlgorithms, } #[cfg(feature = "quinn")] impl FingerprintPinVerifier { fn new(fingerprint: String, supported: rustls::crypto::WebPkiSupportedAlgorithms) -> Self { Self { fingerprint, supported, } } } #[cfg(feature = "quinn")] 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() } } #[cfg(feature = "quinn")] 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 { let presented = alknet_core::fingerprint::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 { if alknet_core::fingerprint::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 { if alknet_core::fingerprint::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 { self.supported.supported_schemes() } } #[cfg(feature = "quinn")] #[derive(Clone)] struct Ed25519SigningKey { key: alknet_core::config::Ed25519SecretKey, } #[cfg(feature = "quinn")] impl Ed25519SigningKey { fn new(key: alknet_core::config::Ed25519SecretKey) -> Self { Self { key } } fn spki_public_key(&self) -> rustls::pki_types::SubjectPublicKeyInfoDer<'static> { rustls::sign::public_key_to_spki( &rustls::pki_types::alg_id::ED25519, self.key.public().as_bytes(), ) } } #[cfg(feature = "quinn")] impl std::fmt::Debug for Ed25519SigningKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Ed25519SigningKey").finish() } } #[cfg(feature = "quinn")] impl rustls::sign::SigningKey for Ed25519SigningKey { fn choose_scheme( &self, offered: &[rustls::SignatureScheme], ) -> Option> { if offered.contains(&rustls::SignatureScheme::ED25519) { Some(Box::new(self.clone())) } else { None } } fn algorithm(&self) -> rustls::SignatureAlgorithm { rustls::SignatureAlgorithm::ED25519 } fn public_key(&self) -> Option> { Some(self.spki_public_key()) } } #[cfg(feature = "quinn")] impl rustls::sign::Signer for Ed25519SigningKey { fn sign(&self, message: &[u8]) -> Result, rustls::Error> { Ok(self.key.sign(message).to_bytes().to_vec()) } fn scheme(&self) -> rustls::SignatureScheme { rustls::SignatureScheme::ED25519 } } #[cfg(test)] mod tests { use super::*; use crate::protocol::connection::CallConnection; use crate::protocol::wire::ResponseEnvelope; use crate::registry::registration::{ make_handler, Handler, HandlerRegistration, OperationProvenance, }; use crate::registry::spec::{AccessControl, OperationSpec, OperationType, Visibility}; use alknet_core::auth::Identity; use alknet_core::types::{Capabilities, MockConnection}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Mutex as StdMutex; struct StubConnection { alpn: &'static [u8], addr: Option, closed: StdMutex>, } impl MockConnection for StubConnection { fn remote_alpn(&self) -> &[u8] { self.alpn } fn remote_addr(&self) -> Option { self.addr } fn close(&self, code: u32, reason: &str) { *self.closed.lock().unwrap() = Some((code, reason.to_string())); } } fn stub_connection() -> Connection { Connection::from_mock(Arc::new(StubConnection { alpn: b"alknet/call", addr: Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4321)), closed: StdMutex::new(None), })) } fn external_spec(name: &str) -> OperationSpec { OperationSpec::new( name, OperationType::Query, Visibility::External, serde_json::json!({}), serde_json::json!({}), vec![], AccessControl::default(), ) } fn caps_inspect_handler() -> Handler { make_handler(|_input, context| async move { let has_google = context.capabilities.get("google").is_some(); ResponseEnvelope::ok( context.request_id, serde_json::json!({ "has_google_capability": has_google }), ) }) } struct NoopIdentityProvider; impl alknet_core::auth::IdentityProvider for NoopIdentityProvider { fn resolve_from_fingerprint(&self, _fp: &str) -> Option { None } fn resolve_from_token(&self, _token: &alknet_core::auth::AuthToken) -> Option { None } } fn registry_with_caps() -> Arc { let mut registry = OperationRegistry::new(); registry.register(HandlerRegistration::new( external_spec("pub/run"), caps_inspect_handler(), OperationProvenance::Local, None, None, Capabilities::new().with_api_key("google", "pub-key".to_string()), )); Arc::new(registry) } fn dispatcher(registry: &Arc) -> Dispatcher { Dispatcher::new(Arc::clone(registry), Arc::new(NoopIdentityProvider)) } async fn dispatch(d: &Dispatcher, conn: &Arc, op: &str) -> ResponseEnvelope { d.dispatch_requested( conn, "req-test".to_string(), serde_json::json!({ "operationId": op, "input": {} }), ) .await } #[test] fn call_credentials_builder_methods() { let creds = CallCredentials::new().with_remote_identity(RemoteIdentity { fingerprint: "SHA256:abc".to_string(), }); assert_eq!( creds.remote_identity.as_ref().unwrap().fingerprint, "SHA256:abc" ); assert!(creds.tls_identity.is_none()); assert!(creds.auth_token.is_none()); } #[tokio::test] async fn external_op_dispatches_and_populates_capabilities() { let registry = registry_with_caps(); let d = dispatcher(®istry); let conn = Arc::new(CallConnection::new(stub_connection())); let response = dispatch(&d, &conn, "pub/run").await; let out = response.result.expect("ok"); assert_eq!( out["has_google_capability"], serde_json::json!(true), "an External op's call must populate capabilities for the handler" ); } #[tokio::test] async fn unknown_op_returns_not_found() { let registry = Arc::new(OperationRegistry::new()); let d = dispatcher(®istry); let conn = Arc::new(CallConnection::new(stub_connection())); let response = dispatch(&d, &conn, "no/such").await; match response.result { Err(e) => assert_eq!(e.code, "NOT_FOUND"), other => panic!("expected NOT_FOUND, got {other:?}"), } } #[tokio::test] async fn spawn_dispatch_returns_live_call_connection() { let registry = registry_with_caps(); let client = CallClient::new(Arc::clone(®istry), Arc::new(NoopIdentityProvider)); let conn = client.spawn_dispatch(stub_connection()); assert_eq!( conn.connection().expect("quic connection present").remote_alpn(), b"alknet/call" ); std::mem::drop(conn); } #[test] fn call_client_is_send_sync() { fn assert_send_sync() {} assert_send_sync::(); assert_send_sync::(); assert_send_sync::(); } #[cfg(feature = "quinn")] fn build_ed25519_spki_der(raw_key: &[u8; 32]) -> Vec { let spki = rustls::sign::public_key_to_spki(&rustls::pki_types::alg_id::ED25519, raw_key); spki.to_vec() } #[cfg(feature = "quinn")] 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() } #[cfg(feature = "quinn")] fn aws_lc_rs_provider() -> Arc { Arc::new(rustls::crypto::aws_lc_rs::default_provider()) } #[cfg(feature = "quinn")] fn verify_pin( verifier: &FingerprintPinVerifier, cert_der: rustls::pki_types::CertificateDer<'_>, ) -> Result { use rustls::client::danger::ServerCertVerifier; let server_name: rustls::pki_types::ServerName<'static> = "alknet".try_into().expect("server name"); verifier.verify_server_cert( &cert_der, &[], &server_name, &[], rustls::pki_types::UnixTime::now(), ) } #[cfg(feature = "quinn")] #[test] fn fingerprint_pin_verifier_matches_correct_ed25519_fingerprint() { let sk = alknet_core::config::Ed25519SecretKey::generate(); let raw_key = sk.public().to_bytes(); let spki_der = build_ed25519_spki_der(&raw_key); let fingerprint = alknet_core::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" ); } #[cfg(feature = "quinn")] #[test] fn fingerprint_pin_verifier_rejects_wrong_ed25519_fingerprint() { let sk = alknet_core::config::Ed25519SecretKey::generate(); let raw_key = sk.public().to_bytes(); let spki_der = build_ed25519_spki_der(&raw_key); let other_sk = alknet_core::config::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" ); } #[cfg(feature = "quinn")] #[test] fn fingerprint_pin_verifier_matches_correct_sha256_fingerprint() { let cert_der = build_x509_cert_der(); let fingerprint = alknet_core::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" ); } #[cfg(feature = "quinn")] #[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" ); } #[cfg(feature = "quinn")] #[test] fn select_server_verifier_returns_ca_verifier_for_none() { let provider = aws_lc_rs_provider(); let remote_identity: Option = 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}" ); } #[cfg(feature = "quinn")] #[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}" ); } #[cfg(feature = "quinn")] #[test] fn build_client_auth_presents_ed25519_raw_key_without_error() { let provider = aws_lc_rs_provider(); let sk = alknet_core::config::Ed25519SecretKey::generate(); let tls_identity = Some(alknet_core::config::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" ); } #[cfg(feature = "quinn")] #[test] fn build_client_auth_none_resolves_to_no_client_cert() { let provider = aws_lc_rs_provider(); let tls_identity: Option = 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)" ); } #[cfg(feature = "quinn")] #[test] fn build_quinn_client_config_with_raw_key_identity_builds_without_error() { let sk = alknet_core::config::Ed25519SecretKey::generate(); let credentials = CallCredentials::new() .with_tls_identity(alknet_core::config::TlsIdentity::RawKey(sk)) .with_remote_identity(RemoteIdentity { fingerprint: "ed25519:deadbeef".to_string(), }); let config = build_quinn_client_config(&credentials, b"alknet/call"); assert!( config.is_ok(), "build_quinn_client_config must build with a RawKey identity + pinned fingerprint" ); } #[cfg(feature = "quinn")] #[test] fn build_quinn_client_config_with_no_remote_identity_builds_without_error() { let sk = alknet_core::config::Ed25519SecretKey::generate(); let credentials = CallCredentials::new().with_tls_identity(alknet_core::config::TlsIdentity::RawKey(sk)); let config = build_quinn_client_config(&credentials, b"alknet/call"); assert!( config.is_ok(), "build_quinn_client_config must build for the None + CA-verification path" ); } #[test] fn remote_identity_none_is_load_bearing_not_defaulted() { let creds = CallCredentials::new(); assert!( creds.remote_identity.is_none(), "CallCredentials::new() must keep remote_identity as None (the load-bearing \ public-X.509-endpoint state), not default it to a placeholder" ); } }