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.
7.0 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level |
|---|---|---|---|---|---|---|---|
| core/endpoint-client-fingerprint | Extract TLS client certificate fingerprint in endpoint dispatch (ADR-004) | completed | narrow | medium | component | 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:159–171 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)→ ConstructAuthContext { 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:316–326) 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:565–595) 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:69–79) 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:145–153) 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_quinnextracts client cert fingerprint fromHandshakeDatawhen presentdispatch_irohextracts peerNodeId(or equivalent raw-key fingerprint) when presentbuild_auth_contextreceivesSome(fingerprint)when a client cert was presented,NoneotherwiseAuthContext.identityisSome(identity)when the fingerprint resolves viaIdentityProvider,Noneotherwise (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 andAuthPolicy::authorized_fingerprintsconfig - Unit test: quinn path with a presented client cert →
auth.tls_client_fingerprintisSome(...) - Unit test: quinn path with no client cert →
auth.tls_client_fingerprintisNone(existing behavior preserved) - Unit test: iroh path →
auth.tls_client_fingerprintisSome(NodeId-format)when peer identity is available cargo test -p alknet-core --all-featuressucceedscargo clippy -p alknet-core --all-features --all-targetssucceeds with no warnings
References
- docs/reviews/004-post-implementation-sanity-check.md — W2 (full finding)
- docs/architecture/crates/core/auth.md:159–171 — 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
Nonesites to fix - crates/alknet-core/src/endpoint.rs:316–326 —
extract_quinn_alpn(pattern to follow forHandshakeDatadowncast) - crates/alknet-core/src/endpoint.rs:565–595 —
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
Nonewhen 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.