Multi-Device Identity Design (Phase 11)
Status: Design Phase Started: 2025-01-13 Target: 3-4 weeks to implementation complete
Problem Statement
Current State: ICN identity = single Ed25519 keypair stored in Age-encrypted keystore.
Failure Modes:
- Device dies → lose entire economic history
- Device stolen → must rotate key, invalidate all trust edges
- Multiple devices → can't use ICN on phone + laptop
- Key compromise → no recovery path
Real-World Blocker: No community will trust their economic coordination to a system where hardware failure = total loss.
Design Goals
- Multi-Device Support: Same DID usable from multiple devices
- Key Rotation: Clean rotation without breaking trust/ledger history
- Recovery: Survive total device loss via social recovery or backup
- Backward Compatible: Migrate existing v2.1 keystores without data loss
- Security: No single point of failure, clear revocation semantics
Architecture Overview
Core Components
┌─────────────────────────────────────────────────────────────┐
│ DID Document v2 │
│ (Canonical list of authorized keys + capabilities) │
└─────────────────────────────────────────────────────────────┘
│
│ references
▼
┌─────────────────────────────────────────────────────────────┐
│ Verification Methods │
│ - device-1: Ed25519 key (laptop) [sign, rotate, recover] │
│ - device-2: Ed25519 key (phone) [sign] │
│ - device-3: X25519 key (encryption) │
└─────────────────────────────────────────────────────────────┘
│
│ stored in
▼
┌─────────────────────────────────────────────────────────────┐
│ Keystore v3 Format │
│ - Primary identity bundle (device-specific) │
│ - DID Document (shared identity state) │
│ - Rotation chain (audit log) │
└─────────────────────────────────────────────────────────────┘
│
│ synced via
▼
┌─────────────────────────────────────────────────────────────┐
│ Identity Sync Protocol (NEW) │
│ - Gossip topic: "identity:updates" │
│ - Broadcasts: DID Document changes, rotation events │
│ - Trust-gated: Only visible to existing trust edges │
└─────────────────────────────────────────────────────────────┘
Data Structures
DID Document v2
/// DID Document version 2: Multi-device identity
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DidDocument {
/// The DID this document describes (did:icn:base58-pubkey)
/// This is the *original* keypair's DID, remains stable across rotations
pub id: DID,
/// Version number (incremented on each update)
pub version: u64,
/// Timestamp of last update
pub updated_at: u64,
/// List of authorized verification methods (keys)
pub verification_method: Vec<VerificationMethod>,
/// Which keys can authenticate (sign messages)
pub authentication: Vec<String>, // refs to verification_method IDs
/// Optional recovery configuration
pub recovery: Option<RecoveryConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationMethod {
/// Unique ID within this DID Document (e.g., "device-1", "device-2")
pub id: String,
/// Human-readable label
pub label: String, // e.g., "Matt's Laptop", "Phone"
/// Key type
pub key_type: KeyType,
/// The actual public key
pub public_key: PublicKey,
/// What this key is authorized to do
pub capabilities: Vec<Capability>,
/// When this key was added
pub added_at: u64,
/// Optional: When this key was revoked
pub revoked_at: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KeyType {
Ed25519, // Signing
X25519, // Encryption
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Capability {
/// Can sign messages on behalf of this DID
Sign,
/// Can add new devices
AddDevice,
/// Can revoke devices (including self)
RevokeDevice,
/// Can rotate keys
RotateKey,
/// Can participate in recovery
Recover,
/// Can encrypt/decrypt messages
Encrypt,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecoveryConfig {
pub method: RecoveryMethod,
pub threshold: u8,
pub trustees: Vec<DID>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RecoveryMethod {
/// M-of-N social recovery
Social { m: u8, n: u8 },
/// Encrypted backup seed (offline)
BackupSeed,
/// No recovery (accept total loss risk)
None,
}
Key Rotation Event
/// Represents a key rotation or device change event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RotationEvent {
/// The DID being rotated
pub did: DID,
/// Event type
pub event_type: RotationEventType,
/// Signature by the authorized key performing this action
pub proof: Signature,
/// Which key signed this (must have RotateKey or RevokeDevice capability)
pub signed_by: String, // verification method ID
/// Timestamp
pub timestamp: u64,
/// New DID Document version
pub new_version: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RotationEventType {
/// Add a new device
AddDevice {
device_id: String,
public_key: PublicKey,
capabilities: Vec<Capability>,
},
/// Revoke a device
RevokeDevice {
device_id: String,
reason: RevocationReason,
},
/// Rotate a key (change the underlying keypair for a device)
RotateKey {
device_id: String,
old_key: PublicKey,
new_key: PublicKey,
},
/// Full recovery (new root key after total loss)
Recover {
new_root_key: PublicKey,
recovery_proofs: Vec<RecoveryProof>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RevocationReason {
/// Normal removal
Removed,
/// Device compromised
Compromised,
/// Device lost/stolen
Lost,
/// Key rotation
Rotated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecoveryProof {
/// Trustee DID
pub trustee: DID,
/// Trustee's signature on recovery request
pub signature: Signature,
/// Timestamp
pub timestamp: u64,
}
Keystore v3 Format
/// Keystore version 3: Multi-device with DID Document
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeystoreV3 {
/// Format version
pub version: u8, // = 3
/// This device's identity bundle
pub identity: IdentityBundle,
/// The canonical DID Document (shared across devices)
pub did_document: DidDocument,
/// This device's ID in the DID Document
pub device_id: String,
/// Rotation event history (audit log)
pub rotation_chain: Vec<RotationEvent>,
}
Key Workflows
1. Initial Setup (Fresh Identity)
User runs: icnctl id init --device-name "Laptop"
1. Generate Ed25519 keypair (primary)
2. Generate X25519 keypair (encryption)
3. Create DID from Ed25519 pubkey: did:icn:abc123
4. Create DID Document v2:
- verification_method = [
{ id: "device-1", label: "Laptop", key_type: Ed25519, capabilities: [Sign, AddDevice, RevokeDevice, RotateKey, Recover] },
{ id: "enc-1", label: "Encryption Key", key_type: X25519, capabilities: [Encrypt] }
]
- authentication = ["device-1"]
- recovery = None (default, can configure later)
5. Create keystore v3
6. Encrypt and save to {data_dir}/identity.age
Output: User has a working identity, ready to add more devices.
2. Add a Second Device
User runs on NEW device: icnctl device add --name "Phone"
Flow:
1. Generate new Ed25519 keypair on phone
2. Create AddDevice request:
{
device_id: "device-2",
public_key: <phone's pubkey>,
capabilities: [Sign, Encrypt] // Note: NOT AddDevice/RevokeDevice (limited trust)
}
3. Export request as QR code or file
4. On EXISTING device (laptop):
icnctl device approve <request-file>
- Verifies request
- Updates DID Document (adds device-2)
- Signs RotationEvent with device-1's key
- Broadcasts update to network via gossip
5. Phone receives updated DID Document
6. Phone creates its own keystore v3 with:
- New identity bundle (phone's keys)
- Shared DID Document
- device_id = "device-2"
Result: Both laptop and phone can sign as the same DID.
3. Revoke a Device
User runs: icnctl device revoke device-2 --reason "Lost phone"
1. Check: Does current device have RevokeDevice capability? (Yes, device-1 does)
2. Update DID Document:
- Set device-2.revoked_at = now()
3. Create RotationEvent:
{
event_type: RevokeDevice { device_id: "device-2", reason: Lost },
signed_by: "device-1",
proof: <signature by device-1>
}
4. Broadcast to network
5. All nodes see the update, stop accepting signatures from device-2
Result: Old device can no longer act as this DID.
4. Key Rotation (Proactive Security)
User runs: icnctl key rotate --device device-1
1. Generate new Ed25519 keypair
2. Create RotationEvent:
{
event_type: RotateKey {
device_id: "device-1",
old_key: <current pubkey>,
new_key: <new pubkey>
},
signed_by: "device-1",
proof: <signed by OLD key>
}
3. Update DID Document:
- Replace device-1's public_key with new key
- Increment version
4. Broadcast to network
5. Update local keystore with new keypair
Result: Underlying key changed, but DID and trust/ledger history intact.
5. Social Recovery (Total Device Loss)
Setup (one-time):
icnctl recovery setup --method social --threshold 3 --trustees did:icn:alice,did:icn:bob,did:icn:carol
User loses ALL devices, gets a new one:
icnctl recovery initiate --did did:icn:abc123
1. Generate new Ed25519 keypair on new device
2. Create recovery request:
{
did: did:icn:abc123,
new_root_key: <new device's pubkey>,
requested_at: now()
}
3. Send recovery request to trustees (out-of-band: email, Signal, in-person)
4. Each trustee runs:
icnctl recovery approve <request> --trustee-key <their key>
- Verifies identity (out-of-band: "Is this really Matt?")
- Signs recovery proof
5. Once M-of-N signatures collected, user runs:
icnctl recovery finalize <recovery-proofs>
- Creates RotationEvent with Recover type
- Includes all recovery proofs
- Broadcasts to network
6. Network validates:
- Check M-of-N threshold met
- Verify trustee signatures
- Accept new root key
7. New DID Document:
- Old devices revoked
- New device becomes device-1 with full capabilities
Result: User regains control of their DID without losing history.
Security Considerations
1. Capability Hierarchy
Not all devices are equal:
- Primary device (e.g., laptop): Full capabilities (AddDevice, RevokeDevice, RotateKey)
- Secondary device (e.g., phone): Limited to Sign, Encrypt
- Rationale: If phone is lost, can't be used to add malicious devices
2. Revocation Checking
Every signature verification must:
- Extract DID from signature
- Fetch current DID Document for that DID
- Check signing key is in
verification_methodAND not revoked - Check key has
Signcapability
Implementation: NetworkActor caches DID Documents, gossip keeps them synced.
3. Replay Protection Across Devices
Current ReplayGuard uses per-peer sequence numbers. With multi-device:
- Option A: Shared sequence counter across devices (requires coordination)
- Option B: Per-device sequences, check against revocation
- Recommendation: Option B (simpler, no coordination overhead)
4. Recovery Attack Prevention
Malicious actor could try to initiate fake recovery:
- Mitigation: Recovery requires M-of-N trustees to verify identity out-of-band
- Threshold: Recommend M ≥ 3, N ≥ 5 (balance security vs. availability)
- Trustee selection: Should be high-trust, long-term relationships
5. DID Document Sync
DID Documents propagate via gossip:
- Topic:
identity:updates(trust-gated, only visible to trust edges) - Conflict resolution: Highest version number wins
- Byzantine resistance: Accept updates only if:
- Signed by a key with appropriate capability
- Version increments by 1 (no skipping)
- Rotation chain is valid
Migration Path (v2.1 → v3)
Automatic Migration on First Unlock
impl Keystore {
pub fn unlock(passphrase: &[u8]) -> Result<KeystoreV3> {
let data = decrypt(passphrase)?;
match detect_version(&data)? {
2 | 21 => {
// Old format: just IdentityBundle
let bundle: IdentityBundle = deserialize(data)?;
// Create v3 keystore
let did_doc = DidDocument {
id: bundle.did.clone(),
version: 1,
updated_at: now(),
verification_method: vec![
VerificationMethod {
id: "device-1".into(),
label: "Primary Device".into(),
key_type: KeyType::Ed25519,
public_key: bundle.keypair.public.clone(),
capabilities: vec![
Capability::Sign,
Capability::AddDevice,
Capability::RevokeDevice,
Capability::RotateKey,
Capability::Recover,
],
added_at: now(),
revoked_at: None,
},
VerificationMethod {
id: "enc-1".into(),
label: "Encryption Key".into(),
key_type: KeyType::X25519,
public_key: bundle.x25519_keypair.public.clone(),
capabilities: vec![Capability::Encrypt],
added_at: now(),
revoked_at: None,
},
],
authentication: vec!["device-1".into()],
recovery: None, // User can configure later
};
let v3 = KeystoreV3 {
version: 3,
identity: bundle,
did_document: did_doc,
device_id: "device-1".into(),
rotation_chain: vec![],
};
// Save upgraded keystore immediately
v3.encrypt_and_save(passphrase)?;
info!("✅ Migrated keystore v2.1 → v3 (multi-device support enabled)");
Ok(v3)
}
3 => {
// Already v3
Ok(deserialize(data)?)
}
v => bail!("Unsupported keystore version: {}", v),
}
}
}
Guarantees:
- Existing identities work unchanged
- DID remains stable (derived from original Ed25519 key)
- Trust edges and ledger history preserved
- Automatic, invisible to user
Implementation Plan
Phase 11.1: Data Structures (Week 1)
Crate: icn-identity
- Define
DidDocument,VerificationMethod,Capabilitytypes - Define
RotationEventand related types - Define
KeystoreV3format - Implement serialization/deserialization
- Write unit tests for data structures
Deliverable: Core types compile, serialize correctly.
Phase 11.2: DID Document Management (Week 1-2)
Crate: icn-identity
- Implement
DidDocument::new()(create initial doc) - Implement
DidDocument::add_device() - Implement
DidDocument::revoke_device() - Implement
DidDocument::rotate_key() - Implement
DidDocument::verify_signature()(check key is authorized) - Implement rotation event validation
- Write tests for each operation
Deliverable: DID Document CRUD operations work.
Phase 11.3: Keystore v3 Migration (Week 2)
Crate: icn-identity
- Implement
KeystoreV3type - Implement v2.1 → v3 migration in
Keystore::unlock() - Implement
KeystoreV3::encrypt_and_save() - Test migration preserves identity, DID, keys
- Test idempotence (migrating twice is safe)
Deliverable: Existing keystores auto-upgrade to v3.
Phase 11.4: icnctl Device Commands (Week 2-3)
Crate: icnctl
-
icnctl device list- Show all devices for this DID -
icnctl device add <name>- Initiate device add flow -
icnctl device approve <request>- Approve device add -
icnctl device revoke <device-id>- Revoke a device -
icnctl key rotate- Rotate current device's key - Test workflows end-to-end
Deliverable: Users can manage devices via CLI.
Phase 11.5: Identity Sync Protocol (Week 3)
Crates: icn-gossip, icn-core
- Define gossip topic
identity:updates - Implement IdentityActor (manages local DID Documents)
- On device add/revoke/rotate: broadcast RotationEvent
- On receive RotationEvent: validate, update local DID cache
- NetworkActor checks DID Documents before accepting messages
- Test: Two nodes, add device on node1, node2 sees update
Deliverable: DID Document changes propagate across network.
Phase 11.6: Recovery Mechanisms (Week 3-4)
Crate: icn-identity, icnctl
- Implement
icnctl recovery setup - Implement
icnctl recovery initiate - Implement
icnctl recovery approve(trustee side) - Implement
icnctl recovery finalize - Test social recovery flow with 3-of-5 threshold
- Document recovery best practices
Deliverable: Users can recover from total device loss.
Phase 11.7: Integration & Testing (Week 4)
All crates
- Update Runtime to load DID Documents on startup
- Update Supervisor to sync identity changes
- End-to-end test: Multi-device gossip + ledger transactions
- End-to-end test: Device revocation prevents signing
- End-to-end test: Social recovery restores identity
- Update documentation
Deliverable: Phase 11 complete, all tests pass.
Open Questions
1. Device Add Flow: Interactive or File-Based?
Option A: Interactive (QR code)
- New device generates request, shows QR
- Existing device scans QR, approves
- Pros: Good UX, secure
- Cons: Requires camera, GUI
Option B: File-based (copy request file)
- New device writes
device-add-request.json - User copies to existing device (USB, scp, etc.)
- Existing device runs
icnctl device approve device-add-request.json - Pros: Works on servers, no GUI needed
- Cons: Manual file transfer
Recommendation: Start with Option B (simpler), add Option A later.
2. Recovery: Social vs. Backup Seed?
Social Recovery:
- Pros: No single point of failure, social trust
- Cons: Requires coordinating with trustees, can fail if trustees unavailable
Backup Seed:
- Pros: Self-sovereign, always available
- Cons: Single point of failure (if backup stolen/lost)
Recommendation: Support both, let users choose. Default: Social recovery.
3. DID Stability: What if root key rotates?
Current Design: DID = hash of original Ed25519 public key, never changes.
Implication: Root key can rotate (via RotationEvent), but DID stays stable.
Alternative: DID = hash of DID Document, changes on rotation.
- Pros: More "pure"
- Cons: Breaks trust edges, ledger references, every rotation
Recommendation: Stick with stable DID (original key). Rotation updates the mapping.
4. Capability Granularity: Per-Device or Per-Key?
Current Design: Capabilities attached to VerificationMethod (per-key).
Alternative: Capabilities at device level, keys inherit.
Recommendation: Per-key is more flexible (can have signing key + encryption key on same device with different caps).
Success Criteria
Phase 11 is complete when:
- ✅ User can add a second device to their identity
- ✅ Both devices can sign messages as the same DID
- ✅ User can revoke a compromised device
- ✅ Revoked device signatures are rejected by network
- ✅ User can recover identity after total device loss (social recovery)
- ✅ All existing tests pass (262+)
- ✅ New tests cover multi-device workflows
- ✅ Documentation updated
Target: 3-4 weeks (end of February 2025)
Next Steps (Today)
- Review this design doc - Any objections? Changes?
- Start Phase 11.1 - Implement data structures in
icn-identity - Set up test infrastructure - Multi-device test helpers in
icn-testkit
Let's build this.