diff --git a/core/archipelago/src/identity_manager.rs b/core/archipelago/src/identity_manager.rs index 4c70f176..4ca03aae 100644 --- a/core/archipelago/src/identity_manager.rs +++ b/core/archipelago/src/identity_manager.rs @@ -215,6 +215,17 @@ impl IdentityManager { self.load_signing_key(id).await } + /// Get the default identity ID, if one is set. + pub async fn get_default_id(&self) -> Result> { + let marker = self.identities_dir.join(DEFAULT_MARKER); + if marker.exists() { + let id = fs::read_to_string(&marker).await?; + Ok(Some(id.trim().to_string())) + } else { + Ok(None) + } + } + /// Sign data with a specific identity. pub async fn sign(&self, id: &str, data: &[u8]) -> Result { let signing_key = self.load_signing_key(id).await?; @@ -415,3 +426,100 @@ impl IdentityManager { Ok(SigningKey::from_bytes(&arr)) } } + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[tokio::test] + async fn test_create_identity_did_key_format() { + let dir = tempdir().unwrap(); + let mgr = IdentityManager::new(dir.path()).await.unwrap(); + let record = mgr.create("Test".to_string(), IdentityPurpose::Personal).await.unwrap(); + assert!(record.did.starts_with("did:key:z6Mk"), "DID should be did:key:z6Mk..., got {}", record.did); + assert!(!record.id.is_empty()); + assert_eq!(record.name, "Test"); + } + + #[tokio::test] + async fn test_create_nostr_key_npub_format() { + let dir = tempdir().unwrap(); + let mgr = IdentityManager::new(dir.path()).await.unwrap(); + let record = mgr.create("Nostr".to_string(), IdentityPurpose::Personal).await.unwrap(); + let npub = mgr.create_nostr_key(&record.id).await.unwrap(); + assert!(npub.starts_with("npub1"), "npub should start with npub1, got {}", npub); + } + + #[tokio::test] + async fn test_sign_and_verify() { + let dir = tempdir().unwrap(); + let mgr = IdentityManager::new(dir.path()).await.unwrap(); + let record = mgr.create("Signer".to_string(), IdentityPurpose::Personal).await.unwrap(); + let data = b"hello archipelago"; + let sig = mgr.sign(&record.id, data).await.unwrap(); + let valid = mgr.verify(&record.did, data, &sig).await.unwrap(); + assert!(valid, "Signature should verify"); + } + + #[tokio::test] + async fn test_sign_verify_wrong_data() { + let dir = tempdir().unwrap(); + let mgr = IdentityManager::new(dir.path()).await.unwrap(); + let record = mgr.create("Signer".to_string(), IdentityPurpose::Personal).await.unwrap(); + let sig = mgr.sign(&record.id, b"correct").await.unwrap(); + let valid = mgr.verify(&record.did, b"wrong", &sig).await.unwrap(); + assert!(!valid, "Signature should not verify with wrong data"); + } + + #[tokio::test] + async fn test_list_identities() { + let dir = tempdir().unwrap(); + let mgr = IdentityManager::new(dir.path()).await.unwrap(); + mgr.create("One".to_string(), IdentityPurpose::Personal).await.unwrap(); + mgr.create("Two".to_string(), IdentityPurpose::Business).await.unwrap(); + let (list, _) = mgr.list().await.unwrap(); + assert_eq!(list.len(), 2); + } + + #[tokio::test] + async fn test_delete_identity() { + let dir = tempdir().unwrap(); + let mgr = IdentityManager::new(dir.path()).await.unwrap(); + let r1 = mgr.create("First".to_string(), IdentityPurpose::Personal).await.unwrap(); + let r2 = mgr.create("Second".to_string(), IdentityPurpose::Business).await.unwrap(); + mgr.delete(&r1.id).await.unwrap(); + let (list, _) = mgr.list().await.unwrap(); + assert_eq!(list.len(), 1); + assert_eq!(list[0].id, r2.id); + } + + #[tokio::test] + async fn test_set_and_get_default() { + let dir = tempdir().unwrap(); + let mgr = IdentityManager::new(dir.path()).await.unwrap(); + let r1 = mgr.create("First".to_string(), IdentityPurpose::Personal).await.unwrap(); + let r2 = mgr.create("Second".to_string(), IdentityPurpose::Business).await.unwrap(); + mgr.set_default(&r2.id).await.unwrap(); + let default_id = mgr.get_default_id().await.unwrap(); + assert_eq!(default_id, Some(r2.id.clone())); + // First is no longer default + assert_ne!(default_id, Some(r1.id)); + } + + #[tokio::test] + async fn test_delete_default_shifts() { + let dir = tempdir().unwrap(); + let mgr = IdentityManager::new(dir.path()).await.unwrap(); + let r1 = mgr.create("First".to_string(), IdentityPurpose::Personal).await.unwrap(); + mgr.create("Second".to_string(), IdentityPurpose::Business).await.unwrap(); + mgr.set_default(&r1.id).await.unwrap(); + mgr.delete(&r1.id).await.unwrap(); + let (list, default_id) = mgr.list().await.unwrap(); + assert_eq!(list.len(), 1); + // Default should have shifted or be cleared + if let Some(def) = default_id { + assert_ne!(def, r1.id, "Default should not point to deleted identity"); + } + } +} diff --git a/docs/adr/011-dwn-deprioritization.md b/docs/adr/011-dwn-deprioritization.md new file mode 100644 index 00000000..77242009 --- /dev/null +++ b/docs/adr/011-dwn-deprioritization.md @@ -0,0 +1,31 @@ +# ADR-011: DWN Deprioritization + +## Status + +Accepted + +## Context + +TBD/Block shut down in November 2024, donating Web5 code to the Decentralized Identity Foundation (DIF). The DWN (Decentralized Web Node) specification was heavily backed by TBD — without their engineering team, the spec has lost momentum: + +- No maintained Rust DWN SDK exists (the `dwn` crate by unavi-xyz is v0.4.0 with 323 downloads) +- TBD's reference implementation was TypeScript-only +- DIF has not allocated resources to continue DWN development +- The spec itself is complex (personal data stores with protocol-based access control) + +Meanwhile, Archipelago's federation over Tor + Nostr relays already serves the core peer data sync use case that DWN was intended for. + +## Decision + +1. **Keep existing DWN store code** in `core/archipelago/src/network/dwn_store.rs` — it works for peer file catalogs and federation state +2. **Stop calling it "Web5 DWN"** in user-facing text — it's our custom implementation, not a full DWN spec implementation +3. **Do not invest in DWN spec compliance** — the spec is stalled and may not stabilize +4. **Prioritize Nostr + federation** for peer discovery and data exchange +5. **Re-evaluate if DIF produces a viable Rust SDK** or the spec gains new maintainers + +## Consequences + +- DWN functionality remains available but is not actively developed +- Peer sync uses federation + Nostr instead of DWN protocols +- Reduces maintenance burden — no need to track a stalled spec +- If DWN resurfaces with strong tooling, we can adopt it later diff --git a/loop/plan.md b/loop/plan.md index 387634eb..ae1e5ed6 100644 --- a/loop/plan.md +++ b/loop/plan.md @@ -116,9 +116,9 @@ - [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`. +- [x] **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`. -- [ ] **Write ADR for DWN deprioritization**: Create `docs/adr/011-dwn-deprioritization.md`. Document: (1) TBD/Block shut down Nov 2024, donated code to DIF, (2) no maintained Rust DWN SDK exists, (3) DWN spec losing momentum without TBD's backing, (4) Archy's federation over Tor + Nostr relays already serve the peer data sync use case, (5) DWN store code stays in codebase but is not actively developed, (6) re-evaluate if DIF produces a viable Rust SDK. Follow existing ADR format in `docs/adr/`. This is documentation only — no code changes. +- [x] **Write ADR for DWN deprioritization**: Create `docs/adr/011-dwn-deprioritization.md`. Document: (1) TBD/Block shut down Nov 2024, donated code to DIF, (2) no maintained Rust DWN SDK exists, (3) DWN spec losing momentum without TBD's backing, (4) Archy's federation over Tor + Nostr relays already serve the peer data sync use case, (5) DWN store code stays in codebase but is not actively developed, (6) re-evaluate if DIF produces a viable Rust SDK. Follow existing ADR format in `docs/adr/`. This is documentation only — no code changes. - [ ] **Deploy to both nodes and test Web5 features**: Deploy with `./scripts/deploy-to-target.sh --both`. Test at `http://192.168.1.228`: (1) navigate to Web5 page — DID displays correctly, (2) click "Publish to DHT" if available — should publish and show status, (3) go to Credentials page — issue a test credential to self, verify it shows in list. Repeat on `http://192.168.1.198`. Check logs on both: `ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago --since "5 min ago" | grep -iE "(did|credential|dwn|identity)"'` and same for .198.