Decompose the source-to-spec sync for the core and call crates into atomic, dependency-ordered tasks for implementation agents: Core (7 tasks + review): - peer-entry-model: PeerEntry struct, AuthPolicy.peers (ADR-030 keystone) - credential-store-trait: CredentialStore/InMemoryCredentialStore/StoreError (ADR-031/035) - identity-store-trait: IdentityStore async write trait (ADR-035) - config-identity-provider-peerentry: ConfigIdentityProvider PeerEntry resolution (ADR-030) - fingerprint-normalization: ed25519:hex for raw keys across quinn/iroh (ADR-030 §6) - three-remote-roles-docs: document ADR-034 roles and verifier selection - review-core-sync: phase gate before call consumes new identity semantics Call (9 tasks + review): - retire-remote-safe: remove ADR-028 machinery, AccessControl is the gate (ADR-029 §3) - operation-context-forwarded-for: forwarded_for field, wire-ingress only (ADR-032) - peer-composite-env: PeerCompositeEnv, PeerId=Identity.id, remove UUID (ADR-029/030) - operation-env-invoke-peer: invoke_peer/peer_contains/PeerRef (ADR-029 §2) - services-list-accesscontrol-filtered: AccessControl filter, list-peers opt-in (ADR-029 §6) - call-client-verifier-selection: TLS client-auth, verifier by PeerEntry (OQ-29, ADR-034) - from-call-forwarded-for: populate forwarded_for, peer-keyed registration (ADR-029 §5, ADR-032) - dispatch-peer-identity: AccessControl::check(peer_identity), PeerId from resolution (ADR-029 §3, ADR-030 §5) - review-call-sync: phase gate for the call sync Validated: 58 tasks, no cycles, logical topo order, two review checkpoints.
5.6 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| core/fingerprint-normalization | Normalize quinn Ed25519 raw-key fingerprint to ed25519:hex format (ADR-030 §6) | pending |
|
narrow | medium | component | implementation |
Description
Normalize the quinn Ed25519 raw-key fingerprint extraction to produce
ed25519:<hex of 32-byte pub key>, matching the iroh path. Currently
fingerprint_from_cert_der produces SHA256:<hex of cert DER> for ALL certs,
including RFC 7250 raw public keys. ADR-030 §6 requires that Ed25519 raw keys
produce ed25519:<hex> regardless of transport (quinn or iroh), so the same
key has the same fingerprint in PeerEntry.fingerprints — one entry, both
transports.
Current state
// crates/alknet-core/src/endpoint.rs
fn extract_quinn_client_fingerprint(connection: &quinn::Connection) -> Option<String> {
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<String> {
// Always SHA256:<hex of DER> — 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<String> {
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:
-
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:<lowercase hex of 32 bytes>. This matches the iroh path — the same key has the same fingerprint regardless of transport. -
X.509 cert: keep
SHA256:<hex of cert DER>(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:<lowercase hex of 32-byte pub key> |
| quinn (RFC 7250 raw key) | SPKI cert → extract raw Ed25519 pub key | ed25519:<lowercase hex of 32-byte pub key> (normalized) |
| quinn (X.509) | leaf client cert DER | SHA256:<hex of SHA-256(cert_der)> |
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:<hex>. Otherwise, hash the full DER as SHA256:<hex>.
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:<lowercase hex of 32-byte pub key> - X.509 cert →
SHA256:<hex of SHA-256(cert_der)>(unchanged) - iroh path already produces
ed25519:<hex>(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:<hex>format - Unit test: X.509 cert →
SHA256:<hex>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-coresucceedscargo clippy -p alknet-coresucceeds 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 producesed25519:abc...on iroh andSHA256:def...on quinn, breaking the ADR-030 resolution path. The X.509 path staysSHA256:<hex of DER>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 sameed25519:<hex>whether direct or proxied).
Summary
To be filled on completion