tasks: decompose vault, core, call crates into 28 atomic implementation tasks
Break down the three initial crates (alknet-vault, alknet-core, alknet-call) into dependency-ordered task files for implementation agents. Structure: - tasks/vault/ (10 tasks) — drift fixes from ADR-025/026 refactor, review, spec sync. Vault is independent and can run fully in parallel with core/call. - tasks/core/ (6 tasks) — crate init, core types, config, auth, endpoint, review. Core is foundational; call depends on it. - tasks/call/ (12 tasks) — split into registry/ and protocol/ topic subdirs reflecting the two subsystems. CallAdapter is the merge point. Key decisions: - Drifts 3+9+10 grouped as one task (key-versioning-rotation) — the complete ADR-021 rotation feature that doesn't compile in pieces - Reviews injected at end of each crate phase (vault, core, call) - Vault spec-sync task removes the drift table and bumps doc status to stable - ACME deferred in core/endpoint (noted as TODO; X509 manual certs for now) - OperationEnv kept as a trait (load-bearing for ADR-024 layering) Validated: 28 tasks, no cycles, 11 generations of parallel work. Critical path runs through call (11 tasks). Vault completes by generation 4. 6 high-risk tasks identified (21%): irpc-removal, endpoint, operation-context, operation-env, call-adapter, abort-cascade.
This commit is contained in:
162
tasks/core/auth.md
Normal file
162
tasks/core/auth.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
id: core/auth
|
||||
name: Implement AuthContext, Identity, AuthToken, IdentityProvider trait, and ConfigIdentityProvider
|
||||
status: pending
|
||||
depends_on: [core/core-types]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the authentication types in `src/auth.rs`. Auth is hybrid: the
|
||||
endpoint resolves what it can (TLS-level), handlers resolve what they need
|
||||
(protocol-level). AuthContext may be partial — handlers complete auth inside
|
||||
`handle()`.
|
||||
|
||||
### AuthContext
|
||||
|
||||
```rust
|
||||
#[derive(Clone)]
|
||||
pub struct AuthContext {
|
||||
pub identity: Option<Identity>,
|
||||
pub alpn: Vec<u8>,
|
||||
pub remote_addr: Option<SocketAddr>,
|
||||
pub tls_client_fingerprint: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
Created by the endpoint for each incoming connection. Passed to
|
||||
`ProtocolHandler::handle()` as an immutable reference.
|
||||
|
||||
- `identity`: peer's authenticated identity, if resolved by the endpoint. None
|
||||
means the endpoint has no identity info for this connection.
|
||||
- `alpn`: negotiated ALPN — always present after TLS handshake.
|
||||
- `remote_addr`: peer's address, if available (may be None for iroh).
|
||||
- `tls_client_fingerprint`: SHA-256 fingerprint of TLS client cert, if presented.
|
||||
|
||||
`AuthContext` is `Clone` (handlers clone for per-stream contexts) and immutable
|
||||
in `handle()` (handlers create local variables for resolved identity, they
|
||||
don't mutate the shared context).
|
||||
|
||||
### Identity
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Identity {
|
||||
pub id: String,
|
||||
pub scopes: Vec<String>,
|
||||
pub resources: HashMap<String, Vec<String>>,
|
||||
}
|
||||
```
|
||||
|
||||
The authenticated peer identity. `id` is ALPN-agnostic:
|
||||
- SSH key auth: `"SHA256:abc123..."` (key fingerprint)
|
||||
- API key auth: `"alk_test"` (key prefix)
|
||||
- Certificate auth: `"username"` (principal name)
|
||||
|
||||
### AuthToken
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthToken {
|
||||
pub raw: Vec<u8>,
|
||||
}
|
||||
```
|
||||
|
||||
Opaque authentication token carried in protocol frames. The handler that
|
||||
extracted it knows its encoding.
|
||||
|
||||
### IdentityProvider trait
|
||||
|
||||
```rust
|
||||
pub trait IdentityProvider: Send + Sync + 'static {
|
||||
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity>;
|
||||
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity>;
|
||||
}
|
||||
```
|
||||
|
||||
- `resolve_from_fingerprint()`: used by endpoint (TLS client cert) and SSH (key fingerprint)
|
||||
- `resolve_from_token()`: used by call protocol (AuthToken in first frame) and HTTP (Bearer header)
|
||||
- Both return `Option<Identity>` — None means credential not recognized
|
||||
|
||||
### ConfigIdentityProvider
|
||||
|
||||
```rust
|
||||
pub struct ConfigIdentityProvider {
|
||||
dynamic: Arc<ArcSwap<DynamicConfig>>,
|
||||
}
|
||||
```
|
||||
|
||||
The default implementation. Resolves identities from `DynamicConfig` (reads
|
||||
from ArcSwap on every call — hot-reloadable).
|
||||
|
||||
Resolution logic:
|
||||
- **Fingerprint**: look up in `DynamicConfig::auth::authorized_fingerprints`.
|
||||
If found, return `Identity { id: fingerprint, scopes: ["relay:connect"], resources: {} }`.
|
||||
- **Token**: parse as UTF-8. If starts with `alk_`, look up in
|
||||
`DynamicConfig::auth::api_keys` by prefix match + SHA-256 hash. If found and
|
||||
not expired, return `Identity { id: prefix, scopes: entry.scopes, resources: entry.resources }`.
|
||||
|
||||
Changes to DynamicConfig via ConfigReloadHandle are reflected immediately.
|
||||
|
||||
### Two Identity Scopes
|
||||
|
||||
There are two distinct identity scopes that must not be conflated:
|
||||
|
||||
| Scope | Where set | Where stored | Represents | Used for |
|
||||
|-------|-----------|--------------|------------|----------|
|
||||
| Connection-level | Handler in `handle()` | `Connection` (via `set_identity`) | Who opened the QUIC connection | Observability, logging |
|
||||
| Per-request | CallAdapter per `call.requested` | `OperationContext.identity` | Who makes this specific call | ACL (ADR-015) |
|
||||
|
||||
The connection-level identity is stable (set once). The per-request identity
|
||||
is dynamic (resolved per call, potentially different across requests). The
|
||||
per-request identity takes precedence for ACL.
|
||||
|
||||
### Security constraints
|
||||
|
||||
- **Token entropy**: generated `alk_` tokens must have ≥128 bits of entropy.
|
||||
The prefix (first 8 chars) is for O(1) lookup and is not secret — it appears
|
||||
in logs by design. SHA-256 of the full token allows offline verification; this
|
||||
is safe only if the full token is high-entropy.
|
||||
- **Config reload must be authenticated**: a reload that adds an authorized
|
||||
fingerprint or API key grants access immediately. The reload trigger must be
|
||||
local-only or admin-scoped.
|
||||
- **Connection-level identity is for observability only**: per-request identity
|
||||
takes precedence for ACL.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `AuthContext` struct with all 4 fields, derives `Clone`
|
||||
- [ ] `Identity` struct with `id`, `scopes`, `resources`, derives `Clone`, `PartialEq`
|
||||
- [ ] `AuthToken` struct with `raw` field, derives `Clone`
|
||||
- [ ] `IdentityProvider` trait with both methods
|
||||
- [ ] `ConfigIdentityProvider` struct holding `Arc<ArcSwap<DynamicConfig>>`
|
||||
- [ ] `ConfigIdentityProvider::resolve_from_fingerprint` looks up in authorized_fingerprints
|
||||
- [ ] `ConfigIdentityProvider::resolve_from_token` parses `alk_` prefix, matches by hash, checks expiry
|
||||
- [ ] ConfigIdentityProvider reads from ArcSwap on every call (hot-reloadable)
|
||||
- [ ] Unit test: fingerprint resolution (known fingerprint → Some, unknown → None)
|
||||
- [ ] Unit test: token resolution (valid non-expired → Some, expired → None, unknown → None)
|
||||
- [ ] Unit test: config reload changes resolution results immediately
|
||||
- [ ] `cargo test -p alknet-core` succeeds
|
||||
- [ ] `cargo clippy -p alknet-core` succeeds with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/crates/core/auth.md — all type definitions, resolution flow
|
||||
- docs/architecture/decisions/004-auth-as-shared-core.md — ADR-004
|
||||
- docs/architecture/decisions/011-authcontext-structure.md — ADR-011
|
||||
|
||||
## Notes
|
||||
|
||||
> Auth is hybrid: endpoint resolves TLS-level, handler resolves protocol-level.
|
||||
> AuthContext may be partial (identity = None). The two identity scopes
|
||||
> (connection-level for observability, per-request for ACL) must not be
|
||||
> conflated. ConfigIdentityProvider reads from ArcSwap on every call so config
|
||||
> reloads take effect immediately.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Reference in New Issue
Block a user