Phase 11: Multi-Device Identity & Sync
Date: 2025-01-14 Phase: 11 Status: Complete ✅
Overview
Phase 11 implements multi-device identity support, allowing a single DID to be controlled by multiple devices with capability-based permissions. This is foundational for ICN's real-world usability - users need to access their cooperative identity from laptops, phones, and servers.
Goals
- Multi-device support: One DID, multiple authorized keys
- Capability-based security: Fine-grained per-device permissions
- Audit trail: Track device lifecycle (add, revoke, rotate)
- Secure storage: Keystore v3 with automatic migration
- Gossip sync: Distribute DID Document updates across network
- CLI workflow: Complete device management interface
Architecture Decisions
1. DID Document v2 Design
Decision: Extend DID Documents to support multiple VerificationMethods per DID.
Rationale:
- Follows W3C DID spec patterns
- Each device gets its own keypair
- Enables device-specific revocation without rotating primary key
- Supports gradual permission reduction (e.g., laptop loses AddDevice capability)
Key data structures:
pub struct DidDocument {
pub id: Did,
pub version: u64,
pub updated_at: u64,
pub verification_method: Vec<VerificationMethod>,
pub authentication: Vec<String>, // Signing-enabled device IDs
pub recovery: Option<RecoveryConfig>,
}
pub struct VerificationMethod {
pub id: String, // Device ID
pub label: String, // User-friendly name
pub key_type: KeyType,
pub public_key: Vec<u8>,
pub capabilities: Vec<Capability>,
pub added_at: u64,
pub revoked_at: Option<u64>,
}
Capabilities:
Sign- Sign messages and contractsAddDevice- Authorize new devicesRevokeDevice- Revoke other devicesRotateKey- Rotate this device's keyRecover- Use recovery mechanismsEncrypt- Decrypt messages (X25519)
2. Keystore v3 Format
Decision: Extend keystore format to include DID Document, device ID, and rotation chain.
Migration path:
- v1: Ed25519 only (legacy)
- v2: + TLS binding
- v2.1: + X25519 keys
- v3: + DID Document + device ID + rotation chain
Auto-migration: Seamless v1 → v2.1 → v3 upgrade on first unlock.
Key implementation detail:
impl Drop for StoredKeyV3 {
fn drop(&mut self) {
// Only zeroize sensitive fields
self.secret_bytes.zeroize();
self.x25519_secret.zeroize();
self.tls_key_der.zeroize();
// DID Document and rotation chain are public data
}
}
Challenge: Balancing security (zeroization) with Rust's ownership model. Initially tried to derive Zeroize, but DidDocument and Vec<RotationEvent> are public data that don't need zeroization. Solution: Manual Drop implementation.
3. Identity Sync Protocol
Decision: Use gossip topic identity:updates for broadcasting DID Document changes.
Message format:
pub struct IdentityUpdateMessage {
pub did: Did,
pub event: RotationEvent,
pub new_version: u64,
pub timestamp: u64,
}
Serialization: bincode (~280 bytes per update)
Cache implementation:
pub struct DidDocumentCache {
inner: Arc<RwLock<HashMap<Did, CachedDidDocument>>>,
}
Version ordering: Prevents replay attacks by rejecting stale updates.
Verification: Every device change is signed by an authorized key (capability: AddDevice or RevokeDevice).
4. CLI Workflow
Device addition workflow:
- New device:
icnctl device add laptop2→ generates Ed25519 + X25519 keys + request file - Authorized device:
icnctl device approve laptop2-request.json→ updates DID Document - Broadcast: IdentityUpdateMessage sent via gossip
- Peers: Apply rotation event to cached DID Document
Device revocation workflow:
icnctl device revoke device-abc123 --reason "lost laptop"- DID Document updated with revoked_at timestamp
- Broadcast via gossip
- Peers: Mark device as revoked in cache
Design decision: Revocation is soft deletion (preserves audit trail) rather than removal.
Implementation Challenges
Challenge 1: Zeroize Trait Constraints
Problem: StoredKeyV3 contains fields that don't implement DefaultIsZeroes (DidDocument, Vec
Initial approach: Derive Zeroize on entire struct.
Error:
error: cannot move out of type 'StoredKeyV3', which implements the 'Drop' trait
Solution: Manual Drop implementation that only zeroizes sensitive fields:
impl Drop for StoredKeyV3 {
fn drop(&mut self) {
self.secret_bytes.zeroize();
self.x25519_secret.zeroize();
self.tls_key_der.zeroize();
}
}
Lesson: Not all data needs zeroization - only cryptographic secrets. DID Documents and rotation events are public data.
Challenge 2: Device Workflow Conceptual Issue
Problem: Initially implemented device add to generate a new DID per device.
Error: Misunderstanding of multi-device identity model.
Insight: Multi-device identity means:
- Same DID across all devices
- Different keys for each device
- Each key has its own VerificationMethod entry
Corrected workflow:
// Prompt for the target DID (the identity to add this device to)
print!("Enter the DID to add this device to: ");
let target_did = did_input.trim();
// Generate Ed25519 keypair for THIS device (not new DID)
let keypair = KeyPair::generate()?;
Lesson: Multi-device is about key management, not identity creation.
Challenge 3: Version Mismatch in Device Approval (Critical Bug)
Problem: Device approval was calling add_device() twice (once for Ed25519, once for X25519), incrementing the version from 1→2→3, but the rotation event was created with new_version = did_doc.version + 1 = 2.
Error: Version mismatch between rotation event and actual DID Document state.
Example:
// rotation_event created BEFORE applying update
let rotation_event = RotationEvent {
new_version: did_doc.version + 1, // e.g., 2
// ...
};
// Then update function called add_device() TWICE
keystore.update_did_document(|did_doc| {
did_doc.add_device(...)?; // version: 1 → 2
did_doc.add_device(...)?; // version: 2 → 3 ⚠️ MISMATCH!
}, Some(rotation_event), &passphrase)?;
Impact: This would cause verification failures in identity sync when peers try to apply rotation events, because the version check at multi_device.rs:450 expects new_version to be did_doc.version + 1.
Solution: Created add_device_with_encryption_key() method that adds both keys with single version increment:
pub fn add_device_with_encryption_key(
&mut self,
device_id: String,
label: String,
ed25519_public_key: Vec<u8>,
x25519_public_key: Vec<u8>,
signing_capabilities: Vec<Capability>,
) -> Result<()> {
// Add Ed25519 signing key
self.verification_method.push(...);
// Add X25519 encryption key
self.verification_method.push(...);
// Update version ONCE for the logical operation
self.version += 1;
Ok(())
}
Test coverage: Added test_add_device_with_encryption_key_version_increment() to verify version increments by exactly 1.
Lesson: When creating rotation events, ensure the version increment matches the semantic operation being performed. A device addition is a single logical operation, even if it involves multiple keys.
Challenge 4: Borrow Checker in Migration Test
Problem: Held reference to DID Document while trying to lock/unlock keystore.
Error:
cannot borrow 'ks' as mutable because it is also borrowed as immutable
Solution: Clone values before mutation:
let did_doc_id = did_doc.id.clone();
let did_doc_version = did_doc.version;
// Now can lock/unlock
ks.lock();
ks.unlock(passphrase).unwrap();
Lesson: Rust's borrow checker enforces clear separation between reading and writing.
Test Coverage
Unit tests (31 total in icn-identity):
multi_device.rs: DID Document creation, device add/revoke, capability checks, version increment behaviorkeystore.rs: v3 creation, unlock, v2.1→v3 migration, update_did_document()sync.rs: IdentityUpdateMessage serialization, cache apply_event(), version ordering
Integration tests (2 in icn-identity/tests/):
identity_sync_test.rs: End-to-end workflow (Alice adds device → Bob caches → Alice revokes → Bob verifies)- Version ordering and conflict resolution
Doc tests: 1 in sync.rs
Test results: All 34 tests pass in icn-identity (31 unit + 2 integration + 1 doc).
Security Considerations
1. Capability Enforcement
Every device operation checks capabilities:
pub fn can_add_device(&self, device_id: &str) -> bool {
self.verification_method.iter().any(|vm| {
vm.id == device_id
&& vm.revoked_at.is_none()
&& vm.capabilities.contains(&Capability::AddDevice)
})
}
2. Revocation Semantics
Soft deletion: Revoked devices remain in DID Document with revoked_at timestamp.
Rationale:
- Preserves audit trail
- Prevents ID reuse
- Enables forensic analysis
Verification:
pub fn can_sign(&self, did: &Did, public_key_bytes: &[u8]) -> bool {
// Check if key exists, matches DID, has Sign capability, NOT revoked
}
3. Version Ordering
Prevents replay attacks: Older versions are rejected.
Implementation:
if update.new_version <= cached.version {
return Ok(false); // Stale update
}
Caveat: This assumes monotonic version increments. Concurrent updates from multiple devices could cause conflicts. Future work: CRDTs or vector clocks.
4. Signature Verification
Current limitation: DidDocumentCache.apply_event() accepts events without verifying signatures.
Justification: Phase 11 focuses on data structures and storage. Signature verification will be added when integrating with supervisor/gossip in production.
TODO: Before production deployment:
pub fn apply_event(&self, did: &Did, event: &RotationEvent, signature: &[u8]) -> Result<bool> {
// Verify signature with authorized key
// Then apply event
}
Gossip Integration
Topic: identity:updates
Access control: Public (all nodes can subscribe)
Message flow:
- Device updates DID Document locally
- Creates
IdentityUpdateMessagewith RotationEvent - Serializes to bytes (bincode)
- Publishes to gossip topic
- Peers receive, deserialize, apply to cache
- Cache validates version ordering
Bandwidth: ~280 bytes per device operation (add/revoke/rotate).
Frequency: Low (human-driven events, not high-frequency data).
Future optimization: Batch multiple rotation events into single message.
CLI User Experience
Device List
$ icnctl device list
Current DID: did:icn:z6Mk...
Devices:
[✓] device-abc123 (laptop-primary)
Added: 2025-01-14 10:23:45 UTC
Capabilities: Sign, AddDevice, RevokeDevice, RotateKey, Recover, Encrypt
Status: Active
[✓] device-xyz789 (phone)
Added: 2025-01-14 11:45:22 UTC
Capabilities: Sign, Encrypt
Status: Active
[✗] device-old456 (old-laptop)
Added: 2025-01-10 08:12:33 UTC
Revoked: 2025-01-14 12:05:10 UTC
Reason: Device lost
Device Add
$ icnctl device add phone
Enter the DID to add this device to: did:icn:z6Mk...
Generated device keys:
Device ID: device-xyz789
Ed25519 Public Key: abc123...
X25519 Public Key: def456...
Device add request saved to: device-xyz789-request.json
Next steps:
1. Transfer this request file to an authorized device
2. On authorized device, run: icnctl device approve device-xyz789-request.json
3. Return to this device and run: icnctl id import <keystore>
Device Approve
$ icnctl device approve phone-request.json
Loaded device add request:
DID: did:icn:z6Mk...
Label: phone
Capabilities: Sign, Encrypt
Approve this device? [y/N]: y
✅ Device approved and DID Document updated
✅ Identity update broadcasted to network
Device Revoke
$ icnctl device revoke device-old456 --reason "Device lost"
Revoking device: device-old456 (old-laptop)
Reason: Device lost
⚠️ This device will no longer be able to sign transactions.
Proceed? [y/N]: y
✅ Device revoked
✅ DID Document updated (version 5)
✅ Identity update broadcasted to network
Deferred Work
11.5.4: Supervisor Integration
Status: Deferred to production deployment
Rationale:
- Core infrastructure complete
- Integration requires broader system changes
- Not blocking for MVP testing
Work required:
- Wire
DidDocumentCacheinto supervisor - Subscribe to
identity:updatestopic - Apply rotation events to cache
- Use cache for signature verification in NetworkActor
11.6: Social Recovery
Status: Deferred (not critical for MVP)
Rationale:
- Complex trust graph integration
- Requires governance mechanisms
- Can be added post-MVP
Design sketch:
pub struct RecoveryConfig {
pub recovery_method: RecoveryMethod,
pub threshold: u8,
pub guardians: Vec<Did>,
}
pub enum RecoveryMethod {
SocialRecovery { threshold: u8, guardians: Vec<Did> },
RecoveryPhrase { hash: Vec<u8> },
MultiSig { threshold: u8, keys: Vec<Vec<u8>> },
}
What's Next
Phase 11 is complete. See ROADMAP.md for next phases:
- Phase 12: Operational Hardening (monitoring, backups, disaster recovery)
- Phase 13: Economic Safety Rails (dynamic credit limits, dispute resolution)
- Phase 14: Governance Primitives v1 (driven by pilot community)
Commits
3b712f9- feat: Implement multi-device identity core (DID Document v2, Keystore v3)c405c23- feat: Implement identity sync protocol via gossip085c59e- feat: Add device management CLI commands857eec3- docs: Add multi-device identity design doc and roadmap0247acc- docs: Add Phase 11 dev journal and update CLAUDE.mdc705831- fix: Correct DID Document version increment in device approval