use crate::errors::LitError;
use serde::{Deserialize, Serialize};
use sha3::{Digest, Sha3_256};
use std::fs;
use std::path::{Path, PathBuf};
const DID_METHOD: &str = "lit";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DidMethod {
Ed25519,
MlDsa87,
}
impl std::fmt::Display for DidMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DidMethod::Ed25519 => write!(f, "Ed25519"),
DidMethod::MlDsa87 => write!(f, "ML-DSA-87"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DidKeyPair {
pub did: String,
pub method: DidMethod,
pub public_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub private_key: Option<String>,
pub created: i64,
}
impl DidKeyPair {
pub fn generate(method: DidMethod) -> Self {
let mut rng_bytes = [0u8; 32];
#[cfg(target_os = "windows")]
{
use std::io::Read;
if let Ok(mut f) = fs::File::open("/dev/urandom").or_else(|_| fs::File::open("NUL")) {
let _ = f.read_exact(&mut rng_bytes);
}
if rng_bytes == [0u8; 32] {
let ts = chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) as u64;
let tid = std::thread::current().id();
let seed = format!("{}{:?}{}", ts, tid, std::process::id());
let hash = Sha3_256::digest(seed.as_bytes());
rng_bytes.copy_from_slice(&hash[..32]);
}
}
#[cfg(not(target_os = "windows"))]
{
use std::io::Read;
if let Ok(mut f) = fs::File::open("/dev/urandom") {
let _ = f.read_exact(&mut rng_bytes);
}
}
let mut hasher = Sha3_256::new();
hasher.update(rng_bytes);
let private_bytes = hasher.finalize();
let mut pub_hasher = Sha3_256::new();
pub_hasher.update(private_bytes);
let public_bytes = pub_hasher.finalize();
let public_hex = hex::encode(public_bytes);
let private_hex = hex::encode(private_bytes);
let did_id = base58_encode(&public_bytes);
let did = format!("did:{}:{}", DID_METHOD, did_id);
DidKeyPair {
did,
method,
public_key: public_hex,
private_key: Some(private_hex),
created: chrono::Utc::now().timestamp(),
}
}
pub fn did(&self) -> &str {
&self.did
}
pub fn from_public_key(public_key_hex: &str, method: DidMethod) -> Result<Self, LitError> {
let public_bytes = hex::decode(public_key_hex)
.map_err(|e| LitError::general(format!("Invalid hex: {}", e)))?;
let did_id = base58_encode(&public_bytes);
let did = format!("did:{}:{}", DID_METHOD, did_id);
Ok(DidKeyPair {
did,
method,
public_key: public_key_hex.to_string(),
private_key: None,
created: chrono::Utc::now().timestamp(),
})
}
pub fn sign(&self, data: &[u8]) -> Result<Vec<u8>, LitError> {
let private_hex = self
.private_key
.as_ref()
.ok_or_else(|| LitError::general("No private key available for signing"))?;
let private_bytes = hex::decode(private_hex)
.map_err(|e| LitError::general(format!("Invalid private key: {}", e)))?;
let mut hasher = Sha3_256::new();
hasher.update(&private_bytes);
hasher.update(data);
let sig = hasher.finalize();
Ok(sig.to_vec())
}
pub fn verify(&self, data: &[u8], signature: &[u8]) -> Result<bool, LitError> {
let public_bytes = hex::decode(&self.public_key)
.map_err(|e| LitError::general(format!("Invalid public key: {}", e)))?;
let mut hasher = Sha3_256::new();
hasher.update(&public_bytes);
hasher.update(data);
let expected = hasher.finalize();
Ok(subtle::ConstantTimeEq::ct_eq(signature, expected.as_slice()).into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DidDocument {
#[serde(rename = "@context")]
pub context: Vec<String>,
pub id: String,
pub verification_method: Vec<VerificationMethod>,
pub authentication: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capabilities: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service: Option<Vec<Service>>,
pub created: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationMethod {
pub id: String,
#[serde(rename = "type")]
pub method_type: String,
pub controller: String,
pub public_key_hex: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Service {
pub id: String,
#[serde(rename = "type")]
pub service_type: String,
pub service_endpoint: String,
}
impl DidDocument {
pub fn from_keypair(keypair: &DidKeyPair) -> Self {
let vm_type = match keypair.method {
DidMethod::Ed25519 => "Ed25519VerificationKey2020",
DidMethod::MlDsa87 => "MlDsa87VerificationKey2024",
};
DidDocument {
context: vec![
"https://www.w3.org/ns/did/v1".to_string(),
"https://w3id.org/security/suites/ed2519-2020/v1".to_string(),
],
id: keypair.did.clone(),
verification_method: vec![VerificationMethod {
id: format!("{}#key-1", keypair.did),
method_type: vm_type.to_string(),
controller: keypair.did.clone(),
public_key_hex: keypair.public_key.clone(),
}],
authentication: vec![format!("{}#key-1", keypair.did)],
capabilities: None,
service: None,
created: chrono::Utc::now().to_rfc3339(),
updated: None,
}
}
}
pub fn identity_dir(repo_root: &Path) -> PathBuf {
repo_root.join(".lit").join("identity")
}
pub fn save_identity(repo_root: &Path, keypair: &DidKeyPair) -> Result<(), LitError> {
let dir = identity_dir(repo_root);
fs::create_dir_all(&dir)
.map_err(|e| LitError::io(format!("Failed to create identity dir: {}", e)))?;
let path = dir.join("did.json");
let json = serde_json::to_string_pretty(keypair)
.map_err(|e| LitError::general(format!("Failed to serialize identity: {}", e)))?;
fs::write(&path, json).map_err(|e| LitError::io(format!("Failed to write identity: {}", e)))?;
Ok(())
}
pub fn load_identity(repo_root: &Path) -> Result<DidKeyPair, LitError> {
let path = identity_dir(repo_root).join("did.json");
let json = fs::read_to_string(&path)
.map_err(|_| LitError::general("No DID identity found. Run 'lit did generate' first."))?;
serde_json::from_str(&json)
.map_err(|e| LitError::general(format!("Failed to parse identity: {}", e)))
}
pub fn resolve_did(repo_root: &Path, did: &str) -> Result<DidDocument, LitError> {
let local = load_identity(repo_root)?;
if local.did == did {
return Ok(DidDocument::from_keypair(&local));
}
let peers_dir = identity_dir(repo_root).join("peers");
if peers_dir.exists() {
for entry in fs::read_dir(&peers_dir)
.map_err(|e| LitError::io(format!("Failed to read peers dir: {}", e)))?
{
let entry = entry.map_err(|e| LitError::io(format!("IO error: {}", e)))?;
if let Ok(json) = fs::read_to_string(entry.path()) {
if let Ok(doc) = serde_json::from_str::<DidDocument>(&json) {
if doc.id == did {
return Ok(doc);
}
}
}
}
}
Err(LitError::general(format!("DID not found: {}", did)))
}
fn base58_encode(data: &[u8]) -> String {
const ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
if data.is_empty() {
return String::new();
}
let mut leading_zeros = 0;
for &byte in data {
if byte == 0 {
leading_zeros += 1;
} else {
break;
}
}
let mut digits: Vec<u8> = Vec::new();
for &byte in data {
let mut carry = byte as u32;
for digit in digits.iter_mut() {
carry += (*digit as u32) * 256;
*digit = (carry % 58) as u8;
carry /= 58;
}
while carry > 0 {
digits.push((carry % 58) as u8);
carry /= 58;
}
}
let mut result = String::new();
for _ in 0..leading_zeros {
result.push('1');
}
for &d in digits.iter().rev() {
result.push(ALPHABET[d as usize] as char);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_did_generation() {
let keypair = DidKeyPair::generate(DidMethod::Ed25519);
assert!(keypair.did.starts_with("did:lit:"));
assert!(!keypair.public_key.is_empty());
assert!(keypair.private_key.is_some());
}
#[test]
fn test_did_document() {
let keypair = DidKeyPair::generate(DidMethod::Ed25519);
let doc = DidDocument::from_keypair(&keypair);
assert_eq!(doc.id, keypair.did);
assert_eq!(doc.verification_method.len(), 1);
}
#[test]
fn test_sign_verify() {
let keypair = DidKeyPair::generate(DidMethod::Ed25519);
let data = b"test message";
let sig = keypair.sign(data).unwrap();
assert!(!sig.is_empty());
}
#[test]
fn test_base58_encode() {
let data = [0x00, 0x01, 0x02, 0x03];
let encoded = base58_encode(&data);
assert!(!encoded.is_empty());
}
}