tasks: decompose ADR-029/030/031/032/034/035 source sync into 17 tasks
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.
This commit is contained in:
114
tasks/core/config-identity-provider-peerentry.md
Normal file
114
tasks/core/config-identity-provider-peerentry.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
id: core/config-identity-provider-peerentry
|
||||
name: Rewrite ConfigIdentityProvider resolution to use PeerEntry multi-credential path (ADR-030)
|
||||
status: pending
|
||||
depends_on: [core/peer-entry-model]
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Rewrite `ConfigIdentityProvider::resolve_from_fingerprint` and
|
||||
`resolve_from_token` to use the new `PeerEntry`-based resolution from
|
||||
`core/peer-entry-model`. This is the resolution-logic half of the ADR-030
|
||||
change — the data model (PeerEntry struct, AuthPolicy.peers) lands in
|
||||
`core/peer-entry-model`; this task updates the `ConfigIdentityProvider` methods
|
||||
that delegate to `AuthPolicy`.
|
||||
|
||||
### Current state (pre-ADR-030)
|
||||
|
||||
```rust
|
||||
impl IdentityProvider for ConfigIdentityProvider {
|
||||
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity> {
|
||||
let config = self.dynamic.load();
|
||||
config.auth.resolve_identity_from_fingerprint(fingerprint)
|
||||
}
|
||||
|
||||
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity> {
|
||||
let config = self.dynamic.load();
|
||||
let token_str = String::from_utf8_lossy(&token.raw);
|
||||
config.auth.resolve_api_key(&token_str) // ← only ApiKeyEntry path
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Target state (ADR-030)
|
||||
|
||||
```rust
|
||||
impl IdentityProvider for ConfigIdentityProvider {
|
||||
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity> {
|
||||
let config = self.dynamic.load();
|
||||
config.auth.resolve_identity_from_fingerprint(fingerprint)
|
||||
// Now resolves: fingerprint → PeerEntry → Identity { id: peer_id, ... }
|
||||
}
|
||||
|
||||
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity> {
|
||||
let config = self.dynamic.load();
|
||||
let token_str = String::from_utf8_lossy(&token.raw);
|
||||
config.auth.resolve_identity_from_token(&token_str)
|
||||
// Now tries PeerEntry.auth_token_hash first, falls through to ApiKeyEntry
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The key change in `resolve_from_token`: it now calls
|
||||
`AuthPolicy::resolve_identity_from_token` (added in `core/peer-entry-model`),
|
||||
which tries the `PeerEntry.auth_token_hash` path first (token is one credential
|
||||
path among several for a stable logical peer → `Identity.id = peer_id`), then
|
||||
falls through to `resolve_api_key` (token IS the identity → `Identity.id =
|
||||
prefix`).
|
||||
|
||||
### ConfigIdentityProvider stays read-only
|
||||
|
||||
`ConfigIdentityProvider` still reads from `ArcSwap<DynamicConfig>` on every call
|
||||
(hot-reloadable). It does NOT implement `IdentityStore` (that's
|
||||
`core/identity-store-trait` — config reload is its write path, not a method
|
||||
call).
|
||||
|
||||
### Test migration
|
||||
|
||||
The existing auth.rs tests use `authorized_fingerprints: HashSet<String>` and
|
||||
expect `Identity.id == fingerprint`. These must migrate to the `PeerEntry` model:
|
||||
- `config_with_fingerprint` helper → `config_with_peer_entry` helper
|
||||
- Fingerprint resolution tests expect `Identity.id == peer_id` (not the fingerprint)
|
||||
- Add token-resolution-via-PeerEntry tests (auth_token_hash path)
|
||||
- Config reload tests use `PeerEntry` in the new config
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ConfigIdentityProvider::resolve_from_fingerprint` delegates to `AuthPolicy::resolve_identity_from_fingerprint` (PeerEntry path)
|
||||
- [ ] `ConfigIdentityProvider::resolve_from_token` delegates to `AuthPolicy::resolve_identity_from_token` (PeerEntry.auth_token_hash → fall through to ApiKeyEntry)
|
||||
- [ ] `ConfigIdentityProvider` reads from ArcSwap on every call (hot-reloadable — unchanged)
|
||||
- [ ] `ConfigIdentityProvider` does NOT implement `IdentityStore`
|
||||
- [ ] Fingerprint resolution returns `Identity { id: peer_id, ... }` (stable, not the fingerprint)
|
||||
- [ ] Token resolution: PeerEntry.auth_token_hash match → `Identity { id: peer_id }`; no match → ApiKeyEntry fall-through → `Identity { id: prefix }`
|
||||
- [ ] All existing auth.rs tests migrated to PeerEntry model (no `authorized_fingerprints` references)
|
||||
- [ ] Unit test: fingerprint resolution via PeerEntry (known → Some with peer_id, unknown → None)
|
||||
- [ ] Unit test: token resolution via PeerEntry.auth_token_hash (matching → Some with peer_id)
|
||||
- [ ] Unit test: token resolution falls through to ApiKeyEntry when no PeerEntry matches
|
||||
- [ ] Unit test: config reload changes resolution results immediately (PeerEntry model)
|
||||
- [ ] Unit test: disabled PeerEntry returns None
|
||||
- [ ] `cargo test -p alknet-core` succeeds
|
||||
- [ ] `cargo clippy -p alknet-core` succeeds with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/crates/core/auth.md — ConfigIdentityProvider, multi-credential resolution
|
||||
- docs/architecture/crates/core/config.md — AuthPolicy.peers, PeerEntry
|
||||
- docs/architecture/decisions/030-peerentry-and-identity-id-decoupling.md — ADR-030 §2 (resolution semantics)
|
||||
|
||||
## Notes
|
||||
|
||||
> This is the resolution-logic half of the ADR-030 change. The data model lands
|
||||
> in `core/peer-entry-model`; this task wires `ConfigIdentityProvider` to the
|
||||
> new `AuthPolicy` methods. The semantic shift: `Identity.id` changes from the
|
||||
> fingerprint to the `peer_id` on the fingerprint path. The token path gains a
|
||||
> new first-try (PeerEntry.auth_token_hash) before the existing ApiKeyEntry
|
||||
> fall-through. ConfigIdentityProvider stays read-only and ArcSwap-backed.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
144
tasks/core/credential-store-trait.md
Normal file
144
tasks/core/credential-store-trait.md
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
id: core/credential-store-trait
|
||||
name: Add CredentialStore trait, InMemoryCredentialStore, EncryptedData mirror, and StoreError (ADR-031/035)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Add the second repo trait to alknet-core: `CredentialStore` for encrypted-
|
||||
credential persistence, alongside its in-memory default adapter and the shared
|
||||
`StoreError` type. Per ADR-031 (the trait) and ADR-035 (refines `put`/`delete`
|
||||
to async, renames the error to `StoreError`).
|
||||
|
||||
This task is standalone — it has no dependency on `core/peer-entry-model`. The
|
||||
`CredentialStore` trait persists `EncryptedData` blobs (the vault's encrypted
|
||||
output); it never decrypts (ADR-025 — the vault is the sole decryption
|
||||
boundary).
|
||||
|
||||
### CredentialStore trait
|
||||
|
||||
```rust
|
||||
pub trait CredentialStore: Send + Sync {
|
||||
fn get(&self, provider: &str) -> Option<EncryptedData>;
|
||||
async fn put(&self, provider: &str, data: &EncryptedData) -> Result<(), StoreError>;
|
||||
async fn delete(&self, provider: &str) -> Result<(), StoreError>;
|
||||
}
|
||||
```
|
||||
|
||||
- `get` is **sync** (cached read — the hot path; ADR-035 §1).
|
||||
- `put`/`delete` are **async** (they hit the backend; ADR-035 §3 refines
|
||||
ADR-031's sync sketch to async within the one-way door).
|
||||
- `get` returns `Option<EncryptedData>` (missing credential is the common case,
|
||||
not an error).
|
||||
- No `list` method (ADR-031 §4 — additive if needed later).
|
||||
|
||||
### InMemoryCredentialStore
|
||||
|
||||
```rust
|
||||
pub struct InMemoryCredentialStore {
|
||||
entries: RwLock<HashMap<String, EncryptedData>>,
|
||||
}
|
||||
|
||||
impl InMemoryCredentialStore {
|
||||
pub fn new() -> Self;
|
||||
pub fn with_entries(entries: HashMap<String, EncryptedData>) -> Self;
|
||||
}
|
||||
|
||||
impl CredentialStore for InMemoryCredentialStore { ... }
|
||||
```
|
||||
|
||||
The default adapter covers tests and config-loaded deployments. `put`/`delete`
|
||||
are async with no `.await` points (trivially satisfy an async trait — ADR-035
|
||||
§3). Same posture as `ConfigIdentityProvider` — no persistence, no backend
|
||||
dependency, no env vars.
|
||||
|
||||
### EncryptedData core mirror
|
||||
|
||||
A thin serializable struct mirroring the vault's `EncryptedData` (ADR-020),
|
||||
so the trait can reference it without a vault dependency (ADR-018 — vault is
|
||||
standalone with zero alknet-crate dependencies):
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EncryptedData {
|
||||
pub key_version: u32,
|
||||
pub salt: Vec<u8>, // wire-format compat (OQ-20); unused in v2 but kept
|
||||
pub iv: Vec<u8>, // AES-GCM IV (OsRng-generated)
|
||||
pub data: Vec<u8>, // ciphertext
|
||||
}
|
||||
```
|
||||
|
||||
The `salt` field is kept for wire-format compatibility with the TS predecessor
|
||||
(OQ-20) — a core mirror that omitted it could not round-trip the vault's
|
||||
`EncryptedData`. v2 may write a zero-length salt but must not drop the field
|
||||
(ADR-035 §6).
|
||||
|
||||
### StoreError
|
||||
|
||||
```rust
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StoreError {
|
||||
#[error("backend error: {message}")]
|
||||
Backend { message: String },
|
||||
#[error("not found: {entity}")]
|
||||
NotFound { entity: String },
|
||||
#[error("serialization error: {message}")]
|
||||
Serialization { message: String },
|
||||
}
|
||||
```
|
||||
|
||||
Shared by both `CredentialStore` and `IdentityStore` (ADR-035 §7 renames
|
||||
ADR-031's `CredentialStoreError` to `StoreError`). `#[non_exhaustive]` so
|
||||
adapter crates can extend without breaking match arms. Lives in alknet-core
|
||||
(where the traits live).
|
||||
|
||||
### Module placement
|
||||
|
||||
Add a new `store` module (or `credential_store` module) in `alknet-core/src/`.
|
||||
Re-export `CredentialStore`, `InMemoryCredentialStore`, `EncryptedData`, and
|
||||
`StoreError` from `lib.rs`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `CredentialStore` trait with sync `get`, async `put`/`delete`
|
||||
- [ ] `InMemoryCredentialStore` with `new()` and `with_entries()`
|
||||
- [ ] `InMemoryCredentialStore` implements `CredentialStore` (async put/delete with no .await points)
|
||||
- [ ] `EncryptedData` core mirror with 4 fields (`key_version`, `salt`, `iv`, `data`), derives Serialize/Deserialize/Clone/Debug
|
||||
- [ ] `StoreError` enum with 3 variants, `#[non_exhaustive]`, `thiserror::Error`
|
||||
- [ ] No vault dependency added to alknet-core (EncryptedData is a core-owned mirror)
|
||||
- [ ] No `list` method on the trait
|
||||
- [ ] Unit test: InMemoryCredentialStore get/put/delete round-trip
|
||||
- [ ] Unit test: get returns None for missing provider
|
||||
- [ ] Unit test: EncryptedData serializes and deserializes (round-trip)
|
||||
- [ ] Unit test: StoreError Display formatting
|
||||
- [ ] `cargo test -p alknet-core` succeeds
|
||||
- [ ] `cargo clippy -p alknet-core` succeeds with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/crates/core/auth.md — CredentialStore, StoreError, EncryptedData mirror
|
||||
- docs/architecture/decisions/031-credentialstore-repo-trait.md — ADR-031 (the trait)
|
||||
- docs/architecture/decisions/035-concrete-persistence-adapter-shapes.md — ADR-035 (async put/delete, StoreError rename, schema)
|
||||
- docs/architecture/decisions/033-storage-boundary-and-repo-adapter-pattern.md — ADR-033 (the pattern)
|
||||
|
||||
## Notes
|
||||
|
||||
> Standalone task — no dependency on PeerEntry. The `CredentialStore` trait is
|
||||
> the second repo trait in core (alongside `IdentityProvider`), establishing
|
||||
> the repo/adapter pattern concretely (ADR-033). The trait is the one-way door;
|
||||
> the in-memory default is the reference implementation; persistence adapters
|
||||
> (alknet-store-sqlite, ADR-035) are separate crates, not built in this sync.
|
||||
> `get` stays sync because the credential load happens at startup into
|
||||
> `Capabilities` (ADR-031); `put`/`delete` are async because a SQLite-backed
|
||||
> adapter cannot do a sync write without blocking (ADR-035 §3).
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
127
tasks/core/fingerprint-normalization.md
Normal file
127
tasks/core/fingerprint-normalization.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
id: core/fingerprint-normalization
|
||||
name: Normalize quinn Ed25519 raw-key fingerprint to ed25519:hex format (ADR-030 §6)
|
||||
status: pending
|
||||
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:<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
|
||||
|
||||
```rust
|
||||
// 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:
|
||||
|
||||
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:<lowercase hex of 32 bytes>`. This matches the iroh path — the same
|
||||
key has the same fingerprint regardless of transport.
|
||||
|
||||
2. **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-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:<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 same
|
||||
> `ed25519:<hex>` whether direct or proxied).
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
98
tasks/core/identity-store-trait.md
Normal file
98
tasks/core/identity-store-trait.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
id: core/identity-store-trait
|
||||
name: Add IdentityStore async write trait extending IdentityProvider (ADR-035)
|
||||
status: pending
|
||||
depends_on: [core/peer-entry-model]
|
||||
scope: single
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Add the `IdentityStore` async write trait for peer management, extending the
|
||||
read-only `IdentityProvider` trait. Per ADR-035 §2.
|
||||
|
||||
`IdentityProvider` is read-only today and stays read-only — it is the hot-path
|
||||
trait called on every incoming connection (sync, no `.await`). Peer mutations
|
||||
(add/update/remove a `PeerEntry`) go through this separate async trait.
|
||||
|
||||
### IdentityStore trait
|
||||
|
||||
```rust
|
||||
/// Write trait — management path, async (ADR-035). ConfigIdentityProvider
|
||||
/// does NOT implement this (config reload is its write path — see below).
|
||||
/// SqliteIdentityProvider does: writes hit SQLite, emit honker NOTIFY,
|
||||
/// and the local LISTEN refreshes the in-memory read index.
|
||||
#[async_trait]
|
||||
pub trait IdentityStore: IdentityProvider {
|
||||
async fn put_peer(&self, peer: &PeerEntry) -> Result<(), StoreError>;
|
||||
async fn update_peer(&self, peer_id: &str, peer: &PeerEntry) -> Result<(), StoreError>;
|
||||
async fn remove_peer(&self, peer_id: &str) -> Result<(), StoreError>;
|
||||
}
|
||||
```
|
||||
|
||||
- `put_peer` — insert or replace a `PeerEntry` (upsert by `peer_id`).
|
||||
- `update_peer` — update an existing `PeerEntry` (error if `peer_id` not found;
|
||||
for upsert semantics use `put_peer`).
|
||||
- `remove_peer` — delete a `PeerEntry` by `peer_id`.
|
||||
|
||||
### Why a separate trait, not async methods on IdentityProvider
|
||||
|
||||
- The hot-path read trait is consumed by the accept loop and every handler —
|
||||
those call sites are sync and must not gain `.await`. If `put_peer` were on
|
||||
`IdentityProvider`, every consumer would see the async method even though
|
||||
only the management path calls it. A separate `IdentityStore: IdentityProvider`
|
||||
supertrait keeps the read surface lean and makes the write surface opt-in.
|
||||
- `ConfigIdentityProvider` does **not** implement `IdentityStore`. Its write
|
||||
path is config reload (`ConfigReloadHandle::reload`), not a method call. This
|
||||
preserves the config-is-source-of-truth model. Implementing `IdentityStore`
|
||||
> for `ConfigIdentityProvider` "for symmetry" would violate that model — the
|
||||
> constraint is the absence of a backend, not a type-system constraint.
|
||||
|
||||
### ConfigIdentityProvider posture
|
||||
|
||||
`ConfigIdentityProvider` deliberately does NOT implement `IdentityStore`. This
|
||||
task does not change `ConfigIdentityProvider` — it only adds the trait. The
|
||||
trait is defined for future adapters (`SqliteIdentityProvider` in
|
||||
`alknet-store-sqlite`) to implement. `StoreError` is already defined by
|
||||
`core/credential-store-trait`.
|
||||
|
||||
### Module placement
|
||||
|
||||
Add `IdentityStore` alongside `IdentityProvider` in `alknet-core/src/auth.rs`
|
||||
(or a new `store` module if `CredentialStore` landed there). Re-export from
|
||||
`lib.rs`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `IdentityStore` trait with `put_peer`, `update_peer`, `remove_peer` (all async)
|
||||
- [ ] `IdentityStore: IdentityProvider` (supertrait)
|
||||
- [ ] `StoreError` used as the error type (from `core/credential-store-trait`)
|
||||
- [ ] `ConfigIdentityProvider` does NOT implement `IdentityStore`
|
||||
- [ ] `#[async_trait]` on the trait
|
||||
- [ ] No changes to `IdentityProvider` trait (stays read-only, sync)
|
||||
- [ ] Unit test: a mock/test impl of `IdentityStore` compiles and works (verify the trait is implementable)
|
||||
- [ ] Unit test: `ConfigIdentityProvider` does not implement `IdentityStore` (compile-time or trait-bound assertion)
|
||||
- [ ] `cargo test -p alknet-core` succeeds
|
||||
- [ ] `cargo clippy -p alknet-core` succeeds with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/crates/core/auth.md — IdentityStore write trait, ConfigIdentityProvider posture
|
||||
- docs/architecture/decisions/035-concrete-persistence-adapter-shapes.md — ADR-035 §2 (the trait, read/write split rationale)
|
||||
- docs/architecture/decisions/033-storage-boundary-and-repo-adapter-pattern.md — ADR-033 (the pattern)
|
||||
|
||||
## Notes
|
||||
|
||||
> Small task but locks the trait shape — a one-way door. The read/write split
|
||||
> keeps the hot path sync (no `.await` in the accept loop). `ConfigIdentityProvider`
|
||||
> not implementing `IdentityStore` is a design posture, not a type-system
|
||||
> constraint: it holds no backend, and its write path is config reload. A
|
||||
> deployment that wants method-call peer management wires the SQLite adapter
|
||||
> (a separate crate, not built in this sync).
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
175
tasks/core/peer-entry-model.md
Normal file
175
tasks/core/peer-entry-model.md
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
id: core/peer-entry-model
|
||||
name: Add PeerEntry struct and replace AuthPolicy.authorized_fingerprints with peers (ADR-030)
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Replace `AuthPolicy.authorized_fingerprints: HashSet<String>` with
|
||||
`AuthPolicy.peers: Vec<PeerEntry>`, per ADR-030. This is the foundational data
|
||||
change for the entire ADR-029/030 sync — every downstream task (core resolution
|
||||
logic, IdentityStore, call peer-keyed routing, fingerprint normalization) depends
|
||||
on this struct and the `AuthPolicy.peers` field.
|
||||
|
||||
This task adds the `PeerEntry` struct and the `AuthPolicy.peers` field, and
|
||||
migrates the `AuthPolicy` resolution methods to the new model. The
|
||||
`ConfigIdentityProvider` rewrite (the resolution-logic half) is a separate task
|
||||
(`core/config-identity-provider-peerentry`) so this task stays focused on the
|
||||
data model + `AuthPolicy` resolution methods.
|
||||
|
||||
### PeerEntry struct
|
||||
|
||||
```rust
|
||||
pub struct PeerEntry {
|
||||
/// Stable logical peer id ("worker-a", "alice"). Does NOT change on
|
||||
/// key rotation. This becomes Identity.id on resolution, regardless of
|
||||
/// which credential path resolved the identity.
|
||||
pub peer_id: String,
|
||||
|
||||
/// TLS fingerprints for this peer — one or more. A peer may have
|
||||
/// multiple keys (e.g., an Ed25519 raw key for P2P and an X.509 cert
|
||||
/// for domain-facing). Resolution matches against any entry.
|
||||
/// Format: "ed25519:<hex of 32-byte pub key>" for RFC 7250 raw keys
|
||||
/// (normalized across quinn and iroh — ADR-030 §6), "SHA256:<hex>" for
|
||||
/// X.509 certs (DER hash). Changes on key rotation.
|
||||
pub fingerprints: Vec<String>,
|
||||
|
||||
/// Optional: bearer-token authentication for this peer. A peer that
|
||||
/// also authenticates via auth_token (e.g., HTTP clients that can't
|
||||
/// do TLS client-auth) stores the SHA-256 hash of the token here.
|
||||
/// Resolution via resolve_from_token matches this field and returns
|
||||
/// the same Identity { id: peer_id, ... } as the fingerprint path.
|
||||
pub auth_token_hash: Option<String>,
|
||||
|
||||
/// Authorization scopes granted to this peer. Resolved into
|
||||
/// Identity.scopes.
|
||||
pub scopes: Vec<String>,
|
||||
|
||||
/// Named resource lists granted to this peer. Resolved into
|
||||
/// Identity.resources.
|
||||
pub resources: HashMap<String, Vec<String>>,
|
||||
|
||||
/// Human-readable display name for logs / UIs. Optional.
|
||||
pub display_name: Option<String>,
|
||||
|
||||
/// Whether this peer is authorized at all. false = recognized but
|
||||
/// disabled (revoked). Resolution returns None.
|
||||
pub enabled: bool,
|
||||
}
|
||||
```
|
||||
|
||||
### AuthPolicy change
|
||||
|
||||
```rust
|
||||
pub struct AuthPolicy {
|
||||
/// Replaces authorized_fingerprints: HashSet<String>. Each entry maps
|
||||
/// a stable logical peer_id to its credential paths (fingerprints,
|
||||
/// optional auth_token_hash) + scopes + resources. The list is keyed
|
||||
/// by peer_id; resolution looks up by fingerprint OR auth_token.
|
||||
pub peers: Vec<PeerEntry>,
|
||||
|
||||
/// API keys for bearer-token auth where the token IS the identity
|
||||
/// (rotation = new identity). Unchanged by ADR-030.
|
||||
pub api_keys: Vec<ApiKeyEntry>,
|
||||
}
|
||||
```
|
||||
|
||||
### AuthPolicy resolution methods (new model)
|
||||
|
||||
`AuthPolicy::resolve_identity_from_fingerprint` and a new
|
||||
`resolve_identity_from_token` method resolve via `PeerEntry`:
|
||||
|
||||
```rust
|
||||
impl AuthPolicy {
|
||||
pub fn resolve_identity_from_fingerprint(&self, fingerprint: &str) -> Option<Identity> {
|
||||
self.peers.iter()
|
||||
.find(|p| p.enabled && p.fingerprints.iter().any(|f| f == fingerprint))
|
||||
.map(|p| Identity {
|
||||
id: p.peer_id.clone(),
|
||||
scopes: p.scopes.clone(),
|
||||
resources: p.resources.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resolve_identity_from_token(&self, token: &str) -> Option<Identity> {
|
||||
let token_hash = sha256(token);
|
||||
self.peers.iter()
|
||||
.find(|p| p.enabled && p.auth_token_hash.as_deref() == Some(&token_hash))
|
||||
.map(|p| Identity {
|
||||
id: p.peer_id.clone(),
|
||||
scopes: p.scopes.clone(),
|
||||
resources: p.resources.clone(),
|
||||
})
|
||||
.or_else(|| self.resolve_api_key(token)) // fall through to ApiKeyEntry
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The key change: `Identity.id` is now the stable `peer_id`, **not** the
|
||||
fingerprint. Key rotation changes the fingerprint but not the `peer_id`, so ACL
|
||||
entries and routing references stay stable (ADR-030 §2-3).
|
||||
|
||||
`resolve_api_key` stays unchanged (the `ApiKeyEntry` path where the token IS the
|
||||
identity — `Identity.id = prefix`).
|
||||
|
||||
### Config validation
|
||||
|
||||
`PeerEntry.peer_id` is operator-chosen and unique within a config. Add a
|
||||
validation method or assertion that duplicate `peer_id` values in
|
||||
`AuthPolicy.peers` are a config error (ADR-030 Assumption 2).
|
||||
|
||||
### What this task does NOT do
|
||||
|
||||
- Does NOT rewrite `ConfigIdentityProvider` — that's
|
||||
`core/config-identity-provider-peerentry` (the `ConfigIdentityProvider` methods
|
||||
delegate to `AuthPolicy` resolution, so they keep working once `AuthPolicy`
|
||||
is updated, but the token-resolution path in `ConfigIdentityProvider` needs to
|
||||
call the new `resolve_identity_from_token` instead of only `resolve_api_key`).
|
||||
- Does NOT normalize quinn fingerprints to `ed25519:<hex>` — that's
|
||||
`core/fingerprint-normalization`.
|
||||
- Does NOT add `IdentityStore` or `CredentialStore` — those are separate tasks.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `PeerEntry` struct with all 7 fields (`peer_id`, `fingerprints`, `auth_token_hash`, `scopes`, `resources`, `display_name`, `enabled`)
|
||||
- [ ] `AuthPolicy.authorized_fingerprints` removed; replaced with `peers: Vec<PeerEntry>`
|
||||
- [ ] `AuthPolicy.api_keys` unchanged
|
||||
- [ ] `AuthPolicy::resolve_identity_from_fingerprint` resolves fingerprint → PeerEntry → `Identity { id: peer_id, ... }`
|
||||
- [ ] `AuthPolicy::resolve_identity_from_token` resolves token hash → PeerEntry → `Identity { id: peer_id, ... }`, falls through to `resolve_api_key`
|
||||
- [ ] `Identity.id` is the `peer_id` (stable), not the fingerprint
|
||||
- [ ] Disabled peers (`enabled: false`) return `None` from resolution
|
||||
- [ ] Duplicate `peer_id` validation (config error)
|
||||
- [ ] Unit test: fingerprint resolution via PeerEntry (known → Some with peer_id, unknown → None, disabled → None)
|
||||
- [ ] Unit test: token resolution via PeerEntry.auth_token_hash (matching → Some with peer_id, non-matching → fall through to ApiKeyEntry)
|
||||
- [ ] Unit test: multi-fingerprint PeerEntry (any fingerprint in the list resolves to the same peer_id)
|
||||
- [ ] Unit test: resources populated from PeerEntry.resources on both paths
|
||||
- [ ] Unit test: duplicate peer_id detected/rejected
|
||||
- [ ] `cargo test -p alknet-core` succeeds
|
||||
- [ ] `cargo clippy -p alknet-core` succeeds with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/crates/core/config.md — PeerEntry, AuthPolicy.peers
|
||||
- docs/architecture/crates/core/auth.md — Identity.id = peer_id, multi-credential resolution
|
||||
- docs/architecture/decisions/030-peerentry-and-identity-id-decoupling.md — ADR-030
|
||||
|
||||
## Notes
|
||||
|
||||
> This is the foundational data change for the ADR-029/030 sync. The key
|
||||
> semantic shift: `Identity.id` changes from the fingerprint (crypto material)
|
||||
> to the `peer_id` (stable logical id). Key rotation changes the fingerprint
|
||||
> but not the `peer_id`, so ACL entries and `PeerRef::Specific(peer_id)`
|
||||
> references stay stable. `ConfigIdentityProvider` keeps working (it delegates
|
||||
> to `AuthPolicy`), but the token path needs the new
|
||||
> `resolve_identity_from_token` — that's the separate
|
||||
> `core/config-identity-provider-peerentry` task.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
123
tasks/core/review-core-sync.md
Normal file
123
tasks/core/review-core-sync.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
id: core/review-core-sync
|
||||
name: Review alknet-core ADR-029/030/031/034/035 sync for spec conformance
|
||||
status: pending
|
||||
depends_on: [core/credential-store-trait, core/identity-store-trait, core/config-identity-provider-peerentry, core/fingerprint-normalization, core/three-remote-roles-docs]
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: phase
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review the alknet-core implementation after the ADR-029/030/031/034/035 sync
|
||||
for spec conformance, pattern consistency, and correctness. This is the quality
|
||||
checkpoint at the end of the core phase — before alknet-call (which depends on
|
||||
the new `Identity.id = peer_id` semantics) begins its sync.
|
||||
|
||||
### Review Checklist
|
||||
|
||||
1. **PeerEntry / AuthPolicy conformance** (config.md, auth.md, ADR-030):
|
||||
- `PeerEntry` has all 7 fields (peer_id, fingerprints, auth_token_hash, scopes, resources, display_name, enabled)
|
||||
- `AuthPolicy.authorized_fingerprints` removed; `peers: Vec<PeerEntry>` in place
|
||||
- `AuthPolicy.api_keys` unchanged
|
||||
- `resolve_identity_from_fingerprint` resolves fingerprint → PeerEntry → `Identity { id: peer_id }`
|
||||
- `resolve_identity_from_token` resolves auth_token_hash → PeerEntry → falls through to ApiKeyEntry
|
||||
- `Identity.id` is the stable `peer_id`, not the fingerprint
|
||||
- Disabled peers (`enabled: false`) return None
|
||||
- Duplicate `peer_id` validation
|
||||
|
||||
2. **ConfigIdentityProvider conformance** (auth.md, ADR-030):
|
||||
- `resolve_from_fingerprint` delegates to `AuthPolicy::resolve_identity_from_fingerprint`
|
||||
- `resolve_from_token` delegates to `AuthPolicy::resolve_identity_from_token` (PeerEntry first, ApiKeyEntry fall-through)
|
||||
- Reads from ArcSwap on every call (hot-reloadable — unchanged)
|
||||
- Does NOT implement `IdentityStore`
|
||||
|
||||
3. **CredentialStore conformance** (auth.md, ADR-031/035):
|
||||
- `CredentialStore` trait with sync `get`, async `put`/`delete`
|
||||
- `InMemoryCredentialStore` default adapter (async put/delete with no .await points)
|
||||
- `EncryptedData` core mirror (4 fields, serializable, no vault dep)
|
||||
- `StoreError` enum (`#[non_exhaustive]`, thiserror, 3 variants)
|
||||
- No `list` method
|
||||
- No vault dependency added to core
|
||||
|
||||
4. **IdentityStore conformance** (auth.md, ADR-035):
|
||||
- `IdentityStore: IdentityProvider` supertrait
|
||||
- `put_peer`/`update_peer`/`remove_peer` all async
|
||||
- `ConfigIdentityProvider` does NOT implement it
|
||||
- `IdentityProvider` trait unchanged (read-only, sync)
|
||||
|
||||
5. **Fingerprint normalization conformance** (auth.md, ADR-030 §6):
|
||||
- Ed25519 raw key (SPKI) → `ed25519:<lowercase hex of 32 bytes>`
|
||||
- X.509 cert → `SHA256:<hex of DER>` (unchanged)
|
||||
- iroh path → `ed25519:<hex>` (unchanged)
|
||||
- Same key, same fingerprint across quinn and iroh
|
||||
- No-client-cert → None (no regression)
|
||||
|
||||
6. **Three remote roles documentation** (ADR-034):
|
||||
- `auth.rs` comments document the three roles and verifier selection rule
|
||||
- `endpoint.rs` comments clarify server-side vs client-side verifier concerns
|
||||
|
||||
7. **Pattern consistency**:
|
||||
- ArcSwap used consistently for DynamicConfig (unchanged)
|
||||
- Repo/adapter pattern consistent (trait + in-memory default, no backend dep in core)
|
||||
- No russh dependency in core (unchanged)
|
||||
- Feature flags (quinn, iroh) gate transport code correctly
|
||||
|
||||
8. **Security constraints**:
|
||||
- `PeerEntry.enabled: false` → resolution returns None (revoked peers)
|
||||
- `StoreError` is `#[non_exhaustive]`
|
||||
- `EncryptedData` carries no plaintext (encrypted blob only)
|
||||
- No env vars in the credential path (ADR-014 invariant preserved)
|
||||
|
||||
9. **Test coverage**:
|
||||
- PeerEntry resolution (fingerprint, auth_token_hash, ApiKeyEntry fall-through)
|
||||
- Multi-fingerprint PeerEntry
|
||||
- Disabled peer → None
|
||||
- Duplicate peer_id validation
|
||||
- CredentialStore get/put/delete round-trip
|
||||
- EncryptedData serialization round-trip
|
||||
- Fingerprint normalization (Ed25519 → ed25519:, X.509 → SHA256:)
|
||||
- Config reload with PeerEntry model
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All PeerEntry / AuthPolicy types match config.md and auth.md
|
||||
- [ ] ConfigIdentityProvider resolution matches auth.md (PeerEntry multi-credential path)
|
||||
- [ ] CredentialStore trait + InMemoryCredentialStore + EncryptedData + StoreError match ADR-031/035
|
||||
- [ ] IdentityStore trait matches ADR-035 (read/write split, ConfigIdentityProvider posture)
|
||||
- [ ] Fingerprint normalization matches ADR-030 §6 (ed25519: for raw keys, SHA256: for X.509)
|
||||
- [ ] Three remote roles documented in source comments (ADR-034)
|
||||
- [ ] No `authorized_fingerprints` references remain
|
||||
- [ ] No `remote_safe`/`trusted_peer` references in core (those are call-side)
|
||||
- [ ] ArcSwap pattern consistent
|
||||
- [ ] No russh dependency, no vault dependency in core
|
||||
- [ ] Test coverage adequate for all new functionality
|
||||
- [ ] `cargo fmt --check -p alknet-core` passes
|
||||
- [ ] `cargo clippy -p alknet-core` passes with no warnings
|
||||
- [ ] All tests pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/crates/core/README.md
|
||||
- docs/architecture/crates/core/auth.md
|
||||
- docs/architecture/crates/core/config.md
|
||||
- docs/architecture/decisions/030-peerentry-and-identity-id-decoupling.md
|
||||
- docs/architecture/decisions/031-credentialstore-repo-trait.md
|
||||
- docs/architecture/decisions/033-storage-boundary-and-repo-adapter-pattern.md
|
||||
- docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md
|
||||
- docs/architecture/decisions/035-concrete-persistence-adapter-shapes.md
|
||||
|
||||
## Notes
|
||||
|
||||
> This review verifies core is spec-conformant after the ADR-029/030/031/034/035
|
||||
> sync before alknet-call begins its sync. alknet-call depends heavily on the new
|
||||
> `Identity.id = peer_id` semantics (PeerCompositeEnv keys, PeerRef::Specific
|
||||
> routing, AccessControl-based peer authorization) — any issues here propagate
|
||||
> to call. If deviations are found, document and fix before proceeding to the
|
||||
> call phase.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
85
tasks/core/three-remote-roles-docs.md
Normal file
85
tasks/core/three-remote-roles-docs.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
id: core/three-remote-roles-docs
|
||||
name: Document the three remote roles and client-side verifier selection rule (ADR-034)
|
||||
status: pending
|
||||
depends_on: [core/peer-entry-model]
|
||||
scope: single
|
||||
risk: trivial
|
||||
impact: isolated
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Update the in-code comments and doc comments in `alknet-core/src/auth.rs` and
|
||||
`alknet-core/src/endpoint.rs` to document the three remote roles (ADR-034) and
|
||||
the client-side verifier selection rule. This is a documentation/comment task —
|
||||
the server-side endpoint code is unchanged; the client-side verifier selection
|
||||
is a call-side task (`call/call-client-verifier-selection`).
|
||||
|
||||
### Three remote roles (ADR-034 §1)
|
||||
|
||||
| Role | Identity | alknet peer? | `PeerEntry` on local side? |
|
||||
|------|----------|--------------|----------------------------|
|
||||
| **Public X.509 endpoint** | Domain + CA-issued X.509 | No (local node is a client) | No |
|
||||
| **Transport relay** (iroh's DERP-equivalent) | iroh `NodeId` (Ed25519) | No (infrastructure) | No |
|
||||
| **Hub / hosting node** | Ed25519 raw key **and/or** X.509 | Yes (full peer) | Yes |
|
||||
|
||||
`PeerEntry` (and the `PeerId` it resolves to) is the model for peers in the
|
||||
call-protocol peer graph (ADR-029). A pure-client connection to a public X.509
|
||||
endpoint is **not** in that graph on the client side: no `PeerEntry`, no
|
||||
`PeerId`, no `PeerRef::Specific` routing.
|
||||
|
||||
### Client-side verifier selection rule (ADR-034 §3)
|
||||
|
||||
| Local has `PeerEntry` for remote? | Remote cert type | Client verifier |
|
||||
|----------------------------------|------------------|-----------------|
|
||||
| No (public X.509 endpoint) | X.509 | `WebPkiServerVerifier` (CA verification) |
|
||||
| No | Ed25519 raw key | fails closed (no CA to fall back to) |
|
||||
| Yes (hub, Ed25519 path) | Ed25519 raw key | fingerprint match (`ed25519:<hex>`) |
|
||||
| Yes (hub, X.509 path) | X.509 | fingerprint match (`SHA256:<hex>`) |
|
||||
|
||||
### What to update
|
||||
|
||||
1. **`auth.rs` doc comments**: add the three-roles table and the verifier
|
||||
selection rule to the `Identity` / `PeerEntry` section doc comments,
|
||||
referencing ADR-034. The `auth.md` spec already has this; mirror it in the
|
||||
source comments.
|
||||
|
||||
2. **`endpoint.rs` doc comments**: clarify that the server-side
|
||||
`AcceptAnyCertVerifier` is "request-but-don't-require" mode for fingerprint
|
||||
extraction (unchanged), and that the **client-side** verifier selection is
|
||||
by `PeerEntry` presence (ADR-034 §3) — note that this is a `CallClient`
|
||||
concern, not an endpoint concern.
|
||||
|
||||
3. **No code changes** — this is comments/docs only. The server-side endpoint
|
||||
is unchanged by ADR-034. The client-side verifier is
|
||||
`call/call-client-verifier-selection`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `auth.rs` doc comments document the three remote roles (ADR-034 §1)
|
||||
- [ ] `auth.rs` doc comments document the client-side verifier selection rule (ADR-034 §3)
|
||||
- [ ] `endpoint.rs` doc comments clarify server-side vs client-side verifier concerns
|
||||
- [ ] Comments reference ADR-034 and `auth.md`
|
||||
- [ ] No code changes (comments only)
|
||||
- [ ] `cargo test -p alknet-core` succeeds (no regressions from comment changes)
|
||||
- [ ] `cargo clippy -p alknet-core` succeeds with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/crates/core/auth.md — Three Remote Roles, Client-side verifier selection
|
||||
- docs/architecture/decisions/034-outgoing-only-x509-and-three-peer-roles.md — ADR-034
|
||||
|
||||
## Notes
|
||||
|
||||
> Documentation-only task to ensure the three-roles model and verifier selection
|
||||
> rule are visible in the source, not just the specs. The server-side endpoint
|
||||
> is unchanged by ADR-034; the client-side verifier selection is implemented in
|
||||
> `call/call-client-verifier-selection`. Folding this into a standalone task
|
||||
> keeps the fingerprint-normalization and resolution-logic tasks focused on
|
||||
> code, not prose.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Reference in New Issue
Block a user