Initialize repository snapshot
This commit is contained in:
11
vaultmesh-identity/Cargo.toml
Normal file
11
vaultmesh-identity/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "vaultmesh-identity"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
vaultmesh-core = { path = "../vaultmesh-core" }
|
||||
284
vaultmesh-identity/src/lib.rs
Normal file
284
vaultmesh-identity/src/lib.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
//! vaultmesh-identity - Identity engine for DID management and receipts.
|
||||
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use vaultmesh_core::{
|
||||
Did,
|
||||
DidType,
|
||||
Receipt,
|
||||
ReceiptHeader,
|
||||
ReceiptMeta,
|
||||
Scroll,
|
||||
VmHash,
|
||||
};
|
||||
|
||||
/// DID Document based on W3C DID Core with a VaultMesh extension namespace.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DidDocument {
|
||||
#[serde(rename = "@context")]
|
||||
pub context: Vec<String>,
|
||||
pub id: Did,
|
||||
pub controller: Option<Did>,
|
||||
#[serde(rename = "verificationMethod")]
|
||||
pub verification_method: Vec<VerificationMethod>,
|
||||
pub authentication: Vec<String>,
|
||||
#[serde(rename = "assertionMethod")]
|
||||
pub assertion_method: Vec<String>,
|
||||
pub service: Vec<Service>,
|
||||
/// Optional human-facing display name.
|
||||
pub display_name: Option<String>,
|
||||
/// Optional role / purpose (e.g. "portal", "guardian", "skill", "human").
|
||||
pub role: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VerificationMethod {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub method_type: String,
|
||||
pub controller: Did,
|
||||
#[serde(rename = "publicKeyMultibase")]
|
||||
pub public_key_multibase: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Service {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub service_type: String,
|
||||
#[serde(rename = "serviceEndpoint")]
|
||||
pub service_endpoint: String,
|
||||
}
|
||||
|
||||
/// Receipt body for `identity_did_create` events.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DidCreateReceipt {
|
||||
/// Newly created DID.
|
||||
pub did: Did,
|
||||
/// DID type as string (e.g. "portal", "guardian", "human").
|
||||
pub did_type: String,
|
||||
/// Optional controller DID.
|
||||
pub controller: Option<Did>,
|
||||
/// Actor who created this DID.
|
||||
pub created_by: Did,
|
||||
/// Human-friendly label.
|
||||
pub display_name: Option<String>,
|
||||
/// Logical role (portal, guardian, skill, human, etc.).
|
||||
pub role: Option<String>,
|
||||
/// Verification key type (e.g. Ed25519VerificationKey2020).
|
||||
pub public_key_type: String,
|
||||
/// Multibase-encoded public key.
|
||||
pub public_key_multibase: String,
|
||||
/// IDs of initial keys attached to the DID document.
|
||||
pub initial_keys: Vec<String>,
|
||||
/// Hash of the full DID Document JSON (blake3:...).
|
||||
pub did_document_hash: String,
|
||||
}
|
||||
|
||||
/// Minimal Identity engine: manages DIDs + emits receipts.
|
||||
pub struct IdentityEngine {
|
||||
did_documents: HashMap<Did, DidDocument>,
|
||||
}
|
||||
|
||||
impl Default for IdentityEngine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl IdentityEngine {
|
||||
/// Create a new, empty IdentityEngine.
|
||||
pub fn new() -> Self {
|
||||
IdentityEngine {
|
||||
did_documents: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a DID with a single Ed25519 key and emit an `identity_did_create` receipt.
|
||||
///
|
||||
/// This does NOT persist anything to disk; it just returns the receipt +
|
||||
/// DID Document so the caller (CLI / service) can:
|
||||
/// - store the keypair safely
|
||||
/// - append the receipt to `receipts/identity/identity_events.jsonl`
|
||||
pub fn create_did(
|
||||
&mut self,
|
||||
did_type: DidType,
|
||||
name: &str,
|
||||
controller: Option<Did>,
|
||||
display_name: Option<String>,
|
||||
role: Option<String>,
|
||||
public_key_multibase: String,
|
||||
created_by: Did,
|
||||
) -> Result<(Receipt<DidCreateReceipt>, DidDocument), IdentityError> {
|
||||
// did:vm:{type}:{name}
|
||||
let did = Did::new(did_type, name);
|
||||
|
||||
if self.did_documents.contains_key(&did) {
|
||||
return Err(IdentityError::DidExists);
|
||||
}
|
||||
|
||||
let key_id = format!("{}#key-1", did.as_str());
|
||||
|
||||
let doc = DidDocument {
|
||||
context: vec![
|
||||
"https://www.w3.org/ns/did/v1".to_string(),
|
||||
"https://vaultmesh.io/ns/did/v1".to_string(),
|
||||
],
|
||||
id: did.clone(),
|
||||
controller: controller.clone(),
|
||||
verification_method: vec![VerificationMethod {
|
||||
id: key_id.clone(),
|
||||
method_type: "Ed25519VerificationKey2020".to_string(),
|
||||
controller: did.clone(),
|
||||
public_key_multibase: public_key_multibase.clone(),
|
||||
}],
|
||||
authentication: vec![key_id.clone()],
|
||||
assertion_method: vec![key_id.clone()],
|
||||
service: vec![],
|
||||
display_name: display_name.clone(),
|
||||
role: role.clone(),
|
||||
};
|
||||
|
||||
// Hash of DID Document JSON for did_document_hash.
|
||||
let doc_hash = VmHash::from_json(&doc)
|
||||
.map_err(|_| IdentityError::SerializationError)?;
|
||||
|
||||
self.did_documents.insert(did.clone(), doc);
|
||||
|
||||
let receipt_body = DidCreateReceipt {
|
||||
did: did.clone(),
|
||||
did_type: did_type.as_str().to_string(),
|
||||
controller,
|
||||
created_by,
|
||||
display_name,
|
||||
role,
|
||||
public_key_type: "Ed25519VerificationKey2020".to_string(),
|
||||
public_key_multibase,
|
||||
initial_keys: vec![key_id],
|
||||
did_document_hash: doc_hash.as_str().to_string(),
|
||||
};
|
||||
|
||||
// Root hash over the receipt body.
|
||||
let root_hash = VmHash::from_json(&receipt_body)
|
||||
.map_err(|_| IdentityError::SerializationError)?;
|
||||
|
||||
let receipt = Receipt {
|
||||
header: ReceiptHeader {
|
||||
receipt_type: "identity_did_create".to_string(),
|
||||
timestamp: Utc::now(),
|
||||
root_hash: root_hash.as_str().to_string(),
|
||||
tags: vec![
|
||||
"identity".to_string(),
|
||||
"did".to_string(),
|
||||
"create".to_string(),
|
||||
receipt_body.did_type.clone(), // e.g. "portal"
|
||||
],
|
||||
},
|
||||
meta: ReceiptMeta {
|
||||
scroll: Scroll::Identity,
|
||||
sequence: 0, // to be filled by append layer
|
||||
anchor_epoch: None, // to be filled by Guardian
|
||||
proof_path: None,
|
||||
},
|
||||
body: receipt_body,
|
||||
};
|
||||
|
||||
// We just inserted it, so unwrap is safe.
|
||||
let stored_doc = self.did_documents.get(&did).unwrap().clone();
|
||||
Ok((receipt, stored_doc))
|
||||
}
|
||||
|
||||
/// Resolve a DID into its document, if present.
|
||||
pub fn resolve_did(&self, did: &Did) -> Option<&DidDocument> {
|
||||
self.did_documents.get(did)
|
||||
}
|
||||
|
||||
/// Check if a DID exists in the engine.
|
||||
pub fn has_did(&self, did: &Did) -> bool {
|
||||
self.did_documents.contains_key(did)
|
||||
}
|
||||
|
||||
/// Get all DIDs in the engine.
|
||||
pub fn list_dids(&self) -> Vec<&Did> {
|
||||
self.did_documents.keys().collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur in the Identity engine.
|
||||
#[derive(Debug)]
|
||||
pub enum IdentityError {
|
||||
DidExists,
|
||||
SerializationError,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IdentityError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
IdentityError::DidExists => write!(f, "DID already exists"),
|
||||
IdentityError::SerializationError => write!(f, "Serialization error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for IdentityError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_did() {
|
||||
let mut engine = IdentityEngine::new();
|
||||
let created_by = Did::new(DidType::Human, "karol");
|
||||
|
||||
let result = engine.create_did(
|
||||
DidType::Portal,
|
||||
"shield",
|
||||
None,
|
||||
Some("VaultMesh Auditor Portal (shield)".to_string()),
|
||||
Some("portal".to_string()),
|
||||
"z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string(),
|
||||
created_by,
|
||||
);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let (receipt, doc) = result.unwrap();
|
||||
|
||||
assert_eq!(doc.id.as_str(), "did:vm:portal:shield");
|
||||
assert_eq!(receipt.header.receipt_type, "identity_did_create");
|
||||
assert_eq!(receipt.body.did_type, "portal");
|
||||
assert!(receipt.header.tags.contains(&"portal".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_did_error() {
|
||||
let mut engine = IdentityEngine::new();
|
||||
let created_by = Did::new(DidType::Human, "karol");
|
||||
|
||||
// First creation should succeed
|
||||
let _ = engine.create_did(
|
||||
DidType::Guardian,
|
||||
"local",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"z6Mktest".to_string(),
|
||||
created_by.clone(),
|
||||
);
|
||||
|
||||
// Second creation with same DID should fail
|
||||
let result = engine.create_did(
|
||||
DidType::Guardian,
|
||||
"local",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"z6MkotherKey".to_string(),
|
||||
created_by,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(IdentityError::DidExists)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user