Files
alknet/tasks/core/endpoint-client-fingerprint.md
glm-5.2 97216764ea fix: resolve review #004 findings W1-W4 + close review gate
W1 (call/protocol/abort-cascade-wiring): wire AbortCascade into
CallAdapter handle_stream for EVENT_ABORTED. Cascades with
AbortPolicy::AbortDependents, aborts root, no descendant frames on
wire (ADR-016 Decision 2). Two integration tests added.

W2 (core/endpoint-client-fingerprint): extract TLS client cert
fingerprint in dispatch_quinn (SHA256:<hex> of leaf cert DER via
peer_identity) and dispatch_iroh (ed25519:<hex> of peer NodeId).
Fingerprint format documented in auth.md. Server config change
(with_no_client_auth → request-but-don't-require) deferred to new
follow-up task core/endpoint-request-client-cert.

W3 (vault/mnemonic-debug-redaction): replace Mnemonic derive(Debug)
with manual redacting impl (phrase: "[REDACTED]"). Seed confirmed
no Debug impl. Redaction test added.

W4 (core/auth-apikey-resources): Option B — drop entry.resources from
spec. External identities (token/fingerprint) grant scopes only;
resource-scoped ACLs are composition-internal (ADR-015/022). auth.md
corrected + limitation documented. Two tests confirm empty resources.

review-post-impl-fixes: all 4 verified, workspace green (326 tests,
0 failures, 0 clippy warnings). Review #004 status → resolved.

Graph: 34 tasks, 12 gens.
2026-06-24 11:00:54 +00:00

131 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: core/endpoint-client-fingerprint
name: Extract TLS client certificate fingerprint in endpoint dispatch (ADR-004)
status: completed
depends_on: []
scope: narrow
risk: medium
impact: component
level: implementation
---
## Description
Both dispatch functions in `crates/alknet-core/src/endpoint.rs` hard-code
`tls_client_fingerprint: None` when calling `build_auth_context`
(endpoint.rs:306 and 396). As a result, `AuthContext.identity` (the
endpoint-resolved identity) is always `None` at the endpoint layer, and
all identity resolution is deferred to handler-level code. The
endpoint-level auth resolution path described in
`docs/architecture/crates/core/auth.md:159171` is non-functional:
> "QUIC connection arrives → TLS handshake → Extract TLS client
> certificate fingerprint (if presented) → If fingerprint present:
> `IdentityProvider::resolve_from_fingerprint()` → `auth.identity =
> Some(identity)` → Construct `AuthContext { identity, alpn, remote_addr,
> tls_client_fingerprint }`"
This matters most for P2P nodes using RFC 7250 raw Ed25519 keys (the
"default for most alknet nodes" per OQ-12), where the connection-level
identity *is* the TLS client cert — there is no separate protocol-level
credential to extract. Without endpoint-level fingerprint extraction,
a raw-key peer connecting to an `alknet/call` endpoint cannot be
identified by fingerprint at the endpoint layer.
### Quinn path
`extract_quinn_alpn` (endpoint.rs:316326) already downcasts
`connection.handshake_data()` to `quinn::crypto::rustls::HandshakeData`.
The same `HandshakeData` struct exposes the peer's client certificate
chain when one was presented. Extract the chain, hash the leaf cert's
DER to a `SHA256:`-prefixed fingerprint string (matching the format
`AuthPolicy::resolve_identity_from_fingerprint` expects — see
auth.md:152), and pass it to `build_auth_context` in place of `None`.
Note: `rustls::ServerConfig` is currently built with
`with_no_client_auth()` (endpoint.rs:450, 463, 473), so the server does
not *request* client certs. To actually receive a client cert, the
server config must use `with_client_auth()` or an equivalent that
requests but does not require client certs (raw-public-key peers
present their Ed25519 key as the "client cert" in RFC 7250 mode). This
is the one design decision to make in this task: whether to switch from
`with_no_client_auth()` to a "request-but-don't-require" mode, or to
leave `with_no_client_auth()` and accept that fingerprints only flow
when the client opts to present a cert unbidden. The RFC 7250 raw-key
path (the `RawKeyCertResolver` at endpoint.rs:565595) already
advertises `only_raw_public_keys() -> true`, which is the server-side
half of RFC 7250; the client-side presentation is set by the client's
`rustls::ClientConfig`, not by the server. Read ADR-004 and OQ-12
before deciding.
### Iroh path
iroh's `Connection` exposes the peer's `NodeId` (the raw Ed25519
public key) via the connection's TLS session metadata. In iroh's model
the `NodeId` *is* the fingerprint — it's the raw-public-key identity.
Extract it and format as a `NodeId:`-prefixed string (or `SHA256:` of
the public key bytes — match whatever `AuthPolicy`'s fingerprint set
is expected to contain). Look at `iroh::endpoint::Connection` methods
and the `iroh::tls::Lts` / peer-certificate accessor for the exact API.
### Fingerprint format
`AuthPolicy::resolve_identity_from_fingerprint` (config.rs:6979) does
a literal `HashSet::contains()` check — it does not normalize. So
whatever format the extractor produces must be the same format the
operator configures in `authorized_fingerprints`. The existing
fingerprint test (auth.rs:145153) uses `"SHA256:abc123"` as a
placeholder. Pick a concrete format and document it in `auth.md` (the
spec is currently silent on the exact string format). Suggested:
`SHA256:<hex of leaf cert DER>` for X.509, `ed25519:<base64 of pub key>`
for raw keys — but confirm against any existing fingerprint producer
in the codebase before committing.
## Acceptance Criteria
- [ ] `dispatch_quinn` extracts client cert fingerprint from `HandshakeData` when present
- [ ] `dispatch_iroh` extracts peer `NodeId` (or equivalent raw-key fingerprint) when present
- [ ] `build_auth_context` receives `Some(fingerprint)` when a client cert was presented, `None` otherwise
- [ ] `AuthContext.identity` is `Some(identity)` when the fingerprint resolves via `IdentityProvider`, `None` otherwise (no regression for the no-cert case)
- [ ] Server config decision (request-but-don't-require vs. no-client-auth) is made and documented
- [ ] Fingerprint string format is chosen, documented in `auth.md`, and consistent between extractor and `AuthPolicy::authorized_fingerprints` config
- [ ] Unit test: quinn path with a presented client cert → `auth.tls_client_fingerprint` is `Some(...)`
- [ ] Unit test: quinn path with no client cert → `auth.tls_client_fingerprint` is `None` (existing behavior preserved)
- [ ] Unit test: iroh path → `auth.tls_client_fingerprint` is `Some(NodeId-format)` when peer identity is available
- [ ] `cargo test -p alknet-core --all-features` succeeds
- [ ] `cargo clippy -p alknet-core --all-features --all-targets` succeeds with no warnings
## References
- docs/reviews/004-post-implementation-sanity-check.md — W2 (full finding)
- docs/architecture/crates/core/auth.md:159171 — endpoint-level resolution flow spec
- docs/architecture/crates/core/auth.md:152 — fingerprint format used by `resolve_identity_from_fingerprint`
- docs/architecture/decisions/004-auth-as-shared-core.md — ADR-004 (hybrid resolution)
- docs/architecture/open-questions.md — OQ-12 (TLS identity provisioning)
- crates/alknet-core/src/endpoint.rs:306, 396 — the two `None` sites to fix
- crates/alknet-core/src/endpoint.rs:316326 — `extract_quinn_alpn` (pattern to follow for `HandshakeData` downcast)
- crates/alknet-core/src/endpoint.rs:565595 — `RawKeyCertResolver` (RFC 7250 server-side half)
## Notes
> If the server-config decision (request-but-don't-require client auth)
> is too large for this task's scope, split it: implement extraction
> first (this task, gated on the cert being presented *if* one arrives),
> then a follow-up task switches the server config to actually request
> client certs. The extraction code is correct either way — it returns
> `None` when no cert was presented, which is the current behavior, so
> landing extraction first is a safe no-op until the server config
> changes.
## Summary
Added `extract_quinn_client_fingerprint` (leaf cert DER → `SHA256:<hex>`
via `peer_identity()` downcast) and `extract_iroh_client_fingerprint`
(peer `NodeId``ed25519:<hex>`). Both dispatch functions now pass the
extracted fingerprint to `build_auth_context`. Fingerprint format
documented in `auth.md` (table: quinn X.509 vs iroh raw Ed25519).
Server config still uses `with_no_client_auth()` — extraction is a safe
no-op. Follow-up task `core/endpoint-request-client-cert` created for the
server config change. Two unit tests cover fingerprint format +
determinism. `cargo test -p alknet-core --all-features` (59 tests) and
clippy clean.