use crate::credential::VerifiableCredential;
use crate::delegation::DelegationScope;
use crate::did::TenzroDid;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
use tenzro_types::identity::KycTier;
use tenzro_types::primitives::Address;
pub const ML_DSA_65_VERIFYING_KEY_LEN: usize = 1952;
pub const BLS_G1_COMPRESSED_LEN: usize = 48;
fn validate_pq_verifying_key<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let bytes: Vec<u8> = Vec::deserialize(deserializer)?;
if bytes.len() != ML_DSA_65_VERIFYING_KEY_LEN {
return Err(serde::de::Error::custom(format!(
"ML-DSA-65 verifying key must be exactly {} bytes, got {}",
ML_DSA_65_VERIFYING_KEY_LEN,
bytes.len()
)));
}
Ok(bytes)
}
fn validate_bls_verifying_key<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let bytes: Vec<u8> = Vec::deserialize(deserializer)?;
if bytes.len() != BLS_G1_COMPRESSED_LEN {
return Err(serde::de::Error::custom(format!(
"BLS12-381 G1-compressed verifying key must be exactly {} bytes, got {}",
BLS_G1_COMPRESSED_LEN,
bytes.len()
)));
}
Ok(bytes)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IdentityStatus {
Active,
Suspended,
Revoked,
}
impl std::fmt::Display for IdentityStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IdentityStatus::Active => write!(f, "active"),
IdentityStatus::Suspended => write!(f, "suspended"),
IdentityStatus::Revoked => write!(f, "revoked"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublicKeyInfo {
pub key_id: String,
pub key_type: String,
pub public_key: Vec<u8>,
pub purposes: Vec<KeyPurpose>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum KeyPurpose {
Authentication,
AssertionMethod,
KeyAgreement,
CapabilityInvocation,
CapabilityDelegation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceEndpoint {
pub id: String,
pub service_type: String,
pub service_endpoint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
pub enum IdentityData {
Human {
display_name: String,
kyc_tier: KycTier,
controlled_machines: Vec<String>,
},
Machine {
capabilities: Vec<String>,
delegation_scope: DelegationScope,
controller_did: Option<String>,
reputation: u32,
tenzro_agent_id: Option<String>,
is_seed_agent: bool,
erc8004_agent_id: Option<u64>,
},
Institution {
legal_name: String,
lei: String,
kyb_tier: KycTier,
vlei_credential_id: Option<String>,
controlled_machines: Vec<String>,
country_iso2: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenzroIdentity {
pub did: TenzroDid,
pub public_keys: Vec<PublicKeyInfo>,
pub identity_data: IdentityData,
pub status: IdentityStatus,
pub wallet_address: Address,
pub wallet_id: String,
#[serde(deserialize_with = "validate_pq_verifying_key")]
pub pq_verifying_key: Vec<u8>,
#[serde(deserialize_with = "validate_bls_verifying_key")]
pub bls_verifying_key: Vec<u8>,
pub credentials: Vec<VerifiableCredential>,
pub services: Vec<ServiceEndpoint>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub metadata: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
}
impl TenzroIdentity {
pub fn is_human(&self) -> bool {
matches!(self.identity_data, IdentityData::Human { .. })
}
pub fn is_machine(&self) -> bool {
matches!(self.identity_data, IdentityData::Machine { .. })
}
pub fn is_institution(&self) -> bool {
matches!(self.identity_data, IdentityData::Institution { .. })
}
pub fn lei(&self) -> Option<&str> {
match &self.identity_data {
IdentityData::Institution { lei, .. } => Some(lei),
_ => None,
}
}
pub fn is_active(&self) -> bool {
self.status == IdentityStatus::Active
}
pub fn did_string(&self) -> String {
self.did.to_string()
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
bincode::serialize(self)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
bincode::deserialize(bytes)
}
pub fn display_name(&self) -> String {
match &self.identity_data {
IdentityData::Human { display_name, .. } => display_name.clone(),
IdentityData::Machine { .. } => self.did.to_string(),
IdentityData::Institution { legal_name, .. } => legal_name.clone(),
}
}
pub fn kyc_tier(&self) -> Option<KycTier> {
match &self.identity_data {
IdentityData::Human { kyc_tier, .. } => Some(*kyc_tier),
IdentityData::Institution { kyb_tier, .. } => Some(*kyb_tier),
IdentityData::Machine { .. } => None,
}
}
pub fn delegation_scope(&self) -> Option<&DelegationScope> {
match &self.identity_data {
IdentityData::Machine {
delegation_scope, ..
} => Some(delegation_scope),
_ => None,
}
}
pub fn controller_did(&self) -> Option<&str> {
match &self.identity_data {
IdentityData::Machine { controller_did, .. } => controller_did.as_deref(),
_ => None,
}
}
pub fn controlled_machines(&self) -> Option<&[String]> {
match &self.identity_data {
IdentityData::Human {
controlled_machines,
..
} => Some(controlled_machines),
IdentityData::Institution {
controlled_machines,
..
} => Some(controlled_machines),
IdentityData::Machine { .. } => None,
}
}
pub fn is_seed_agent(&self) -> bool {
match &self.identity_data {
IdentityData::Machine { is_seed_agent, .. } => *is_seed_agent,
_ => false,
}
}
pub fn erc8004_agent_id(&self) -> Option<u64> {
match &self.identity_data {
IdentityData::Machine { erc8004_agent_id, .. } => *erc8004_agent_id,
_ => None,
}
}
pub(crate) fn set_erc8004_agent_id(&mut self, id: u64) {
if let IdentityData::Machine { erc8004_agent_id, .. } = &mut self.identity_data {
*erc8004_agent_id = Some(id);
}
}
pub fn add_service(&mut self, service: ServiceEndpoint) -> crate::error::Result<()> {
tenzro_types::validation::validate_service_endpoint_url(&service.service_endpoint)
.map_err(|e| crate::error::IdentityError::InvalidServiceEndpoint(e.to_string()))?;
self.services.push(service);
self.updated_at = Utc::now();
Ok(())
}
pub fn add_credential(&mut self, credential: VerifiableCredential) {
self.credentials.push(credential);
self.updated_at = Utc::now();
}
pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.metadata.insert(key.into(), value.into());
self.updated_at = Utc::now();
}
pub fn set_username(&mut self, username: &str) -> crate::error::Result<()> {
validate_username(username)?;
self.username = Some(username.to_string());
self.updated_at = Utc::now();
Ok(())
}
pub fn username(&self) -> Option<&str> {
self.username.as_deref()
}
pub fn pq_verifying_key_bytes(&self) -> &[u8] {
&self.pq_verifying_key
}
pub fn bls_verifying_key_bytes(&self) -> &[u8] {
&self.bls_verifying_key
}
}
pub fn validate_username(username: &str) -> crate::error::Result<()> {
if username.len() < 3 {
return Err(crate::error::IdentityError::UsernameInvalid(
"username must be at least 3 characters".to_string(),
));
}
if username.len() > 20 {
return Err(crate::error::IdentityError::UsernameInvalid(
"username must be at most 20 characters".to_string(),
));
}
if !username
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
{
return Err(crate::error::IdentityError::UsernameInvalid(
"username must contain only lowercase letters, digits, and underscores".to_string(),
));
}
if username.starts_with('_') || username.ends_with('_') {
return Err(crate::error::IdentityError::UsernameInvalid(
"username must not start or end with an underscore".to_string(),
));
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RevocationEntry {
pub did: String,
pub revoked_at: DateTime<Utc>,
pub reason: String,
pub revoked_by: String,
}
#[cfg(test)]
mod tests {
use super::*;
fn test_pq_vk() -> Vec<u8> {
tenzro_crypto::pq::MlDsaSigningKey::generate()
.verifying_key_bytes()
.to_vec()
}
fn test_bls_vk() -> Vec<u8> {
tenzro_crypto::bls::BlsKeyPair::generate()
.unwrap()
.public_key()
.to_bytes()
.to_vec()
}
fn make_test_human() -> TenzroIdentity {
TenzroIdentity {
did: TenzroDid::new_human(),
public_keys: vec![PublicKeyInfo {
key_id: "key-1".to_string(),
key_type: "Ed25519".to_string(),
public_key: vec![1; 32],
purposes: vec![KeyPurpose::Authentication, KeyPurpose::AssertionMethod],
}],
identity_data: IdentityData::Human {
display_name: "Alice".to_string(),
kyc_tier: KycTier::Enhanced,
controlled_machines: Vec::new(),
},
status: IdentityStatus::Active,
wallet_address: Address::new([0u8; 32]),
wallet_id: "wallet-1".to_string(),
pq_verifying_key: test_pq_vk(),
bls_verifying_key: test_bls_vk(),
credentials: Vec::new(),
services: Vec::new(),
created_at: Utc::now(),
updated_at: Utc::now(),
metadata: HashMap::new(),
username: None,
}
}
fn make_test_machine(controller: &str) -> TenzroIdentity {
TenzroIdentity {
did: TenzroDid::new_machine("ctrl-id"),
public_keys: vec![PublicKeyInfo {
key_id: "key-1".to_string(),
key_type: "Ed25519".to_string(),
public_key: vec![2; 32],
purposes: vec![KeyPurpose::Authentication],
}],
identity_data: IdentityData::Machine {
capabilities: vec!["inference".to_string()],
delegation_scope: DelegationScope::unrestricted(),
controller_did: Some(controller.to_string()),
reputation: 500,
tenzro_agent_id: None,
erc8004_agent_id: None,
is_seed_agent: false,
},
status: IdentityStatus::Active,
wallet_address: Address::new([1u8; 32]),
wallet_id: "wallet-2".to_string(),
pq_verifying_key: test_pq_vk(),
bls_verifying_key: test_bls_vk(),
credentials: Vec::new(),
services: Vec::new(),
created_at: Utc::now(),
updated_at: Utc::now(),
metadata: HashMap::new(),
username: None,
}
}
#[test]
fn test_human_identity() {
let identity = make_test_human();
assert!(identity.is_human());
assert!(!identity.is_machine());
assert!(identity.is_active());
assert_eq!(identity.display_name(), "Alice");
assert_eq!(identity.kyc_tier(), Some(KycTier::Enhanced));
assert!(identity.delegation_scope().is_none());
assert!(identity.controller_did().is_none());
assert_eq!(identity.controlled_machines().unwrap().len(), 0);
}
#[test]
fn test_machine_identity() {
let identity = make_test_machine("did:tenzro:human:alice");
assert!(identity.is_machine());
assert!(!identity.is_human());
assert!(identity.is_active());
assert_eq!(
identity.controller_did(),
Some("did:tenzro:human:alice")
);
assert!(identity.delegation_scope().is_some());
assert!(identity.kyc_tier().is_none());
assert!(identity.controlled_machines().is_none());
}
#[test]
fn test_identity_status() {
assert_eq!(format!("{}", IdentityStatus::Active), "active");
assert_eq!(format!("{}", IdentityStatus::Suspended), "suspended");
assert_eq!(format!("{}", IdentityStatus::Revoked), "revoked");
}
#[test]
fn test_add_service() {
let mut identity = make_test_human();
identity.add_service(ServiceEndpoint {
id: "svc-1".to_string(),
service_type: "InferenceEndpoint".to_string(),
service_endpoint: "https://example.com/inference".to_string(),
}).unwrap();
assert_eq!(identity.services.len(), 1);
}
#[test]
fn test_add_service_rejects_invalid_url() {
let mut identity = make_test_human();
let result = identity.add_service(ServiceEndpoint {
id: "svc-bad".to_string(),
service_type: "InferenceEndpoint".to_string(),
service_endpoint: "ftp://files.example.com/model".to_string(),
});
assert!(result.is_err());
assert_eq!(identity.services.len(), 0);
}
#[test]
fn test_add_service_rejects_empty_url() {
let mut identity = make_test_human();
assert!(identity.add_service(ServiceEndpoint {
id: "svc-bad".to_string(),
service_type: "InferenceEndpoint".to_string(),
service_endpoint: "".to_string(),
}).is_err());
}
#[test]
fn test_set_metadata() {
let mut identity = make_test_human();
identity.set_metadata("org", "TenzroLabs");
assert_eq!(identity.metadata.get("org"), Some(&"TenzroLabs".to_string()));
}
#[test]
fn test_identity_serialization() {
let identity = make_test_human();
let bytes = identity.to_bytes().unwrap();
assert!(!bytes.is_empty());
let deserialized = TenzroIdentity::from_bytes(&bytes).unwrap();
assert_eq!(deserialized.did.to_string(), identity.did.to_string());
assert_eq!(deserialized.display_name(), identity.display_name());
assert_eq!(deserialized.status, identity.status);
}
#[test]
fn test_machine_identity_serialization() {
let identity = make_test_machine("did:tenzro:human:ctrl");
let bytes = identity.to_bytes().unwrap();
let deserialized = TenzroIdentity::from_bytes(&bytes).unwrap();
assert_eq!(deserialized.controller_did(), identity.controller_did());
assert!(deserialized.is_machine());
}
}