ADR-007: RFC 7250 cert-type negotiation — the offer follows the identity (OQ-TLS-10 resolved)
Resolve the cert-type negotiation gap (review 001 §U-3, OQ-TLS-10) by deviation from alknet: the gap was a defect in the prior art (alknet's code never delivered its spec's raw-key-over-TCP promise — ADR-082 "works for both QUIC and TCP+TLS"), not behavior to preserve. - FingerprintPinVerifier::requires_raw_public_keys() derives from the pin format: ed25519: -> true (offer [RawPublicKey]), SHA256: -> false (X.509 offer). Crate pin client now completes against the crate raw-key server; SHA256: pins negotiate unchanged. - RawKeyClientCertResolver presents the SPKI under the X.509 offer (only_raw_public_keys() == false): a raw-only client offer can only negotiate against a requires_raw server verifier, and AcceptAnyCertVerifier correctly stays on the default (accepts both cert types). The server extracts the ed25519: fingerprint from the SPKI bytes either way. - Fail-closed preserved and strengthened: an ed25519: pin against an X.509 server now aborts at negotiation (suite 2b), never a downgrade; no API change (no public signature affected; the fix is invisible to consumers apart from working handshakes). - tests/handshake_behavior.rs: suite 3 now runs crate-native (no custom iroh-shaped verifier), new negotiation fail-closed suite, suite 3b inverted to end-to-end success; invariant_pins.rs resolver-offer assertions flipped; unused imports dropped. - Docs: ADR-007 written; OQ-TLS-10 -> resolved-by-deviation; client.md/server.md/overview/README/task postscript synced (incl. the strict-foreign-server limit in ADR-007 §Limits). Verification: cargo test 81 / --features tcp 94 / --all-features 105 green; clippy -D warnings clean (default + all-features); fmt clean; cargo doc warning-free.
This commit is contained in:
@@ -22,7 +22,7 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
|
||||
| OQ-TLS-07 | iroh key surface | **resolved** (ADR-005, byte access pinned) | low |
|
||||
| OQ-TLS-08 | `quinn` → `noq` feature rename | **resolved** (ADR-003) | high |
|
||||
| OQ-TLS-09 | Server-path proof-of-possession | **open** | high |
|
||||
| OQ-TLS-10 | RFC 7250 over TCP: cert-type negotiation gap | **open** | high |
|
||||
| OQ-TLS-10 | RFC 7250 over TCP: cert-type negotiation gap | **resolved** (ADR-007) | high |
|
||||
|
||||
## Identity & types
|
||||
|
||||
@@ -156,52 +156,24 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
|
||||
(`server/hs.rs::process_cert_type_extension`,
|
||||
`client/hs.rs::process_cert_type_extension`); pinned by
|
||||
`tests/handshake_behavior.rs`.
|
||||
- **Status**: open (recorded 2026-09-11)
|
||||
- **Priority**: high
|
||||
- **Mechanism** (rustls 0.23.44, verified):
|
||||
- A raw-key *server* resolver (`only_raw_public_keys() == true`)
|
||||
requires the client to offer `server_certificate_types =
|
||||
[RawPublicKey]`. rustls' client sends that offer only when the
|
||||
client verifier overrides `requires_raw_public_keys() == true`.
|
||||
- This crate's `FingerprintPinVerifier` keeps the trait default
|
||||
(`false`), and `AcceptAnyCertVerifier` never overrides it either.
|
||||
- Consequently: crate-pin-client ↔ crate-raw-key-server fails; and a
|
||||
raw-key *client* resolver (offering `[RawPublicKey]` client cert
|
||||
types) fails against `AcceptAnyCertVerifier` (the
|
||||
`(false, true, false)` arm → `IncorrectCertificateTypeExtension`).
|
||||
The fail-closed rule still holds — no path downgrades — but the
|
||||
raw-key-over-TCP interop the extraction implies does not exist
|
||||
yet.
|
||||
- **Context**: the raw-key paths that work today are iroh's (its
|
||||
built-in TLS overrides `requires_raw_public_keys() == true` on both
|
||||
verifiers — `iroh/src/tls/verifier.rs`) and the QUIC path
|
||||
(noq/iroh negotiate cert types differently from rustls' TCP
|
||||
state machine). alknet's extracted client never negotiated
|
||||
raw-key-over-TCP either (no override in alknet-tls) — behavior
|
||||
preservation holds; the gap is inherited, not introduced.
|
||||
- **Options**:
|
||||
- **(a) Document the gap** — raw-key peers ride iroh/noq (their own
|
||||
TLS stacks), not rustls TCP+TLS; TCP+TLS is the X.509 transport.
|
||||
No code change; the handshake tests pin the executed behavior.
|
||||
- **(b) Add a `requires_raw_public_keys() == true` override** on
|
||||
`FingerprintPinVerifier` for `ed25519:` pins (the iroh shape).
|
||||
Changes the pin verifier's negotiation: a pin client could reach
|
||||
raw-key servers — but then an X.509 remote pinned by `SHA256:`
|
||||
could no longer negotiate with the same client config (the offer
|
||||
would exclude X509), so the two pin formats cannot share one
|
||||
client config. An API-shape decision before the first consumer.
|
||||
- **(c) Add a server-side verifier that overrides
|
||||
`requires_raw_public_keys() == true`** (raw-key-only client auth),
|
||||
additive like OQ-TLS-09's option (b); pairs with it if mandatory
|
||||
raw-key client auth is wanted.
|
||||
- **Constraints**: the executed behavior is pinned both ways by
|
||||
`tests/handshake_behavior.rs` (`raw_key_server_path_completes_…`
|
||||
with an iroh-shape verifier;
|
||||
`raw_key_client_resolver_fails_against_accept_any_cert_verifier`) —
|
||||
any decision must update those tests together with this OQ.
|
||||
- **Cross-references**: src/client.rs (`FingerprintPinVerifier` — the
|
||||
default `false`), src/server.rs (`RawKeyCertResolver`,
|
||||
`AcceptAnyCertVerifier`), review 001 §N-4 (the client-resolver trap),
|
||||
- **Status**: resolved (2026-09-11) — by deviation from alknet,
|
||||
[ADR-007](decisions/007-cert-type-negotiation.md). Option (b) plus a
|
||||
resolver change: `FingerprintPinVerifier::requires_raw_public_keys()`
|
||||
derives from the pin format (`ed25519:` → `true`, `SHA256:` →
|
||||
`false`), and `RawKeyClientCertResolver` presents the SPKI under the
|
||||
X.509 offer (`only_raw_public_keys() == false`). The gap was a bug in
|
||||
the prior art (alknet's code never delivered its spec's raw-key-over-
|
||||
TCP promise), not an alknet behavior to preserve — so the
|
||||
behavior-preservation invariant does not cover it. The two pin
|
||||
formats negotiate independently (per-connection verifier, never
|
||||
shared); fail-closed is preserved and strengthened (an `ed25519:`
|
||||
pin against an X.509 server aborts at negotiation, never a
|
||||
downgrade). The original "defer until the first consumer" rationale
|
||||
was circular — the crate is the component that must enable the
|
||||
raw-key-over-TCP consumer, and the fix required no API change.
|
||||
- **Cross-references**: src/client.rs (`FingerprintPinVerifier`,
|
||||
`RawKeyClientCertResolver`), src/server.rs (`RawKeyCertResolver`,
|
||||
`AcceptAnyCertVerifier`), review 001 §N-4, ADR-007,
|
||||
iroh `iroh/src/tls/verifier.rs` (the working prior art)
|
||||
|
||||
## Quality / process
|
||||
@@ -235,9 +207,4 @@ are authoritative; the Phase 0 doc's statuses are the historical record.
|
||||
|
||||
- OQ-TLS-09 (server-path proof-of-possession): open by design — the
|
||||
decision needs the rewrite's auth-layer design in hand (option (c))
|
||||
or an API-shape call before the first consumer (option (b)).
|
||||
- OQ-TLS-10 (RFC 7250 over TCP negotiation gap): open by design —
|
||||
behavior-preserving (the gap is inherited from alknet); deciding
|
||||
needs the first raw-key-over-TCP consumer in hand (options (b)/(c)
|
||||
are API-shape decisions), or the X.509-only TCP posture is
|
||||
documented as-is (option (a)).
|
||||
or an API-shape call before the first consumer (option (b)).
|
||||
Reference in New Issue
Block a user