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.
5.9 KiB
status, last_updated
| status | last_updated |
|---|---|
| reviewed | 2026-09-10 |
alktls — Overview
Purpose
alktls is the TLS layer of the alk* stack: shared TLS setup types —
server and client rustls configs, cert resolvers, verifiers, and
ACME state-machine wiring — transport-agnostic and shareable across
transports. It is the extraction of the TLS handling from alknet
(crates/alknet-tls + the identity/credential/fingerprint types in
crates/alknet-core), and it is the TLS crate the alknet rewrite
consumes.
The crate owns config construction: given an identity and an ALPN
list, produce a rustls::ServerConfig or rustls::ClientConfig, and
hand it to whichever transport wrapper the deployment runs. It does
not dial, accept, dispatch, or resolve peer identities — those are
the dial seam's, the accept loop's, and the auth layer's jobs
(alknet ADR-083/089; the scope boundary in ADR-001).
Terminology. The assembly layer is the deployment binary that
builds the TlsServerConfigs / TlsClientConfigs, builds the
transports from them, and wires the results together (alknet
ADR-014's term — in practice, the hub/worker/endpoint binary). The
dial seam is the outbound connection point the assembly layer (or a
client crate) consumes; the accept loop is the inbound counterpart.
alktls is the cert provider for both, never the loops themselves.
The transport picture
A deployment (endpoint / hub) assembles a subset of three transports; alktls serves the first two directly and feeds the third a key:
| Transport | Stack | What alktls provides |
|---|---|---|
| TCP+TLS | tokio-rustls |
TlsServerConfig::for_tcp_tls() → TlsAcceptor (feature tcp); TlsClientConfig::into_rustls_config() → TlsConnector (ungated) |
| QUIC | noq (iroh's extracted quinn fork) |
for_noq() on both configs (feature noq) |
| iroh | iroh's own TLS | nothing — Ed25519SecretKey 32-byte access feeds iroh_base::SecretKey (key-not-config) |
One identity, N transports: the inner rustls config is Clone
(Arc-shared resolvers); one TlsServerConfig feeds every transport an
endpoint runs. One ACME state machine per domain, shared across
transports — duplicate orders risk Let's Encrypt rate limits and
cert-cache divergence (ADR-001).
What the crate is
The public API surface (ADR-004, ADR-005):
TlsServerConfig— built once from aTlsIdentity+ ALPN list; accessorsfor_noq(),for_tcp_tls(),rustls_config(). NotClone(holds the ACME task'sJoinHandle); share viaArc.TlsClientConfig— built per dial fromConnectionCredentials+ ALPN; accessorsfor_noq(),into_rustls_config().TlsError— the ADR-088 six-variant#[non_exhaustive]config-construction error type (ADR-002).- Identity types —
TlsIdentity(X509/RawKey/SelfSigned/Acme),Ed25519SecretKey,AcmeDirectory(ADR-005). - Credential bundle —
ConnectionCredentials,RemoteIdentity(ADR-005); drives the verifier selection matrix (alknet ADR-034). - Fingerprint helpers —
fingerprint_from_cert_der,extract_ed25519_raw_key_from_spki(ADR-005); normalizeded25519:<hex>/SHA256:<hex>formats.
Design Decisions
All design decisions are documented as ADRs in decisions/.
| ADR | Decision | Summary |
|---|---|---|
| 001 | Inherit the alknet TLS design | The alknet ADRs + crate spec are the baseline; deviations recorded as alktls ADRs |
| 002 | TlsError — ADR-088 shape from day one |
Six typed variants, #[non_exhaustive], no string catch-all; config-construction scope boundary |
| 003 | The QUIC feature is noq |
noq 1.2 (iroh's extracted fork) replaces quinn pre-consumer; iroh stays key-not-config |
| 004 | Complete the accessors | for_tcp_tls() adopted; server accessors borrow (&self), client accessors consume |
| 005 | Config types move into alktls | Identity + credentials + fingerprint move in; auth layer stays out |
| 006 | Module layout and test surface | Eight modules; in-module seed tests + integration invariant pins |
| 007 | RFC 7250 cert-type negotiation | The cert-type offer follows the identity/pin format — raw-key-over-TCP works (deviation from alknet; resolves OQ-TLS-10) |
| 008 | Server-path possession verification | The verifying verifier is the default on every server path; AcceptAnyCertVerifier is the explicit escape hatch (resolves OQ-TLS-09) |
Open Questions
Open questions are tracked in open-questions.md. All Phase 0 questions (OQ-TLS-01..08) were resolved at Phase 1 entry (2026-09-10) — six via ADR-001..006, two resolved as documented behavior (OQ-TLS-02, OQ-TLS-06); the tracker records the resolutions with pointers.
Components
- server.md —
TlsServerConfig, resolvers, the ACME path, and the server-side invariants - client.md —
TlsClientConfig, verifier selection, client-auth presentation, the root-store fallback
Relationship to the alknet docs
The full design rationale lives in alknet's architecture docs
(/workspace/@alkdev/alknet/docs/architecture/): ADR-082 (why a
standalone TLS crate — the cert-reuse problem), ADR-083 (why the
endpoint takes no TLS config), ADR-084 (why aws-lc-rs), ADR-027 (the
identity model), ADR-034 (verifier selection), ADR-086 §3 (split ALPN
lists), ADR-087 (TlsClientConfig), ADR-088 (TlsError, root-store
fallback), ADR-089 (the dial seam), ADR-091 (ConnectionCredentials),
and crates/tls/README.md (the full crate spec). alktls' ADR series
records its own posture and points at the alknet reasoning rather
than duplicating it.