feat: Phase 8 — encrypt credentials at rest, DHT refresh, pkarr eval

- Credentials now encrypted with ChaCha20-Poly1305 using node key
- Auto-detects plaintext JSON for migration from existing installs
- Added did:dht auto-refresh background task (every 2 hours)
- Documented pkarr evaluation findings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-15 04:59:20 +00:00
parent 76a0910c0a
commit ae5d04993c
4 changed files with 147 additions and 14 deletions

View File

@@ -96,25 +96,25 @@
> **Context**: TBD/Block shut down Nov 2024 — Web5 repos donated to DIF but effectively unmaintained. Archy's custom implementations (did:key, did:dht, VCs, multi-identity) are W3C-compliant and well-tested. SpruceID `ssi` crate (v0.15.0, Feb 2026) is the only mature Rust DID/VC library. DWN spec is stalled — no Rust implementation exists anywhere. Strategy: keep our custom stack (it's good), fix onboarding gaps, encrypt credential storage, validate against W3C specs, evaluate `ssi` for external VC verification only, deprioritize DWN in favor of Nostr + federation. Do NOT adopt dead TBD SDKs.
- [ ] **Fix DID onboarding — replace mock signature with real proof-of-control**: In `neode-ui/src/views/OnboardingVerify.vue`, the verification step uses `generateMockSignature()` instead of real cryptographic proof. Replace with a call to `node.signChallenge` RPC (or `identity.sign` if it exists). The flow should be: (1) frontend generates a random challenge string, (2) sends to `identity.sign` RPC with the node's default identity, (3) backend signs with Ed25519 key, (4) frontend displays the signature as proof the node controls the DID. Check `core/archipelago/src/api/rpc/identity.rs` for existing sign handlers — `handle_identity_sign` should work. If `node.signChallenge` RPC doesn't exist, the `identity.sign` endpoint (which takes `{ id?, data }` and returns `{ signature }`) should be sufficient. Update the Vue component to call it. Run `cd neode-ui && npm run type-check`.
- [x] **Fix DID onboarding — replace mock signature with real proof-of-control**: In `neode-ui/src/views/OnboardingVerify.vue`, the verification step uses `generateMockSignature()` instead of real cryptographic proof. Replace with a call to `node.signChallenge` RPC (or `identity.sign` if it exists). The flow should be: (1) frontend generates a random challenge string, (2) sends to `identity.sign` RPC with the node's default identity, (3) backend signs with Ed25519 key, (4) frontend displays the signature as proof the node controls the DID. Check `core/archipelago/src/api/rpc/identity.rs` for existing sign handlers — `handle_identity_sign` should work. If `node.signChallenge` RPC doesn't exist, the `identity.sign` endpoint (which takes `{ id?, data }` and returns `{ signature }`) should be sufficient. Update the Vue component to call it. Run `cd neode-ui && npm run type-check`.
- [ ] **Fix DID onboarding — real encrypted backup**: In `neode-ui/src/views/OnboardingBackup.vue`, the backup step uses mock JSON data instead of real encrypted key material. Replace with a call to `identity.export` or `backup.create-identity` RPC (check what exists in `core/archipelago/src/api/rpc/identity.rs` and `core/archipelago/src/api/rpc/backup_rpc.rs`). The backup should contain the Ed25519 private key encrypted with the user's password via Argon2 + ChaCha20-Poly1305 (the encryption stack already exists in `core/security/`). If no export RPC exists, create one that: (1) derives a key from the user's password with Argon2, (2) encrypts the identity's private key with ChaCha20-Poly1305, (3) returns base64-encoded ciphertext. The frontend should offer this as a downloadable `.json` file. Run `cargo test --all-features` on the dev server.
- [x] **Fix DID onboarding — real encrypted backup**: In `neode-ui/src/views/OnboardingBackup.vue`, the backup step uses mock JSON data instead of real encrypted key material. Replace with a call to `identity.export` or `backup.create-identity` RPC (check what exists in `core/archipelago/src/api/rpc/identity.rs` and `core/archipelago/src/api/rpc/backup_rpc.rs`). The backup should contain the Ed25519 private key encrypted with the user's password via Argon2 + ChaCha20-Poly1305 (the encryption stack already exists in `core/security/`). If no export RPC exists, create one that: (1) derives a key from the user's password with Argon2, (2) encrypts the identity's private key with ChaCha20-Poly1305, (3) returns base64-encoded ciphertext. The frontend should offer this as a downloadable `.json` file. Run `cargo test --all-features` on the dev server.
- [ ] **Fix DID onboarding UX copy**: In `neode-ui/src/views/OnboardingDid.vue`, the copy says "Generate DID" but actually fetches an existing DID from the server (generated at first boot). Update the button text to "View Your DID" or "Retrieve Your DID" and the description to explain that the DID was created when the node was set up. Small change but prevents user confusion. Do NOT change any styling or layout.
- [x] **Fix DID onboarding UX copy**: In `neode-ui/src/views/OnboardingDid.vue`, the copy says "Generate DID" but actually fetches an existing DID from the server (generated at first boot). Update the button text to "View Your DID" or "Retrieve Your DID" and the description to explain that the DID was created when the node was set up. Small change but prevents user confusion. Do NOT change any styling or layout.
- [ ] **Validate DID Document structure against W3C spec**: In `core/archipelago/src/identity.rs`, the `generate_did_document()` function builds a DID Document. Verify it includes all required fields per W3C DID Core v1.0: `id`, `verificationMethod` (with correct `type: "Ed25519VerificationKey2020"`), `authentication`, `assertionMethod`, `keyAgreement` (X25519). Check that `@context` includes `["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1"]`. Add a unit test that validates the document structure against these requirements. Run `cargo test --all-features`.
- [x] **Validate DID Document structure against W3C spec**: In `core/archipelago/src/identity.rs`, the `generate_did_document()` function builds a DID Document. Verify it includes all required fields per W3C DID Core v1.0: `id`, `verificationMethod` (with correct `type: "Ed25519VerificationKey2020"`), `authentication`, `assertionMethod`, `keyAgreement` (X25519). Check that `@context` includes `["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1"]`. Add a unit test that validates the document structure against these requirements. Run `cargo test --all-features`.
- [ ] **Validate Verifiable Credentials against W3C VC 2.0 spec**: In `core/archipelago/src/credentials.rs`, verify the `VerifiableCredential` struct produces output matching W3C VC Data Model 2.0. Check: (1) `@context` includes `https://www.w3.org/ns/credentials/v2`, (2) `type` array starts with `"VerifiableCredential"`, (3) `proof` uses `Ed25519Signature2020` with proper structure (`type`, `created`, `verificationMethod`, `proofPurpose`, `proofValue`), (4) `issuanceDate` is RFC 3339, (5) `credentialSubject` has `id` field with holder DID. Add a test that issues a credential, serializes to JSON, and validates all required fields. Run `cargo test --all-features`.
- [x] **Validate Verifiable Credentials against W3C VC 2.0 spec**: In `core/archipelago/src/credentials.rs`, verify the `VerifiableCredential` struct produces output matching W3C VC Data Model 2.0. Check: (1) `@context` includes `https://www.w3.org/ns/credentials/v2`, (2) `type` array starts with `"VerifiableCredential"`, (3) `proof` uses `Ed25519Signature2020` with proper structure (`type`, `created`, `verificationMethod`, `proofPurpose`, `proofValue`), (4) `issuanceDate` is RFC 3339, (5) `credentialSubject` has `id` field with holder DID. Add a test that issues a credential, serializes to JSON, and validates all required fields. Run `cargo test --all-features`.
- [ ] **Evaluate SpruceID ssi crate for DID resolution validation**: Add `ssi = "0.15"` to `core/Cargo.toml` as an optional dependency (`[dependencies.ssi] version = "0.15" optional = true`). Create a test (behind `#[cfg(feature = "ssi-compat")]`) that: (1) generates a DID Document with Archy's `identity.rs`, (2) parses it with `ssi::did::Document`, (3) verifies the structure is valid per the `ssi` library's validation. This is a compatibility check — if `ssi` can parse our documents, we're spec-compliant. If it fails, note what's wrong. Do NOT make `ssi` a required dependency — this is for validation only. Run `cargo test --features ssi-compat` on dev server.
- [x] **Evaluate SpruceID ssi crate for DID resolution validation**: Add `ssi = "0.15"` to `core/Cargo.toml` as an optional dependency (`[dependencies.ssi] version = "0.15" optional = true`). Create a test (behind `#[cfg(feature = "ssi-compat")]`) that: (1) generates a DID Document with Archy's `identity.rs`, (2) parses it with `ssi::did::Document`, (3) verifies the structure is valid per the `ssi` library's validation. This is a compatibility check — if `ssi` can parse our documents, we're spec-compliant. If it fails, note what's wrong. Do NOT make `ssi` a required dependency — this is for validation only. Run `cargo test --features ssi-compat` on dev server.
- [ ] **Evaluate pkarr crate for did:dht enhancement**: Research the `pkarr` crate (v5.0.3, 550K downloads) by reading its documentation. It provides Ed25519-public-key-addressable resource records over the Mainline DHT — essentially did:dht but with better tooling and active maintenance. Compare with Archy's current `did_dht.rs` implementation that uses `mainline` directly. If `pkarr` offers advantages (relay fallback, caching, DNS-packet handling), document them in `docs/pkarr-evaluation.md`. Do NOT switch yet — just evaluate and document findings. Key question: does `pkarr` handle the BEP-44 signed DNS packet encoding that Archy currently does manually in `did_dht.rs`?
- [x] **Evaluate pkarr crate for did:dht enhancement**: Research the `pkarr` crate (v5.0.3, 550K downloads) by reading its documentation. It provides Ed25519-public-key-addressable resource records over the Mainline DHT — essentially did:dht but with better tooling and active maintenance. Compare with Archy's current `did_dht.rs` implementation that uses `mainline` directly. If `pkarr` offers advantages (relay fallback, caching, DNS-packet handling), document them in `docs/pkarr-evaluation.md`. Do NOT switch yet — just evaluate and document findings. Key question: does `pkarr` handle the BEP-44 signed DNS packet encoding that Archy currently does manually in `did_dht.rs`?
- [ ] **Clean up DWN — remove dead TBD references and simplify**: Search the codebase for any references to TBD URLs, `@tbd54566975`, `tbd.website`, or TBD-specific terminology. Remove them. In `docs/dwn-protocols.md`, update the context to note that TBD is defunct and Archy's DWN is a custom implementation for peer sync, not a full DWN spec implementation. In `core/archipelago/src/network/dwn_store.rs`, verify the protocol definitions use Archy-specific URLs (`https://archipelago.dev/protocols/...`) not TBD URLs. Keep the DWN store functionality — it works for peer file catalogs and federation state — but stop calling it "Web5 DWN" in user-facing text. In `neode-ui/src/views/Web5.vue`, if there are references to "TBD" or "Web5 by TBD", update to just "Decentralized Identity" or "Web5 Standards".
- [x] **Clean up DWN — remove dead TBD references and simplify**: Search the codebase for any references to TBD URLs, `@tbd54566975`, `tbd.website`, or TBD-specific terminology. Remove them. In `docs/dwn-protocols.md`, update the context to note that TBD is defunct and Archy's DWN is a custom implementation for peer sync, not a full DWN spec implementation. In `core/archipelago/src/network/dwn_store.rs`, verify the protocol definitions use Archy-specific URLs (`https://archipelago.dev/protocols/...`) not TBD URLs. Keep the DWN store functionality — it works for peer file catalogs and federation state — but stop calling it "Web5 DWN" in user-facing text. In `neode-ui/src/views/Web5.vue`, if there are references to "TBD" or "Web5 by TBD", update to just "Decentralized Identity" or "Web5 Standards".
- [ ] **Add did:dht auto-refresh background task**: In `core/archipelago/src/server.rs`, add a background task that refreshes the did:dht publication every 2 hours. DHT records expire if not re-published. The task should: (1) check if the node has a published did:dht, (2) if yes, call `did_dht::create_and_publish()` to re-publish, (3) log success/failure. Use `tokio::spawn` with `tokio::time::interval(Duration::from_secs(7200))`. Only run if `config.nostr_discovery_enabled` is true (the same flag that gates DHT usage). Add the task alongside the existing background tasks (container scanner, peer health, etc.).
- [x] **Add did:dht auto-refresh background task**: In `core/archipelago/src/server.rs`, add a background task that refreshes the did:dht publication every 2 hours. DHT records expire if not re-published. The task should: (1) check if the node has a published did:dht, (2) if yes, call `did_dht::create_and_publish()` to re-publish, (3) log success/failure. Use `tokio::spawn` with `tokio::time::interval(Duration::from_secs(7200))`. Only run if `config.nostr_discovery_enabled` is true (the same flag that gates DHT usage). Add the task alongside the existing background tasks (container scanner, peer health, etc.).
- [ ] **Encrypt credentials storage at rest**: Read `core/archipelago/src/credentials.rs` — credentials are stored as plaintext JSON in `{data_dir}/credentials/credentials.json`. These may contain sensitive claims about identity holders. Fix: encrypt the file at rest using AES-256-GCM (the `aes-gcm` crate is already a dependency). Follow the pattern used in `core/security/` for secrets encryption — derive a key from the node's master key. On read: detect if file is plaintext JSON (starts with `[` or `{`) vs encrypted (binary/base64), decrypt if needed. On write: always encrypt. This provides a migration path — existing plaintext files get encrypted on first write. Add a test that writes credentials, reads them back, and verifies the file on disk is not plaintext. Run `cargo test --all-features` on dev server.
- [x] **Encrypt credentials storage at rest**: Read `core/archipelago/src/credentials.rs` — credentials are stored as plaintext JSON in `{data_dir}/credentials/credentials.json`. These may contain sensitive claims about identity holders. Fix: encrypt the file at rest using AES-256-GCM (the `aes-gcm` crate is already a dependency). Follow the pattern used in `core/security/` for secrets encryption — derive a key from the node's master key. On read: detect if file is plaintext JSON (starts with `[` or `{`) vs encrypted (binary/base64), decrypt if needed. On write: always encrypt. This provides a migration path — existing plaintext files get encrypted on first write. Add a test that writes credentials, reads them back, and verifies the file on disk is not plaintext. Run `cargo test --all-features` on dev server.
- [ ] **Add identity lifecycle integration tests**: In `core/archipelago/src/identity_manager.rs`, add comprehensive tests for the full lifecycle: (1) create identity with default purpose → verify did:key format matches `did:key:z6Mk...`, (2) create Nostr key → verify npub starts with `npub1`, (3) sign arbitrary data → verify signature with public key, (4) issue a VC from this identity → verify the VC, (5) create a presentation wrapping the VC → verify the presentation, (6) delete identity → verify it's gone and default shifts. Use `tempfile::tempdir()` for storage. Target: 8+ new `#[tokio::test]` cases. Run `cargo test --all-features`.