feat(call): wire CallClient TLS client-auth and server cert verifier selection (call/call-client-verifier-selection)

Replace AcceptAnyServerCertVerifier (a security hole for X.509) with
verifier selection by PeerEntry presence (ADR-034 §3, OQ-29):

- build_client_auth presents the Ed25519 key as an RFC 7250 raw public
  key client cert (replaces with_no_client_auth), activating the
  PeerEntry fingerprint -> peer_id resolution path on quinn.
- select_server_verifier: Some(fingerprint) -> FingerprintPinVerifier
  (fingerprint match for known peers); None -> WebPkiServerVerifier
  (CA verification for public X.509 endpoints). None + Ed25519 raw key
  fails closed at handshake (no CA to fall back to).
- FingerprintPinVerifier matches ed25519:<hex> (raw key extraction) and
  SHA256:<hex> (DER hash); verifies handshake signatures via
  verify_tls13_signature_with_raw_key / verify_tls12/13_signature.
- Extract shared fingerprint logic into alknet_core::fingerprint (pub
  module) reused by endpoint (server-side) and call_client (client-side).
- remote_identity: None is load-bearing (not defaulted to placeholder).
- Integration tests updated to pin the self-signed server cert
  fingerprint (the known-peer path).
This commit is contained in:
2026-06-28 22:23:37 +00:00
parent d9227b8123
commit c106f4a37b
7 changed files with 873 additions and 316 deletions

View File

@@ -11,7 +11,7 @@ name = "alknet_call"
[features]
default = ["quinn"]
quinn = ["dep:quinn", "dep:rustls", "alknet-core/quinn"]
quinn = ["dep:quinn", "dep:rustls", "dep:rustls-native-certs", "dep:rustls-pemfile", "alknet-core/quinn"]
[dependencies]
alknet-core = { path = "../alknet-core" }
@@ -26,8 +26,10 @@ uuid = { version = "1", features = ["v4"] }
futures = "0.3"
parking_lot = "0.12"
quinn = { version = "0.11", optional = true }
rustls = { version = "0.23", optional = true }
rustls = { version = "0.23", optional = true, features = ["aws_lc_rs"] }
rustls-native-certs = { version = "0.8", optional = true }
rustls-pemfile = { version = "2", optional = true }
[dev-dependencies]
rcgen = "0.13"
rustls-pemfile = "2"
hex = "0.4"

View File

@@ -25,12 +25,23 @@ use crate::protocol::connection::CallConnection;
use crate::protocol::dispatch::Dispatcher;
use crate::registry::registration::OperationRegistry;
/// Expected identity of the remote node (ADR-017 §7). The concrete shape is
/// an implementation-detail two-way door; v1 carries a fingerprint string the
/// assembly layer derives from `Capabilities` (ADR-014). Verification is the
/// assembly layer's trust decision — `CallClient` surfaces the expected value
/// so the transport can pin it, but the v1 quinn client config does not enforce
/// a specific verifier (recorded as a two-way-door remainder).
/// 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,
@@ -48,6 +59,10 @@ pub struct CallCredentials {
/// Opaque call-protocol-level auth token, decrypted from the vault.
pub auth_token: Option<alknet_core::auth::AuthToken>,
/// 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<RemoteIdentity>,
}
@@ -173,31 +188,20 @@ impl CallClient {
#[cfg(feature = "quinn")]
fn build_quinn_client_config(
_credentials: &CallCredentials,
credentials: &CallCredentials,
alpn: &[u8],
) -> Result<quinn::ClientConfig, String> {
// The client presents its Ed25519 key as an RFC 7250 raw public key
// client cert (OQ-29, resolved — ADR-030 §6). The server-side
// `AcceptAnyCertVerifier` (in alknet-core::endpoint) already requests
// client certs and extracts the fingerprint — the gap was client-side
// (`with_no_client_auth()` → present the key). This activates the
// `PeerEntry` fingerprint → `peer_id` resolution path.
//
// Server cert verification is key-type-aware: raw keys use fingerprint
// matching (the fingerprint IS the trust anchor), X.509 uses CA
// verification (`WebPkiServerVerifier`). `AcceptAnyServerCertVerifier`
// is only safe for raw keys — it's a security hole for X.509.
//
// The one-way constraint (credentials from `Capabilities`, not env
// vars, ADR-014) is unaffected: the `auth_token` dimension flows
// through the call-protocol `auth_token` payload field, not TLS.
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(Arc::new(AcceptAnyServerCertVerifier))
.with_no_client_auth();
.with_custom_certificate_verifier(verifier)
.with_client_cert_resolver(client_auth);
config.alpn_protocols = vec![alpn.to_vec()];
config.enable_early_data = true;
@@ -206,59 +210,359 @@ fn build_quinn_client_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 `tls_identity` configured)
/// resolves to no client cert (the server gets nothing to fingerprint).
#[cfg(feature = "quinn")]
struct AcceptAnyServerCertVerifier;
fn build_client_auth(
provider: &Arc<rustls::crypto::CryptoProvider>,
tls_identity: &Option<TlsIdentity>,
) -> Result<Arc<dyn rustls::client::ResolvesClientCert>, 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<rustls::crypto::CryptoProvider>,
remote_identity: &Option<RemoteIdentity>,
) -> Result<Arc<dyn rustls::client::danger::ServerCertVerifier>, 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<rustls::RootCertStore, String> {
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")]
impl std::fmt::Debug for AcceptAnyServerCertVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AcceptAnyServerCertVerifier").finish()
fn load_cert_chain(
path: &std::path::Path,
) -> Result<Vec<rustls::pki_types::CertificateDer<'static>>, 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::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())
}
#[cfg(feature = "quinn")]
fn load_private_key(
path: &std::path::Path,
) -> Result<rustls::pki_types::PrivateKeyDer<'static>, 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<rustls::sign::CertifiedKey>,
raw_public_keys: bool,
}
#[cfg(feature = "quinn")]
impl RawKeyClientCertResolver {
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,
}
}
}
#[cfg(feature = "quinn")]
impl rustls::client::danger::ServerCertVerifier for AcceptAnyServerCertVerifier {
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<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 `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<Arc<rustls::sign::CertifiedKey>> {
None
}
fn has_certs(&self) -> bool {
false
}
}
/// `ServerCertVerifier` that pins a specific fingerprint (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.
#[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<'_>,
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> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
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,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
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,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
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<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,
]
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<Box<dyn rustls::sign::Signer>> {
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<rustls::pki_types::SubjectPublicKeyInfoDer<'_>> {
Some(self.spki_public_key())
}
}
#[cfg(feature = "quinn")]
impl rustls::sign::Signer for Ed25519SigningKey {
fn sign(&self, message: &[u8]) -> Result<Vec<u8>, rustls::Error> {
Ok(self.key.sign(message).to_bytes().to_vec())
}
fn scheme(&self) -> rustls::SignatureScheme {
rustls::SignatureScheme::ED25519
}
}
@@ -415,4 +719,222 @@ mod tests {
assert_send_sync::<CallCredentials>();
assert_send_sync::<RemoteIdentity>();
}
#[cfg(feature = "quinn")]
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()
}
#[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<rustls::crypto::CryptoProvider> {
Arc::new(rustls::crypto::aws_lc_rs::default_provider())
}
#[cfg(feature = "quinn")]
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> =
"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<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}"
);
}
#[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<alknet_core::config::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)"
);
}
#[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"
);
}
}

View File

@@ -8,7 +8,7 @@
use std::sync::Arc;
use std::time::Duration;
use alknet_call::client::{CallClient, CallCredentials};
use alknet_call::client::{CallClient, CallCredentials, RemoteIdentity};
use alknet_call::protocol::adapter::CallAdapter;
use alknet_call::protocol::wire::ResponseEnvelope;
use alknet_call::registry::discovery::{
@@ -49,11 +49,14 @@ fn echo_handler() -> Handler {
/// Build a raw quinn server endpoint with a self-signed cert and the
/// `CallAdapter` accepting `alknet/call` connections. Returns
/// `(bound_addr, join_handle)`. The accept loop spawns a task per connection
/// that hands the connection to `CallAdapter::handle`.
/// `(bound_addr, server_fingerprint, join_handle)` — the fingerprint is the
/// `SHA256:<hex>` of the self-signed cert DER, which the client pins via
/// `CallCredentials::with_remote_identity` (the known-peer path, ADR-034 §3).
/// The accept loop spawns a task per connection that hands the connection to
/// `CallAdapter::handle`.
async fn build_raw_quinn_server(
registry: Arc<OperationRegistry>,
) -> (std::net::SocketAddr, tokio::task::JoinHandle<()>) {
) -> (std::net::SocketAddr, String, tokio::task::JoinHandle<()>) {
let provider: Arc<dyn IdentityProvider> = Arc::new(NoopIdentityProvider);
let adapter = Arc::new(CallAdapter::new(
Arc::clone(&registry),
@@ -64,6 +67,8 @@ async fn build_raw_quinn_server(
let params = rcgen::CertificateParams::default();
let cert = params.self_signed(&key_pair).expect("self-signed cert");
let cert_der = cert.der().clone();
let fingerprint = alknet_core::fingerprint::fingerprint_from_cert_der(cert_der.as_ref())
.expect("cert produces fingerprint");
let key_der = rustls::pki_types::PrivateKeyDer::Pkcs8(
rustls::pki_types::PrivatePkcs8KeyDer::from(key_pair.serialize_der()),
);
@@ -112,7 +117,7 @@ async fn build_raw_quinn_server(
}
});
(bound_addr, join)
(bound_addr, fingerprint, join)
}
/// Build the server's registry: an echo op, a secret op, and the
@@ -177,10 +182,14 @@ fn build_server_registry() -> Arc<OperationRegistry> {
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn two_node_call_round_trip() {
let server_registry = build_server_registry();
let (server_addr, _server_join) = build_raw_quinn_server(Arc::clone(&server_registry)).await;
let (server_addr, server_fingerprint, _server_join) =
build_raw_quinn_server(Arc::clone(&server_registry)).await;
// Client side: a CallClient with its own ops so the server can call back
// (connection symmetry).
// (connection symmetry). Pin the server's self-signed cert fingerprint
// (the known-peer path, ADR-034 §3) — `WebPkiServerVerifier` would reject
// it as UnknownIssuer since the self-signed cert is not in the platform
// root store.
let mut client_registry = OperationRegistry::new();
client_registry.register(HandlerRegistration::new(
external_spec("client/echo"),
@@ -193,9 +202,12 @@ async fn two_node_call_round_trip() {
let client_registry = Arc::new(client_registry);
let client = CallClient::new(Arc::clone(&client_registry), Arc::new(NoopIdentityProvider));
let credentials = CallCredentials::new().with_remote_identity(RemoteIdentity {
fingerprint: server_fingerprint,
});
let conn = tokio::time::timeout(
Duration::from_secs(5),
client.connect(server_addr, CallCredentials::new()),
client.connect(server_addr, credentials),
)
.await
.expect("connect did not time out")
@@ -224,15 +236,21 @@ async fn from_call_discovers_and_forwards_over_quic_loopback() {
use alknet_call::registry::context::ScopedOperationEnv;
let server_registry = build_server_registry();
let (server_addr, _server_join) = build_raw_quinn_server(Arc::clone(&server_registry)).await;
let (server_addr, server_fingerprint, _server_join) =
build_raw_quinn_server(Arc::clone(&server_registry)).await;
// Client with an empty registry — from_call will populate its overlay.
// Pin the server's self-signed cert fingerprint (ADR-034 §3 known-peer
// path).
let client_registry = Arc::new(OperationRegistry::new());
let client = CallClient::new(Arc::clone(&client_registry), Arc::new(NoopIdentityProvider));
let credentials = CallCredentials::new().with_remote_identity(RemoteIdentity {
fingerprint: server_fingerprint,
});
let conn = tokio::time::timeout(
Duration::from_secs(5),
client.connect(server_addr, CallCredentials::new()),
client.connect(server_addr, credentials),
)
.await
.expect("connect did not time out")