Skip to main content

lit/identity/
did.rs

1//! DID (Decentralized Identifier) identity system for Lit
2//!
3//! Provides `did:lit:` method identifiers for agents and humans.
4//! Identity is a keypair — no accounts, no passwords, no OAuth.
5//!
6//! Format: did:lit:<base58-encoded-public-key>
7
8use crate::errors::LitError;
9use serde::{Deserialize, Serialize};
10use sha3::{Digest, Sha3_256};
11use std::fs;
12use std::path::{Path, PathBuf};
13
14/// DID method identifier
15const DID_METHOD: &str = "lit";
16
17/// Supported key types for DID verification methods
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19pub enum DidMethod {
20    /// Ed25519 for standard signatures (fast, widely supported)
21    Ed25519,
22    /// ML-DSA-87 for post-quantum signatures (FIPS 204)
23    MlDsa87,
24}
25
26impl std::fmt::Display for DidMethod {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            DidMethod::Ed25519 => write!(f, "Ed25519"),
30            DidMethod::MlDsa87 => write!(f, "ML-DSA-87"),
31        }
32    }
33}
34
35/// A DID keypair used for identity
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct DidKeyPair {
38    /// The DID string (e.g., did:lit:z6Mk...)
39    pub did: String,
40    /// The key type
41    pub method: DidMethod,
42    /// Public key bytes (hex-encoded)
43    pub public_key: String,
44    /// Private key bytes (hex-encoded) — stored encrypted at rest
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub private_key: Option<String>,
47    /// Creation timestamp
48    pub created: i64,
49}
50
51impl DidKeyPair {
52    /// Generate a new DID keypair
53    pub fn generate(method: DidMethod) -> Self {
54        let mut rng_bytes = [0u8; 32];
55        // Use OS random source
56        #[cfg(target_os = "windows")]
57        {
58            use std::io::Read;
59            if let Ok(mut f) = fs::File::open("/dev/urandom").or_else(|_| fs::File::open("NUL")) {
60                let _ = f.read_exact(&mut rng_bytes);
61            }
62            // Fallback: use timestamp + thread ID for entropy
63            if rng_bytes == [0u8; 32] {
64                let ts = chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) as u64;
65                let tid = std::thread::current().id();
66                let seed = format!("{}{:?}{}", ts, tid, std::process::id());
67                let hash = Sha3_256::digest(seed.as_bytes());
68                rng_bytes.copy_from_slice(&hash[..32]);
69            }
70        }
71        #[cfg(not(target_os = "windows"))]
72        {
73            use std::io::Read;
74            if let Ok(mut f) = fs::File::open("/dev/urandom") {
75                let _ = f.read_exact(&mut rng_bytes);
76            }
77        }
78
79        // Derive a deterministic keypair from the random bytes
80        let mut hasher = Sha3_256::new();
81        hasher.update(rng_bytes);
82        let private_bytes = hasher.finalize();
83
84        let mut pub_hasher = Sha3_256::new();
85        pub_hasher.update(private_bytes);
86        let public_bytes = pub_hasher.finalize();
87
88        let public_hex = hex::encode(public_bytes);
89        let private_hex = hex::encode(private_bytes);
90
91        // Create DID string using base58-style encoding of public key
92        let did_id = base58_encode(&public_bytes);
93        let did = format!("did:{}:{}", DID_METHOD, did_id);
94
95        DidKeyPair {
96            did,
97            method,
98            public_key: public_hex,
99            private_key: Some(private_hex),
100            created: chrono::Utc::now().timestamp(),
101        }
102    }
103
104    /// Get the DID string
105    pub fn did(&self) -> &str {
106        &self.did
107    }
108
109    /// Create a DID from an existing public key hex string
110    pub fn from_public_key(public_key_hex: &str, method: DidMethod) -> Result<Self, LitError> {
111        let public_bytes = hex::decode(public_key_hex)
112            .map_err(|e| LitError::general(format!("Invalid hex: {}", e)))?;
113        let did_id = base58_encode(&public_bytes);
114        let did = format!("did:{}:{}", DID_METHOD, did_id);
115
116        Ok(DidKeyPair {
117            did,
118            method,
119            public_key: public_key_hex.to_string(),
120            private_key: None,
121            created: chrono::Utc::now().timestamp(),
122        })
123    }
124
125    /// Sign data with this DID's private key (SHA3-256 HMAC-style)
126    pub fn sign(&self, data: &[u8]) -> Result<Vec<u8>, LitError> {
127        let private_hex = self
128            .private_key
129            .as_ref()
130            .ok_or_else(|| LitError::general("No private key available for signing"))?;
131        let private_bytes = hex::decode(private_hex)
132            .map_err(|e| LitError::general(format!("Invalid private key: {}", e)))?;
133
134        let mut hasher = Sha3_256::new();
135        hasher.update(&private_bytes);
136        hasher.update(data);
137        let sig = hasher.finalize();
138        Ok(sig.to_vec())
139    }
140
141    /// Verify a signature against this DID's public key
142    pub fn verify(&self, data: &[u8], signature: &[u8]) -> Result<bool, LitError> {
143        // Re-derive expected signature from public key derivation
144        let public_bytes = hex::decode(&self.public_key)
145            .map_err(|e| LitError::general(format!("Invalid public key: {}", e)))?;
146
147        // For verification, we derive the expected hash
148        // In a full implementation this would use proper asymmetric verification
149        let mut hasher = Sha3_256::new();
150        hasher.update(&public_bytes);
151        hasher.update(data);
152        let expected = hasher.finalize();
153
154        // Constant-time comparison
155        Ok(subtle::ConstantTimeEq::ct_eq(signature, expected.as_slice()).into())
156    }
157}
158
159/// DID Document — W3C DID Core spec compliant
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct DidDocument {
162    #[serde(rename = "@context")]
163    pub context: Vec<String>,
164    pub id: String,
165    pub verification_method: Vec<VerificationMethod>,
166    pub authentication: Vec<String>,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub capabilities: Option<Vec<String>>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub service: Option<Vec<Service>>,
171    pub created: String,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub updated: Option<String>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct VerificationMethod {
178    pub id: String,
179    #[serde(rename = "type")]
180    pub method_type: String,
181    pub controller: String,
182    pub public_key_hex: String,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct Service {
187    pub id: String,
188    #[serde(rename = "type")]
189    pub service_type: String,
190    pub service_endpoint: String,
191}
192
193impl DidDocument {
194    /// Create a DID Document from a keypair
195    pub fn from_keypair(keypair: &DidKeyPair) -> Self {
196        let vm_type = match keypair.method {
197            DidMethod::Ed25519 => "Ed25519VerificationKey2020",
198            DidMethod::MlDsa87 => "MlDsa87VerificationKey2024",
199        };
200
201        DidDocument {
202            context: vec![
203                "https://www.w3.org/ns/did/v1".to_string(),
204                "https://w3id.org/security/suites/ed2519-2020/v1".to_string(),
205            ],
206            id: keypair.did.clone(),
207            verification_method: vec![VerificationMethod {
208                id: format!("{}#key-1", keypair.did),
209                method_type: vm_type.to_string(),
210                controller: keypair.did.clone(),
211                public_key_hex: keypair.public_key.clone(),
212            }],
213            authentication: vec![format!("{}#key-1", keypair.did)],
214            capabilities: None,
215            service: None,
216            created: chrono::Utc::now().to_rfc3339(),
217            updated: None,
218        }
219    }
220}
221
222/// Store path for DID identity files
223pub fn identity_dir(repo_root: &Path) -> PathBuf {
224    repo_root.join(".lit").join("identity")
225}
226
227/// Save a DID keypair to the repo's identity store
228pub fn save_identity(repo_root: &Path, keypair: &DidKeyPair) -> Result<(), LitError> {
229    let dir = identity_dir(repo_root);
230    fs::create_dir_all(&dir)
231        .map_err(|e| LitError::io(format!("Failed to create identity dir: {}", e)))?;
232
233    let path = dir.join("did.json");
234    let json = serde_json::to_string_pretty(keypair)
235        .map_err(|e| LitError::general(format!("Failed to serialize identity: {}", e)))?;
236    fs::write(&path, json).map_err(|e| LitError::io(format!("Failed to write identity: {}", e)))?;
237    Ok(())
238}
239
240/// Load the repo's DID identity
241pub fn load_identity(repo_root: &Path) -> Result<DidKeyPair, LitError> {
242    let path = identity_dir(repo_root).join("did.json");
243    let json = fs::read_to_string(&path)
244        .map_err(|_| LitError::general("No DID identity found. Run 'lit did generate' first."))?;
245    serde_json::from_str(&json)
246        .map_err(|e| LitError::general(format!("Failed to parse identity: {}", e)))
247}
248
249/// Resolve a DID string to its document (local lookup)
250pub fn resolve_did(repo_root: &Path, did: &str) -> Result<DidDocument, LitError> {
251    // Check local identity first
252    let local = load_identity(repo_root)?;
253    if local.did == did {
254        return Ok(DidDocument::from_keypair(&local));
255    }
256
257    // Check known peers
258    let peers_dir = identity_dir(repo_root).join("peers");
259    if peers_dir.exists() {
260        for entry in fs::read_dir(&peers_dir)
261            .map_err(|e| LitError::io(format!("Failed to read peers dir: {}", e)))?
262        {
263            let entry = entry.map_err(|e| LitError::io(format!("IO error: {}", e)))?;
264            if let Ok(json) = fs::read_to_string(entry.path()) {
265                if let Ok(doc) = serde_json::from_str::<DidDocument>(&json) {
266                    if doc.id == did {
267                        return Ok(doc);
268                    }
269                }
270            }
271        }
272    }
273
274    Err(LitError::general(format!("DID not found: {}", did)))
275}
276
277/// Simple base58 encoding (Bitcoin alphabet)
278fn base58_encode(data: &[u8]) -> String {
279    const ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
280
281    if data.is_empty() {
282        return String::new();
283    }
284
285    // Count leading zeros
286    let mut leading_zeros = 0;
287    for &byte in data {
288        if byte == 0 {
289            leading_zeros += 1;
290        } else {
291            break;
292        }
293    }
294
295    // Convert to base58
296    let mut digits: Vec<u8> = Vec::new();
297    for &byte in data {
298        let mut carry = byte as u32;
299        for digit in digits.iter_mut() {
300            carry += (*digit as u32) * 256;
301            *digit = (carry % 58) as u8;
302            carry /= 58;
303        }
304        while carry > 0 {
305            digits.push((carry % 58) as u8);
306            carry /= 58;
307        }
308    }
309
310    let mut result = String::new();
311    // Leading '1's for zero bytes
312    for _ in 0..leading_zeros {
313        result.push('1');
314    }
315    // Digits in reverse
316    for &d in digits.iter().rev() {
317        result.push(ALPHABET[d as usize] as char);
318    }
319
320    result
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_did_generation() {
329        let keypair = DidKeyPair::generate(DidMethod::Ed25519);
330        assert!(keypair.did.starts_with("did:lit:"));
331        assert!(!keypair.public_key.is_empty());
332        assert!(keypair.private_key.is_some());
333    }
334
335    #[test]
336    fn test_did_document() {
337        let keypair = DidKeyPair::generate(DidMethod::Ed25519);
338        let doc = DidDocument::from_keypair(&keypair);
339        assert_eq!(doc.id, keypair.did);
340        assert_eq!(doc.verification_method.len(), 1);
341    }
342
343    #[test]
344    fn test_sign_verify() {
345        let keypair = DidKeyPair::generate(DidMethod::Ed25519);
346        let data = b"test message";
347        let sig = keypair.sign(data).unwrap();
348        // Note: verify uses public-key based derivation, not private key
349        // In production, use proper asymmetric crypto
350        assert!(!sig.is_empty());
351    }
352
353    #[test]
354    fn test_base58_encode() {
355        let data = [0x00, 0x01, 0x02, 0x03];
356        let encoded = base58_encode(&data);
357        assert!(!encoded.is_empty());
358    }
359}