Close review 001 §S-1: the default client-cert verifier never checked the client's CertificateVerify, so anyone holding a peer's *public* cert/SPKI bytes (public by design — peers publish them to be dialable) could complete a handshake as that peer, and the auth layer could not detect it. The consumer designs are known (X.509 and raw-key TCP/QUIC endpoints with identity-bearing clients), so implementing now — the zero-consumer moment — avoids the guaranteed breaking republish of flipping the default later. - VerifyPresentedCertVerifier (new): request, don't require, verify possession — permissive verify_client_cert (self-signed chains and bare SPKIs stay valid presentation) + CertificateVerify routing by presented cert kind (Ed25519 SPKI -> verify_tls13_signature_with_ raw_key both TLS versions; X.509 -> standard route), the same routing FingerprintPinVerifier implements. Nine-scheme list verbatim (shared fn, exact-list pin covers both). - Default on every TlsServerConfig path — X509 / RawKey / SelfSigned / ACME (the verifier install is crate-side rustls in new_acme, not rustls-acme's). - AcceptAnyCertVerifier stays public as the explicit no-pop escape hatch, no longer installed by any crate path. - tests/impersonation_posture.rs: four pins — default rejects the attacker (X.509: UnsupportedSignatureAlgorithmForPublicKeyContext; raw-key: BadSignature), escape hatch still accepts + extracts the victim's fingerprint (both cert types). - tests/handshake_behavior.rs: suites 4/4b — possession-checked legit clients (raw-key pin vs raw-key server; X.509 client vs X.509 server) complete and the server extracts the fingerprint; suite 3b doc updated. - Docs: ADR-008 written; OQ-TLS-09 -> resolved (option (b)); ADR-007 §Limits deferral retired to not-planned; server.md/client.md invariants/README/overview synced. Verification: cargo test 81 / --features tcp 95 / --all-features 104 green; clippy -D warnings clean (default + all-features); fmt clean; cargo doc warning-free.
8.4 KiB
status, last_updated
| status | last_updated |
|---|---|
| reviewed | 2026-09-11 |
alktls — Server side
TlsServerConfig and its resolvers: the server-side TLS setup,
extracted from alknet (crates/alknet-tls/src/server.rs) and ported
per the ADRs. Statuses of the decisions referenced here: see
open-questions.md and the ADR index in
overview.md.
TlsServerConfig
The central server-side type. Built once from a TlsIdentity + ALPN
list, shared across transports via Arc (not Clone — it holds the
ACME task's JoinHandle).
pub struct TlsServerConfig {
rustls_config: rustls::ServerConfig, // Clone-safe — Arc internally
acme_handle: Option<tokio::task::JoinHandle<()>>, // acme-gated
}
impl TlsServerConfig {
pub async fn new(identity: &TlsIdentity, alpns: &[Vec<u8>])
-> Result<Self, TlsError>;
#[cfg(feature = "noq")]
pub fn for_noq(&self) -> Result<noq::ServerConfig, TlsError>;
#[cfg(feature = "tcp")]
pub fn for_tcp_tls(&self) -> tokio_rustls::TlsAcceptor;
pub fn rustls_config(&self) -> &rustls::ServerConfig;
}
Construction dispatch (ADR-001's identity model):
X509 { cert, key }— loads the chain + key from disk (pem.rs),with_single_cert.RawKey(Ed25519SecretKey)—RawKeyCertResolverpresents the Ed25519 key as an RFC 7250 raw public key server cert.SelfSigned—generate_self_signed_cert()(rcgen), in-memory.Acme { domains, cache_dir, directory, contact }— the ACME path (below). Server-only: on the client path it is a config error.
Behavior-preservation invariants
These are load-bearing (ADR-001); an implementation that omits any of them compiles but silently changes TLS behavior. Each is asserted by test (ADR-006):
max_early_data_size = u32::MAXon every server config path — enables 0-RTT / early data. Omitting it silently breaks 0-RTT clients.rustls::crypto::aws_lc_rs::default_provider()as the crypto provider on all paths (alknet ADR-084). Neverring, never the process-default provider, without a new ADR.- The nine-scheme
supported_verify_schemes()(shared byVerifyPresentedCertVerifierandAcceptAnyCertVerifier) returns ED25519 + ECDSA P-256/P-384 + RSA PSS (SHA256/384/512) + RSA PKCS1 (SHA256/384/512) — nine schemes, verbatim, pinned by an exact-list integration test. - The default client-cert verifier verifies possession
(ADR-008):
VerifyPresentedCertVerifieris installed on every path; the CertificateVerify is checked against the presented cert's public key.AcceptAnyCertVerifierexists only as the explicit escape hatch. acme-tls/1ALPN append for the ACME path only, done by the crate, not the caller (alknet ADR-027 §7).- Non-empty root store — the client CA path merges
webpki-rootswhen the platform store is empty (see client.md).
VerifyPresentedCertVerifier (the default) and AcceptAnyCertVerifier (the escape hatch)
The server-side client-cert verifier: request, don't require, verify
possession (ADR-008, resolving OQ-TLS-09).
VerifyPresentedCertVerifier
is the default on every TlsServerConfig path (X509 / RawKey /
SelfSigned / ACME). It asks for a client cert (X.509 or RFC 7250 raw
key) so the caller can extract the fingerprint via peer_identity(),
does not require one, and does not verify the presented cert against a
CA — self-signed chains and bare SPKIs are valid presentation. The
client's CertificateVerify signature is verified against the
presented cert's public key (Ed25519 SPKIs route through
verify_tls13_signature_with_raw_key; X.509 through the standard
route): the extracted fingerprint is possession-checked — presenting a
victim's public bytes under an attacker's key fails the handshake.
Who the fingerprint maps to remains the auth layer's concern
(ADR-005).
The escape hatch: AcceptAnyCertVerifier is the documented no-pop
verifier — same request-not-require shape, no CertificateVerify check,
so the fingerprint it extracts is attacker-suppliable (S-1). Install
it explicitly only when a deployment deliberately wants that posture.
Both postures are pinned by tests/impersonation_posture.rs (default
rejects the attacker, escape hatch accepts — both cert types); a
change must update that test together with this doc.
RawKeyCertResolver
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 (signing.rs).
Interop note (ADR-007, executed in
tests/handshake_behavior.rs): a raw-key server resolver requires
the client to offer [RawPublicKey] server cert types, which rustls
sends only when the client verifier overrides
requires_raw_public_keys() == true. The crate's own
FingerprintPinVerifier now does exactly that for ed25519: pins
(the offer follows the pin format), so a crate pin client completes
against this raw-key server
(raw_key_server_path_completes_with_crate_pin_client). A raw-key
client identity presents its SPKI under the X.509 offer, which the
verifiers (requires_raw_public_keys() == false — correctly; both
accept X.509-or-raw cert types) accept end-to-end, the default
possession-verifying the presentation
(raw_key_client_presents_spki_and_server_extracts_fingerprint,
raw_key_client_vs_raw_key_server_default_verifier_checks_possession).
Raw-key peers riding iroh/noq are unaffected (their TLS stacks own
their negotiation).
The ACME path
For TlsIdentity::Acme, new (feature acme):
- Builds
rustls_acme::AcmeConfig— the upstream builder, distinct fromTlsError::AcmeConfig(domains,DirCachecache dir, directory URL fromAcmeDirectory, contacts). - Wires
state.resolver()as the cert resolver into the server config. - Appends
acme-tls/1to the ALPN list (TLS-ALPN-01 challenge). - Spawns the event-loop task (
tokio::spawn) matchingEventOk/EventErrorvariants totracinglogs, and returns immediately — it does not await the first certificate.
Lifecycle semantics:
- The returned config is usable right away; handshakes fail transiently until the first order completes (or a cached cert deploys).
- Order errors log at
warn!and retry insiderustls-acme; the task exits only when the event stream ends. - The task is detached: the stored
JoinHandleis never aborted; ACME runs for the process lifetime (OQ-TLS-06 resolved detached-only; ashutdown()surface would be additive later if the rewrite's graceful-shutdown design wants one). - One state machine per domain — never spawn a second ACME task
for an already-served domain (duplicate orders = Let's Encrypt
rate-limit risk + cert-cache divergence). This is why
TlsServerConfigis notClone. - Runtime errors are stream events, not
TlsErrorvariants (ADR-002's scope boundary).TlsError::AcmeConfigcovers config-mismatch mistakes ("ACME feature not enabled butAcmeconfigured" — and on the client path,Acmeused for client auth), not runtime failures.
What the server side does NOT do
- No accept loop:
for_tcp_tls()yields aTlsAcceptor; theTcpListener::accept→TlsAcceptor::accept→ dispatch loop belongs to the caller (alknet ADR-083 — the endpoint takes no TLS config; the assembly layer builds configs and transports). - No ALPN policy: the ALPN list is a parameter (the assembly layer
filters per endpoint type — alknet ADR-086 §3); the crate appends
only
acme-tls/1on the ACME path. - No handshake: verifier selection and handshake outcomes on the
server side are the default
VerifyPresentedCertVerifier(or the explicitly-installed escape hatch) + the caller's fingerprint extraction; a rejected handshake is the transport's error, notTlsError. - No peer resolution: the extracted fingerprint string goes to the
caller;
PeerEntry/AuthPolicylive in the auth layer.
References
- overview.md — the index; client.md — the client side
- alknet
crates/tls/README.md§Architecture — the full server-side spec this doc mirrors - ADR-001 (invariants), ADR-002 (
TlsError), ADR-003 (for_noq), ADR-004 (accessors), ADR-005 (identity types), ADR-006 (modules, tests), ADR-007 (cert-type negotiation), ADR-008 (possession verification)