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

7.0 KiB
Raw Blame History

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: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 NodeIded25519:<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.