136 lines
5.9 KiB
Markdown
136 lines
5.9 KiB
Markdown
# ADR-0008: chronogrid_net — Node Identity & Swarm Key (Task 2.2)
|
|
|
|
**Date:** 2026-08-22
|
|
**Status:** Accepted
|
|
**Deciders:** Architecture Review Board (automated agent cycle)
|
|
**Task:** 2.2 — Implement chronogrid_net: Node Identity & Swarm Key
|
|
|
|
---
|
|
|
|
## Context
|
|
|
|
Task 2.2 requires implementing the foundational identity and cluster-authentication
|
|
primitives for the `chronogrid_net` crate. Every ChronoGrid node must have a
|
|
stable, cryptographically verifiable identity and a mechanism to restrict mesh
|
|
participation to authorized devices only.
|
|
|
|
The security specification (`docs/ARCHITECTURE/02-security-and-crypto.md`) mandates:
|
|
|
|
- **Node Identity:** Ed25519 keypair; public key acts as the `NodeId`.
|
|
- **Swarm Key:** 256-bit pre-shared secret; unauthenticated nodes cannot join.
|
|
- **Wire Protocol:** Noise XK (X25519 + ChaCha20-Poly1305) — requires the
|
|
Ed25519 identity for the static key role.
|
|
|
|
---
|
|
|
|
## Decision
|
|
|
|
### 1. Ed25519 Keypair via `ed25519-dalek v2`
|
|
|
|
`ed25519-dalek` v2 is the current stable, audited Rust implementation of
|
|
Ed25519. It integrates cleanly with `rand` v0.8 via the `rand_core` feature
|
|
and produces `SigningKey` / `VerifyingKey` types that are compatible with the
|
|
libp2p identity layer (Task 2.3 will wrap these into a `libp2p::identity::Keypair`).
|
|
|
|
**Alternative considered:** Using `libp2p::identity` directly for key generation.
|
|
**Rejected because:** Task 2.2 must not introduce the full libp2p dependency
|
|
tree yet; identity primitives should be independently testable without the
|
|
network stack.
|
|
|
|
### 2. NodeId = SHA-256(public key bytes)
|
|
|
|
The `NodeId` is derived by SHA-256-hashing the 32-byte Ed25519 public key,
|
|
producing a 32-byte (64 hex char) stable identifier.
|
|
|
|
- Matches the `node_peers.id` column definition in `schema.sql`
|
|
(`NodeId: Ed25519 public key hash (hex)`).
|
|
- SHA-256 is collision-resistant and produces a fixed-length output suitable
|
|
for use as a database primary key and display string.
|
|
- The `NodeId` is distinct from the raw public key, preventing accidental
|
|
exposure of the key material in log output.
|
|
|
|
### 3. SwarmKey = 32 random bytes (OsRng)
|
|
|
|
The Swarm Key is generated using `OsRng` (OS-backed CSPRNG) and stored as a
|
|
64-character lowercase hex string. This matches the `node_peers.swarm_key_fingerprint`
|
|
schema column which stores the first 16 hex chars of `SHA-256(swarm_key_bytes)`.
|
|
|
|
**Fingerprint design:** Only the first 8 bytes (16 hex chars) of the SHA-256
|
|
hash are stored in the database, providing a non-reversible display identifier
|
|
without exposing the full key.
|
|
|
|
### 4. Zeroize on Drop
|
|
|
|
Both `NodeIdentity` (via `ZeroizeOnDrop` derive) and `SwarmKey` (via
|
|
`ZeroizeOnDrop` derive) scrub their secret bytes from memory when dropped.
|
|
`PersistedIdentity` (the JSON serialization helper) also derives `Zeroize +
|
|
ZeroizeOnDrop` to prevent secret key hex strings from lingering in heap memory.
|
|
|
|
### 5. Persistence Format
|
|
|
|
| Artifact | Format | Location (recommended) |
|
|
|--------------|---------------------------------|-------------------------------------------|
|
|
| NodeIdentity | JSON with hex-encoded key fields | `<vault_dir>/identity.json` |
|
|
| SwarmKey | 64-char hex string (plain text) | `<vault_dir>/swarm.key` |
|
|
|
|
Both files must reside inside the encrypted vault directory (SQLCipher-protected
|
|
at rest). The caller (`chronogrid_daemon` / Flutter app) is responsible for
|
|
placing them in the correct location.
|
|
|
|
### 6. Constant-Time Equality for SwarmKey
|
|
|
|
`SwarmKey::eq` uses a byte-by-byte XOR accumulation pattern with
|
|
`std::hint::black_box` to prevent the compiler from short-circuiting the
|
|
comparison, mitigating timing side-channel attacks when comparing swarm keys.
|
|
|
|
---
|
|
|
|
## Consequences
|
|
|
|
### Positive
|
|
|
|
- `NodeIdentity` and `SwarmKey` are independently testable without any network
|
|
stack dependency.
|
|
- The `NodeId` hex string maps directly to the `node_peers.id` primary key,
|
|
enabling zero-transformation database inserts.
|
|
- `SwarmKey::fingerprint()` maps directly to `node_peers.swarm_key_fingerprint`,
|
|
enabling schema-aligned peer registry entries.
|
|
- All secret material is zeroized on drop, satisfying the zero-trust local
|
|
storage mandate.
|
|
- 30 unit tests cover: generation uniqueness, determinism, hex round-trips,
|
|
serialization round-trips, file persistence, error paths, and security
|
|
properties (debug output redaction, constant-time equality).
|
|
|
|
### Negative / Trade-offs
|
|
|
|
- The JSON persistence format for `NodeIdentity` stores the secret key in hex.
|
|
This is acceptable only because the file resides inside the encrypted vault
|
|
directory. A future hardening pass (Phase 5) may encrypt the identity file
|
|
itself using the master key from `chronogrid_crypto`.
|
|
- `ed25519-dalek` v2 and `libp2p` use compatible but distinct key type wrappers.
|
|
Task 2.3 will need a thin conversion layer (`SigningKey` → `libp2p::identity::ed25519::Keypair`).
|
|
|
|
---
|
|
|
|
## Module Structure
|
|
|
|
```
|
|
crates/chronogrid_net/
|
|
├── Cargo.toml # ed25519-dalek, rand, sha2, hex, serde, thiserror, zeroize
|
|
└── src/
|
|
├── lib.rs # Public re-exports: NetError, NetResult, NodeId, NodeIdentity, SwarmKey
|
|
├── error.rs # NetError enum + NetResult<T> alias
|
|
├── identity.rs # NodeId, NodeIdentity, generate_node_identity(), load_or_create_identity()
|
|
└── swarm_key.rs # SwarmKey: generate, from_bytes, from_hex, fingerprint, save, load
|
|
```
|
|
|
|
---
|
|
|
|
## Test Coverage
|
|
|
|
| Module | Tests | Key Scenarios |
|
|
|---------------|-------|----------------------------------------------------------------------|
|
|
| `identity` | 11 | Uniqueness, determinism, hex format, round-trip, file I/O, tampering |
|
|
| `swarm_key` | 16 | Generation, hex/bytes round-trips, error paths, fingerprint, file I/O, security |
|
|
| **Total** | **27**| |
|