port-server (src/server.rs, 614 lines): - TlsServerConfig with ADR-004 accessors: for_noq(&self) (borrow + inner-clone + NoqWrap via #[from]), for_tcp_tls(&self) adopted (infallible TlsAcceptor), rustls_config(&self) adopted - acme_handle renamed per ADR-006; the defensive Acme arm returns TlsError::AcmeConfig (unreachable! eliminated) - error sites remapped to typed variants (Rustls/SelfSigned/AcmeConfig) - ACME path verbatim: DirCache + directory + contacts + resolver + acme-tls/1 + spawned event loop (tracing lines ported), returns immediately, detached handle - 14 tests incl. the nine-scheme exact-list pin, ACME spawn/return/ALPN assertion (blackhole URL — no network I/O), no-feature AcmeConfig, for_tcp_tls/rustls_config round-trips port-client (src/credentials.rs + src/client.rs): - ConnectionCredentials/RemoteIdentity wholesale with load-bearing Option-semantics docs (None = public-X.509 state, Some = pin) - verifier selection matrix + client-auth presentation matrix test-pinned at unit level; enable_early_data=true pinned; root-store fallback asserted non-empty - FingerprintPinVerifier: pin match/mismatch + raw-key signature routing (verify_tls13_signature_with_raw_key) asserted - resolvers/verifier made pub for the re-export block Cargo.toml (two required deltas): - noq feature gains aws-lc-rs: lockfile resolves noq-proto 1.3.0 where ServerConfig::with_crypto is #[cfg(any(aws-lc-rs, ring))]; ADR-003's TOML block was written against the 1.2 API. Amendment note recorded in tasks/port-server.md for the review-impl sync - acme = [dep:rustls-acme, dep:futures] — ADR-006 already gates the futures dep on acme; the scaffold omitted it lib.rs: re-export block complete (all eight modules) Verification: cargo test 68, --all-features 75, --features noq 72, clippy -D warnings, fmt --check, doc --no-deps (0 warnings) — green
176 lines
8.1 KiB
Markdown
176 lines
8.1 KiB
Markdown
---
|
||
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<TlsIdentity>`, `remote_identity: Option<RemoteIdentity>`, 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). |