Files
alktls/docs/architecture/server.md
T
glm-5.3-flash ac440f3a9a ADR-008: server-path possession verification — the verifying verifier is the default (OQ-TLS-09 resolved)
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.
2026-09-11 11:12:48 +00:00

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)RawKeyCertResolver presents the Ed25519 key as an RFC 7250 raw public key server cert.
  • SelfSignedgenerate_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::MAX on 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). Never ring, never the process-default provider, without a new ADR.
  • The nine-scheme supported_verify_schemes() (shared by VerifyPresentedCertVerifier and AcceptAnyCertVerifier) 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): VerifyPresentedCertVerifier is installed on every path; the CertificateVerify is checked against the presented cert's public key. AcceptAnyCertVerifier exists only as the explicit escape hatch.
  • acme-tls/1 ALPN 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-roots when 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):

  1. Builds rustls_acme::AcmeConfig — the upstream builder, distinct from TlsError::AcmeConfig (domains, DirCache cache dir, directory URL from AcmeDirectory, contacts).
  2. Wires state.resolver() as the cert resolver into the server config.
  3. Appends acme-tls/1 to the ALPN list (TLS-ALPN-01 challenge).
  4. Spawns the event-loop task (tokio::spawn) matching EventOk/EventError variants to tracing logs, 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 inside rustls-acme; the task exits only when the event stream ends.
  • The task is detached: the stored JoinHandle is never aborted; ACME runs for the process lifetime (OQ-TLS-06 resolved detached-only; a shutdown() 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 TlsServerConfig is not Clone.
  • Runtime errors are stream events, not TlsError variants (ADR-002's scope boundary). TlsError::AcmeConfig covers config-mismatch mistakes ("ACME feature not enabled but Acme configured" — and on the client path, Acme used for client auth), not runtime failures.

What the server side does NOT do

  • No accept loop: for_tcp_tls() yields a TlsAcceptor; the TcpListener::acceptTlsAcceptor::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/1 on 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, not TlsError.
  • No peer resolution: the extracted fingerprint string goes to the caller; PeerEntry/AuthPolicy live 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)