use std::path::PathBuf;
use auths_crypto::Pkcs8Der;
use auths_id::error::{InitError, StorageError};
use git2::Repository;
use serde::{Deserialize, Serialize};
use auths_core::storage::keychain::IdentityDID;
use auths_id::identity::helpers::ManagedIdentity;
use auths_id::storage::identity::IdentityStorage;
use super::adapter::{GitRegistryBackend, REGISTRY_REF};
use super::config::RegistryConfig;
use super::tree_ops::{TreeMutator, TreeNavigator};
use auths_id::ports::registry::RegistryBackend;
use auths_id::storage::registry::shard::identity_path;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct StoredMetadata {
version: u32,
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<serde_json::Value>,
}
impl StoredMetadata {
const CURRENT_VERSION: u32 = 1;
fn new(metadata: Option<serde_json::Value>) -> Self {
Self {
version: Self::CURRENT_VERSION,
metadata,
}
}
}
pub struct RegistryIdentityStorage {
repo_path: PathBuf,
backend: GitRegistryBackend,
}
impl RegistryIdentityStorage {
pub fn new(repo_path: impl Into<PathBuf>) -> Self {
let repo_path = repo_path.into();
let backend =
GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(&repo_path));
Self { repo_path, backend }
}
pub fn initialize_identity(
&self,
metadata: Option<serde_json::Value>,
witness_config: Option<&auths_id::witness_config::WitnessConfig>,
) -> Result<(String, auths_id::keri::InceptionResult), InitError> {
use auths_core::crypto::said::compute_next_commitment;
use auths_id::keri::{
Event, IcpEvent, InceptionResult, KERI_VERSION, KeriSequence, Prefix, Said,
finalize_icp_event, serialize_for_signing,
};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use ring::rand::SystemRandom;
use ring::signature::{Ed25519KeyPair, KeyPair};
self.backend
.init_if_needed()
.map_err(|e| InitError::Registry(format!("Failed to initialize registry: {e}")))?;
let rng = SystemRandom::new();
let current_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
.map_err(|e| InitError::Crypto(format!("Key generation failed: {e}")))?;
let current_keypair = Ed25519KeyPair::from_pkcs8(current_pkcs8.as_ref())
.map_err(|e| InitError::Crypto(format!("Key parsing failed: {e}")))?;
let next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
.map_err(|e| InitError::Crypto(format!("Key generation failed: {e}")))?;
let next_keypair = Ed25519KeyPair::from_pkcs8(next_pkcs8.as_ref())
.map_err(|e| InitError::Crypto(format!("Key parsing failed: {e}")))?;
let current_pub_encoded = format!(
"D{}",
URL_SAFE_NO_PAD.encode(current_keypair.public_key().as_ref())
);
let next_commitment = compute_next_commitment(next_keypair.public_key().as_ref());
let (bt, b) = match witness_config {
Some(cfg) if cfg.is_enabled() => (
cfg.threshold.to_string(),
cfg.witness_urls.iter().map(|u| u.to_string()).collect(),
),
_ => ("0".to_string(), vec![]),
};
let icp = IcpEvent {
v: KERI_VERSION.to_string(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: "1".to_string(),
k: vec![current_pub_encoded],
nt: "1".to_string(),
n: vec![next_commitment],
bt,
b,
a: vec![],
x: String::new(),
};
let mut finalized = finalize_icp_event(icp)
.map_err(|e| InitError::Keri(format!("Failed to finalize ICP: {e}")))?;
let prefix = finalized.i.clone();
let canonical = serialize_for_signing(&Event::Icp(finalized.clone()))
.map_err(|e| InitError::Keri(format!("Failed to serialize for signing: {e}")))?;
let sig = current_keypair.sign(&canonical);
finalized.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
self.backend
.append_event(&prefix, &Event::Icp(finalized))
.map_err(|e| InitError::Registry(format!("Failed to store event in registry: {e}")))?;
let controller_did = format!("did:keri:{}", prefix);
if metadata.is_some() {
self.store_metadata(&prefix, metadata)?;
}
Ok((
controller_did,
InceptionResult {
prefix,
current_keypair_pkcs8: Pkcs8Der::new(current_pkcs8.as_ref()),
next_keypair_pkcs8: Pkcs8Der::new(next_pkcs8.as_ref()),
current_public_key: current_keypair.public_key().as_ref().to_vec(),
next_public_key: next_keypair.public_key().as_ref().to_vec(),
},
))
}
fn store_metadata(
&self,
prefix: &auths_verifier::keri::Prefix,
metadata: Option<serde_json::Value>,
) -> Result<(), StorageError> {
let repo = Repository::open(&self.repo_path)?;
let registry_ref = repo.find_reference(REGISTRY_REF)?;
let commit = registry_ref.peel_to_commit()?;
let tree = commit.tree()?;
let id_path = identity_path(prefix)
.map_err(|e| StorageError::InvalidData(format!("Invalid prefix: {}", e)))?;
let metadata_path = format!("{}/metadata.json", id_path);
let stored = StoredMetadata::new(metadata);
let json = serde_json::to_vec_pretty(&stored)?;
let mut mutator = TreeMutator::new();
mutator.write_blob(&metadata_path, json);
let new_tree_oid = mutator
.build_tree(&repo, Some(&tree))
.map_err(|e| StorageError::InvalidData(e.to_string()))?;
let new_tree = repo.find_tree(new_tree_oid)?;
#[allow(clippy::disallowed_methods)]
let now = chrono::Utc::now();
let sig =
git2::Signature::new("auths", "auths@local", &git2::Time::new(now.timestamp(), 0))?;
let parent = &[&commit];
repo.commit(
Some(REGISTRY_REF),
&sig,
&sig,
"Store identity metadata",
&new_tree,
parent,
)?;
Ok(())
}
fn load_metadata(
&self,
prefix: &auths_verifier::keri::Prefix,
) -> Result<Option<serde_json::Value>, StorageError> {
let repo = Repository::open(&self.repo_path)?;
let registry_ref = match repo.find_reference(REGISTRY_REF) {
Ok(r) => r,
Err(_) => return Ok(None),
};
let commit = registry_ref.peel_to_commit()?;
let tree = commit.tree()?;
let id_path = identity_path(prefix)
.map_err(|e| StorageError::InvalidData(format!("Invalid prefix: {}", e)))?;
let metadata_path = format!("{}/metadata.json", id_path);
let nav = TreeNavigator::new(&repo, tree);
match nav.read_blob_path(&metadata_path) {
Ok(bytes) => {
let stored: StoredMetadata = serde_json::from_slice(&bytes)?;
Ok(stored.metadata)
}
Err(_) => Ok(None),
}
}
fn get_storage_id(&self) -> String {
self.repo_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string()
}
fn find_first_identity(&self) -> Result<Option<String>, StorageError> {
use std::ops::ControlFlow;
let mut prefix = None;
self.backend
.visit_identities(&mut |p| {
prefix = Some(p.to_string());
ControlFlow::Break(())
})
.map_err(|e| StorageError::InvalidData(format!("Failed to visit identities: {}", e)))?;
Ok(prefix)
}
}
impl IdentityStorage for RegistryIdentityStorage {
fn create_identity(
&self,
controller_did: &str,
metadata: Option<serde_json::Value>,
) -> Result<(), StorageError> {
use auths_verifier::keri::Prefix;
let prefix_str = controller_did.strip_prefix("did:keri:").ok_or_else(|| {
StorageError::InvalidData(format!("Invalid controller DID format: {}", controller_did))
})?;
let prefix = Prefix::new_unchecked(prefix_str.to_string());
self.store_metadata(&prefix, metadata)?;
Ok(())
}
fn load_identity(&self) -> Result<ManagedIdentity, StorageError> {
use auths_verifier::keri::Prefix;
let prefix_str = self
.find_first_identity()?
.ok_or_else(|| StorageError::NotFound("No identity found in registry".to_string()))?;
let prefix = Prefix::new_unchecked(prefix_str.clone());
self.backend
.get_key_state(&prefix)
.map_err(|e| StorageError::InvalidData(format!("Failed to load key state: {}", e)))?;
let controller_did = format!("did:keri:{}", prefix_str);
let metadata = self.load_metadata(&prefix)?;
#[allow(clippy::disallowed_methods)]
let controller_did = IdentityDID::new_unchecked(controller_did);
Ok(ManagedIdentity {
controller_did,
storage_id: self.get_storage_id(),
metadata,
})
}
fn get_identity_ref(&self) -> Result<String, StorageError> {
Ok(REGISTRY_REF.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn setup_test_repo() -> (TempDir, RegistryIdentityStorage) {
let dir = TempDir::new().unwrap();
Repository::init(dir.path()).unwrap();
let storage = RegistryIdentityStorage::new(dir.path());
(dir, storage)
}
#[test]
fn test_get_identity_ref() {
let (_dir, storage) = setup_test_repo();
let ref_name = storage.get_identity_ref().unwrap();
assert_eq!(ref_name, "refs/auths/registry");
}
#[test]
fn test_initialize_and_load_identity() {
let (_dir, storage) = setup_test_repo();
let metadata = serde_json::json!({
"name": "Test Identity",
"created": "2024-01-01"
});
let (did, _result) = storage
.initialize_identity(Some(metadata.clone()), None)
.unwrap();
assert!(did.starts_with("did:keri:"));
let identity = storage.load_identity().unwrap();
assert_eq!(identity.controller_did, did.as_str());
assert!(identity.metadata.is_some());
assert_eq!(identity.metadata.as_ref().unwrap()["name"], "Test Identity");
}
#[test]
fn test_load_identity_without_metadata() {
let (_dir, storage) = setup_test_repo();
let (did, _result) = storage.initialize_identity(None, None).unwrap();
let identity = storage.load_identity().unwrap();
assert_eq!(identity.controller_did, did.as_str());
assert!(identity.metadata.is_none());
}
#[test]
fn test_load_identity_not_found() {
let (_dir, storage) = setup_test_repo();
storage.backend.init_if_needed().unwrap();
let result = storage.load_identity();
assert!(result.is_err());
}
}