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.
189 lines
8.4 KiB
Markdown
189 lines
8.4 KiB
Markdown
---
|
|
status: reviewed
|
|
last_updated: 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](open-questions.md) and the ADR index in
|
|
[overview.md](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`).
|
|
|
|
```rust
|
|
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.
|
|
- `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::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](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`](decisions/008-server-path-possession-verification.md)
|
|
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::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/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](overview.md) — the index; [client.md](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) |