--- id: port-client name: Port client side — TlsClientConfig, verifier selection, client auth (src/client.rs) status: completed depends_on: [port-identity-types, port-fingerprint, port-pem-signing] scope: broad risk: medium impact: phase level: implementation tags: [client, verifiers, port, invariants] --- ## Description Port `src/client.rs` from alknet-tls per the ADRs: `TlsClientConfig`, `select_server_verifier`, `build_client_auth`, `RawKeyClientCertResolver`, `NoClientCertResolver`, `FingerprintPinVerifier`, `load_platform_root_cert_store`. ### The API deltas (from the extracted code) - Types rewire: `ConnectionCredentials`/`RemoteIdentity` come from this crate's `credentials.rs` (ADR-005); fingerprint helpers from this crate's `fingerprint.rs`; the identity from this crate's `identity.rs`. - `for_noq(self)` replaces `for_quinn(self)` (consuming — ADR-004); `into_rustls_config(self)` unchanged. ### Credentials module (small, rides here) `src/credentials.rs` — port `ConnectionCredentials` (`local_identity: Option`, `remote_identity: Option`, the builder methods) and `RemoteIdentity` (`fingerprint: String`) from alknet-core `credentials.rs`, including the load-bearing doc comments (`None` is the public-X.509-endpoint state, not a placeholder — the `Option`s drive verifier selection). This task owns the module because the verifier selection is the semantic consumer of the bundle; the co-location prevents semantic drift (ADR-005). ### The invariants / selection matrix (test-pinned here) - Every config: `enable_early_data = true` (the client half of the 0-RTT invariant — ADR-001; pinned as a test) and the aws-lc-rs provider. - Verifier selection matrix: `Some(fingerprint)` → `FingerprintPinVerifier`; `None` → `WebPkiServerVerifier` over the root store. Fail-closed for unknown raw-key remotes is structural (the CA verifier is what `None` installs; a raw-key remote cannot satisfy it — the failure manifests at handshake, never via `TlsError`). - Root-store fallback: platform certs first; if empty, merge `webpki-roots` (never empty); native-certs load errors logged, not returned. Testable by asserting the merge path (construct with an empty platform store simulation if the API permits; otherwise assert the fallback branch by construction — `load_platform_root_cert_store` returns a non-empty store). - Client-auth presentation: RawKey → RFC 7250 SPKI cert with `only_raw_public_keys()` auto-detected from the DER; X509 → loaded chain via `CertifiedKey::from_der` (errors → `TlsError::Rustls` per ADR-002); `SelfSigned`/`None` → `NoClientCertResolver` (`has_certs() == false`); `Acme` → `TlsError::AcmeConfig`. - `FingerprintPinVerifier`: pin match / mismatch on `verify_server_cert`; TLS 1.2/1.3 signature verification routes Ed25519 SPKI through `verify_tls13_signature_with_raw_key`; a mismatched pin fails verification. ## Work 1. Port `credentials.rs` (types + builders + doc comments). 2. Port `client.rs`; apply the deltas; rewire imports. 3. Port the extracted in-module tests. 4. Add the selection-matrix integration test (below). ## Verification - [x] Selection-matrix test: all four client-auth presentations × both verifier branches construct and select the expected resolver types (inspect via the config's client-auth/verifier state where the API permits; otherwise assert construction success/error kind per cell) - [x] `Acme` local identity → `TlsError::AcmeConfig` - [x] `enable_early_data == true` pinned - [x] Root store non-empty (fallback exercised) - [x] FingerprintPinVerifier unit tests ported (pin match, mismatch, raw-key signature routing) - [x] `cargo test` (default), `cargo test --all-features`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check` ## Acceptance Criteria - [x] `for_noq(self)` / `into_rustls_config(self)` per ADR-004 - [x] The selection matrix has no fourth path (fail-closed structural) - [x] `lib.rs` re-exports the client surface + `ConnectionCredentials` / `RemoteIdentity` ## References - docs/architecture/client.md (the normative doc) - docs/architecture/decisions/002-tlserror-shape.md, 004, 005 - alknet ADR-034 §3, ADR-088 §5, ADR-091 - Prior art: `/workspace/@alkdev/alknet/crates/alknet-tls/src/client.rs`, `/workspace/@alkdev/alknet/crates/alknet-core/src/credentials.rs` ## Notes > Agent fills this during implementation. - Verifier selection / client-auth presentation are inspected through the `rustls::ClientConfig` Debug output (`ClientConfig` derives `Debug` and embeds the verifier's debug name — `FingerprintPinVerifier` / `WebPkiServerVerifier`) and the public `client_auth_cert_resolver` field; the `verifier` field itself is `pub(super)` at rustls 0.23.44, so the debug-string probe is the structural assertion shape. - `select_server_verifier`'s `WebPkiServerVerifier` build error maps to `TlsError::VerifierBuild` via `#[from] rustls::client::VerifierBuilderError` (the `rustls::webpki` module is private at 0.23.44; same type, public path — lib.rs's variant doc already records this). - The signature-routing unit tests construct `DigitallySignedStruct` from its wire encoding through the doc-hidden `rustls::internal::msgs` surface (`Codec::read`; `new` is `pub(crate)`), then drive `verify_tls12_signature` / `verify_tls13_signature` on the SPKI-as-`CertificateDer` exactly as rustls presents it in an RFC 7250 handshake — pin match + signature possession-proof both asserted. - The `for_noq()` wrap relies on `TlsError::NoqWrap(#[from])` (`NoInitialCipherSuite` → `?`), ADR-002/ADR-003. - The extracted `TlsClientConfig`'s `#[allow(dead_code)]` is kept (the server-side `rustls_config()` accessor shape is ADR-004's; no consumer touches the field within this crate yet). ## Summary > Agent fills this on completion. Ported `src/credentials.rs` (wholesale from alknet-core, doc comments rewired to this crate's ADRs — ADR-005, alknet ADR-091/034/030 semantics preserved verbatim in substance) and `src/client.rs` (port of alknet-tls `client.rs` with the task's error-mapping deltas and import rewires). Visibility: `RawKeyClientCertResolver`, `NoClientCertResolver`, `FingerprintPinVerifier` made `pub` (lib.rs re-exports them); `build_client_auth`, `select_server_verifier`, `load_platform_root_cert_store` `pub`. `lib.rs` gained the client + credentials re-export lines; the pending-modules comment now notes only the server line remains (its port task owns it). Deltas vs the extraction: - `TlsError` mapping per ADR-002: `with_safe_default_protocol_versions` / `CertifiedKey::from_der` / `RootCertStore::add` errors → `TlsError::Rustls` (`#[from] rustls::Error`); the `WebPkiServerVerifier` build error → `TlsError::VerifierBuild` (`#[from] rustls::client::VerifierBuilderError`); the Acme client-auth error string → `TlsError::AcmeConfig`. No `Config(String)` catch-all. - `for_quinn` → `for_noq` (ADR-003/004): consuming, noq-gated; the wrap error flows through `TlsError::NoqWrap(#[from])` — no `map_err` stringification. - Imports rewired to `crate::{credentials, fingerprint, identity}`; the `PeerEntry` reference in the extraction's `NoClientCertResolver` doc became the peer-id resolution language (auth layer stays out, ADR-005). - Invariants pinned by tests: `enable_early_data = true` + exact ALPN; aws-lc-rs default provider (9-suite set + `crypto_provider()` identity); root store non-empty; verifier-selection matrix (`Some` → `FingerprintPinVerifier`, `None` → `WebPkiServerVerifier`); client-auth presentation matrix (RawKey → RFC 7250 raw pub keys, X509 → loaded chain, SelfSigned/None → nothing, Acme → `TlsError::AcmeConfig`); FingerprintPinVerifier pin match/mismatch (Ed25519 SPKI + SHA256 X.509) and raw-key signature routing (TLS 1.2 + 1.3, forged-signature rejection). Verification: `cargo test` (56 pass), `cargo test --all-features` (59 pass, incl. both `for_noq` tests), `cargo clippy --all-targets --all-features -- -D warnings` (clean), `cargo fmt --check` (clean), `cargo check --features noq` / `--features tcp` / default (clean).