--- id: core/fingerprint-normalization name: Normalize quinn Ed25519 raw-key fingerprint to ed25519:hex format (ADR-030 §6) status: completed depends_on: [core/peer-entry-model] scope: narrow risk: medium impact: component level: implementation --- ## Description Normalize the quinn Ed25519 raw-key fingerprint extraction to produce `ed25519:`, matching the iroh path. Currently `fingerprint_from_cert_der` produces `SHA256:` for ALL certs, including RFC 7250 raw public keys. ADR-030 §6 requires that Ed25519 raw keys produce `ed25519:` regardless of transport (quinn or iroh), so the same key has the same fingerprint in `PeerEntry.fingerprints` — one entry, both transports. ### Current state ```rust // crates/alknet-core/src/endpoint.rs fn extract_quinn_client_fingerprint(connection: &quinn::Connection) -> Option { let identity = connection.peer_identity()?; let cert = identity.iter().next()?; fingerprint_from_cert_der(cert.as_ref()) } fn fingerprint_from_cert_der(cert_der: &[u8]) -> Option { // Always SHA256: — wrong for Ed25519 raw keys let mut hasher = Sha256::new(); hasher.update(cert_der); Some(format!("SHA256:{}", hex::encode(hasher.finalize()))) } fn extract_iroh_client_fingerprint(connection: &iroh::endpoint::Connection) -> Option { let node_id = connection.remote_node_id().ok()?; Some(format!("ed25519:{}", node_id)) // ← already correct } ``` ### Target state (ADR-030 §6) `fingerprint_from_cert_der` (or a new `fingerprint_from_client_cert` function) must distinguish: 1. **RFC 7250 raw public key cert** (SPKI with Ed25519 algorithm identifier): extract the raw 32-byte Ed25519 public key from the SPKI DER and format as `ed25519:`. This matches the iroh path — the same key has the same fingerprint regardless of transport. 2. **X.509 cert**: keep `SHA256:` (the DER hash — X.509 certs don't have a "raw public key" form). The distinction is whether the presented cert is an RFC 7250 raw public key (SPKI with Ed25519 algorithm identifier, no X.509 wrapper) or a full X.509 cert. The `RawKeyCertResolver` on the server side already has the raw key bytes via `Ed25519SecretKey::public()`; the client-side extraction must parse the SPKI DER to extract the raw key. ### Fingerprint format table (ADR-030 §6) | Transport | Source | Format | |-----------|--------|--------| | iroh (direct or relay) | peer `NodeId` (Ed25519 public key) | `ed25519:` | | quinn (RFC 7250 raw key) | SPKI cert → extract raw Ed25519 pub key | `ed25519:` (normalized) | | quinn (X.509) | leaf client cert DER | `SHA256:` | ### Implementation approach Parse the cert DER to detect whether it's a raw public key (SPKI) or an X.509 cert. If SPKI with Ed25519 algorithm identifier, extract the 32-byte public key and format as `ed25519:`. Otherwise, hash the full DER as `SHA256:`. The `rustls-pki-types` crate (already a dependency) provides `CertificateDer`. The `rustls` crate's webpki or a manual DER parse of the SPKI's `SubjectPublicKeyInfo` → `subjectPublicKey` field can extract the raw key. A minimal DER parser for the SPKI structure (AlgorithmIdentifier + subjectPublicKey) is sufficient — the structure is small and well-defined. ### Test migration The existing endpoint.rs tests expect `SHA256:` for all fingerprints. Tests with Ed25519 raw keys must migrate to expect `ed25519:`. Tests with X.509 certs stay `SHA256:`. Add a test that the same Ed25519 key produces the same fingerprint via both the quinn SPKI-extraction path and the iroh NodeId path (if testable without a live iroh connection, test the format function directly). ## Acceptance Criteria - [ ] `fingerprint_from_cert_der` (or replacement) distinguishes RFC 7250 raw key SPKI from X.509 cert - [ ] Ed25519 raw key (SPKI) → `ed25519:` - [ ] X.509 cert → `SHA256:` (unchanged) - [ ] iroh path already produces `ed25519:` (unchanged — verify) - [ ] Same Ed25519 key produces same fingerprint via quinn and iroh paths - [ ] No-client-cert case still produces `tls_client_fingerprint: None` (no regression) - [ ] Unit test: Ed25519 raw key SPKI → `ed25519:` format - [ ] Unit test: X.509 cert → `SHA256:` format (unchanged) - [ ] Unit test: fingerprint is lowercase hex - [ ] Unit test: 32-byte pub key extracted correctly (not the DER wrapper) - [ ] Existing endpoint.rs fingerprint tests migrated (Ed25519 → `ed25519:`, X.509 → `SHA256:`) - [ ] `cargo test -p alknet-core` succeeds - [ ] `cargo clippy -p alknet-core` succeeds with no warnings ## References - docs/architecture/crates/core/auth.md — Fingerprint string format table - docs/architecture/decisions/030-peerentry-and-identity-id-decoupling.md — ADR-030 §6 (normalization rationale) - docs/architecture/decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md — ADR-027 (RawKey model) ## Notes > The normalization is load-bearing for the peer graph: a peer that connects > via quinn direct and via iroh must have the same fingerprint in > `PeerEntry.fingerprints` — one entry, both transports. Without this, the same > key produces `ed25519:abc...` on iroh and `SHA256:def...` on quinn, breaking > the ADR-030 resolution path. The X.509 path stays `SHA256:` > because X.509 certs don't have a "raw public key" form. This also simplifies > the coming WebTransport relay work (proxied Ed25519 identity is the same > `ed25519:` whether direct or proxied). ## Summary Implemented DER parser for SPKI to extract raw 32-byte Ed25519 public key from RFC 7250 raw public key certs; fingerprint_from_cert_der now produces ed25519: for Ed25519 SPKI and keeps SHA256: for X.509 certs. Same Ed25519 key now produces identical fingerprint via quinn SPKI path and iroh NodeId path. Added 5 unit tests covering Ed25519 SPKI format, lowercase hex, 32-byte key extraction, cross-transport parity, X.509 fallback, and non-Ed25519 SPKI fallback. 135 tests pass with default features, clippy clean, fmt clean.