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, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum MachineAnchor {
Delegated {
controller_did: String,
},
InstitutionDelegated {
controller_did: String,
},
HardwareRooted {
hardware_root_hex: String,
sources: Vec<String>,
},
}
impl MachineAnchor {
pub fn controller_did(&self) -> Option<&str> {
match self {
MachineAnchor::Delegated { controller_did }
| MachineAnchor::InstitutionDelegated { controller_did } => Some(controller_did),
MachineAnchor::HardwareRooted { .. } => None,
}
}
pub fn is_delegated(&self) -> bool {
self.controller_did().is_some()
}
pub fn is_valid(&self) -> bool {
match self {
MachineAnchor::Delegated { controller_did }
| MachineAnchor::InstitutionDelegated { controller_did } => !controller_did.is_empty(),
MachineAnchor::HardwareRooted {
hardware_root_hex,
sources,
} => {
hardware_root_hex.len() == 64
&& hardware_root_hex.chars().all(|c| c.is_ascii_hexdigit())
&& sources.iter().any(|label| {
tenzro_types::machine_id::IdentifierSource::parse(label)
.is_some_and(|src| src.grade().is_attestable())
})
}
}
}
pub fn rejection_reason(&self) -> Option<&'static str> {
if self.is_valid() {
return None;
}
Some(match self {
MachineAnchor::Delegated { .. } | MachineAnchor::InstitutionDelegated { .. } => {
"a delegated machine must name the DID accountable for it"
}
MachineAnchor::HardwareRooted { .. } => {
"a machine with no human controller must be anchored by a hardware root of trust \
that can prove possession — a TPM, secure enclave or secure element. A readable \
serial is not enough: anything on the machine can read one, and anything anywhere \
can claim one"
}
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "authority", rename_all = "snake_case")]
pub enum TransferAuthority {
Controller {
controller_did: String,
},
HardwareRoot {
hardware_root_hex: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransferError {
WrongAuthority,
NotTheController {
expected: String,
},
WrongHardwareRoot,
InvalidNewOwner,
Expired,
}
impl std::fmt::Display for TransferError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WrongAuthority => write!(
f,
"the authority presented does not anchor this machine. A machine a human \
delegated moves only on that controller's authority — holding the hardware does \
not override an accountable party. A machine nobody delegated moves only on \
proof of its hardware root"
),
Self::NotTheController { expected } => write!(
f,
"this machine is controlled by {expected}, and only that identity can transfer it"
),
Self::WrongHardwareRoot => write!(
f,
"the hardware root proven is not the one this machine is anchored on — a \
different root of trust is a different machine"
),
Self::InvalidNewOwner => write!(
f,
"the new owner must be a real identity, and a different one from the current owner"
),
Self::Expired => write!(
f,
"this transfer's validity window has passed; issue a fresh one rather than \
replaying an old authorisation"
),
}
}
}
impl std::error::Error for TransferError {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OwnershipTransfer {
pub machine_did: String,
pub new_owner_did: String,
pub authority: TransferAuthority,
pub expires_at_ms: u64,
}
impl OwnershipTransfer {
pub fn authorize(
&self,
current: &MachineAnchor,
now_ms: u64,
) -> Result<MachineAnchor, TransferError> {
if now_ms >= self.expires_at_ms {
return Err(TransferError::Expired);
}
if self.new_owner_did.trim().is_empty() {
return Err(TransferError::InvalidNewOwner);
}
if current.controller_did() == Some(self.new_owner_did.as_str()) {
return Err(TransferError::InvalidNewOwner);
}
match (&self.authority, current) {
(
TransferAuthority::Controller { controller_did },
MachineAnchor::Delegated {
controller_did: actual,
}
| MachineAnchor::InstitutionDelegated {
controller_did: actual,
},
) => {
if controller_did != actual {
return Err(TransferError::NotTheController {
expected: actual.clone(),
});
}
Ok(MachineAnchor::Delegated {
controller_did: self.new_owner_did.clone(),
})
}
(
TransferAuthority::HardwareRoot { hardware_root_hex },
MachineAnchor::HardwareRooted {
hardware_root_hex: actual,
sources,
},
) => {
if hardware_root_hex != actual {
return Err(TransferError::WrongHardwareRoot);
}
let _ = sources;
Ok(MachineAnchor::Delegated {
controller_did: self.new_owner_did.clone(),
})
}
_ => Err(TransferError::WrongAuthority),
}
}
}
#[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, PartialEq, Eq)]
pub struct WalletRef {
pub wallet_id: String,
pub address: Address,
#[serde(default)]
pub pq_verifying_key: Vec<u8>,
#[serde(default)]
pub bls_verifying_key: Vec<u8>,
}
#[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)]
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 ownership_transfer_tests {
use super::*;
const ROOT: &str = "ab";
fn root_hex() -> String {
ROOT.repeat(32)
}
fn delegated(controller: &str) -> MachineAnchor {
MachineAnchor::Delegated {
controller_did: controller.to_string(),
}
}
fn hardware() -> MachineAnchor {
MachineAnchor::HardwareRooted {
hardware_root_hex: root_hex(),
sources: vec!["tpm:ek".to_string()],
}
}
fn transfer(to: &str, authority: TransferAuthority) -> OwnershipTransfer {
OwnershipTransfer {
machine_did: "did:tenzro:machine:box".to_string(),
new_owner_did: to.to_string(),
authority,
expires_at_ms: 10_000,
}
}
#[test]
fn a_controller_transfers_the_machine_it_controls() {
let t = transfer(
"did:tenzro:human:bob",
TransferAuthority::Controller {
controller_did: "did:tenzro:human:alice".into(),
},
);
let next = t
.authorize(&delegated("did:tenzro:human:alice"), 1_000)
.expect("the controller may transfer");
assert_eq!(next.controller_did(), Some("did:tenzro:human:bob"));
}
#[test]
fn holding_the_hardware_cannot_take_a_delegated_machine() {
let t = transfer(
"did:tenzro:human:thief",
TransferAuthority::HardwareRoot {
hardware_root_hex: root_hex(),
},
);
let err = t
.authorize(&delegated("did:tenzro:human:alice"), 1_000)
.expect_err("possession must not override delegation");
assert_eq!(err, TransferError::WrongAuthority);
assert!(err.to_string().contains("does not override"), "{err}");
}
#[test]
fn the_hardware_holder_transfers_a_machine_nobody_delegated() {
let t = transfer(
"did:tenzro:human:buyer",
TransferAuthority::HardwareRoot {
hardware_root_hex: root_hex(),
},
);
let next = t.authorize(&hardware(), 1_000).expect("the TPM holder may");
assert_eq!(next.controller_did(), Some("did:tenzro:human:buyer"));
assert!(next.is_delegated());
}
#[test]
fn a_transfer_proving_the_wrong_root_is_refused() {
let t = transfer(
"did:tenzro:human:buyer",
TransferAuthority::HardwareRoot {
hardware_root_hex: "cd".repeat(32),
},
);
assert_eq!(
t.authorize(&hardware(), 1_000),
Err(TransferError::WrongHardwareRoot)
);
}
#[test]
fn a_stranger_claiming_to_be_the_controller_is_refused() {
let t = transfer(
"did:tenzro:human:bob",
TransferAuthority::Controller {
controller_did: "did:tenzro:human:mallory".into(),
},
);
let err = t
.authorize(&delegated("did:tenzro:human:alice"), 1_000)
.expect_err("only the real controller may transfer");
assert_eq!(
err,
TransferError::NotTheController {
expected: "did:tenzro:human:alice".into()
}
);
}
#[test]
fn a_controller_cannot_move_a_machine_that_has_none() {
let t = transfer(
"did:tenzro:human:bob",
TransferAuthority::Controller {
controller_did: "did:tenzro:human:alice".into(),
},
);
assert_eq!(
t.authorize(&hardware(), 1_000),
Err(TransferError::WrongAuthority)
);
}
#[test]
fn an_expired_authorisation_is_refused() {
let t = transfer(
"did:tenzro:human:bob",
TransferAuthority::Controller {
controller_did: "did:tenzro:human:alice".into(),
},
);
assert_eq!(
t.authorize(&delegated("did:tenzro:human:alice"), 10_000),
Err(TransferError::Expired),
"expiry is exclusive"
);
t.authorize(&delegated("did:tenzro:human:alice"), 9_999)
.expect("still inside the window");
}
#[test]
fn a_transfer_to_the_current_owner_or_to_nobody_is_refused() {
let to_self = transfer(
"did:tenzro:human:alice",
TransferAuthority::Controller {
controller_did: "did:tenzro:human:alice".into(),
},
);
assert_eq!(
to_self.authorize(&delegated("did:tenzro:human:alice"), 1_000),
Err(TransferError::InvalidNewOwner)
);
let to_nobody = transfer(
" ",
TransferAuthority::Controller {
controller_did: "did:tenzro:human:alice".into(),
},
);
assert_eq!(
to_nobody.authorize(&delegated("did:tenzro:human:alice"), 1_000),
Err(TransferError::InvalidNewOwner)
);
}
#[test]
fn an_institution_transfers_its_own_machine() {
let anchor = MachineAnchor::InstitutionDelegated {
controller_did: "did:tenzro:institution:acme".into(),
};
let t = transfer(
"did:tenzro:human:bob",
TransferAuthority::Controller {
controller_did: "did:tenzro:institution:acme".into(),
},
);
let next = t.authorize(&anchor, 1_000).expect("the institution may");
assert_eq!(next.controller_did(), Some("did:tenzro:human:bob"));
}
#[test]
fn ownership_replaces_rather_than_accumulating() {
let mut anchor = delegated("did:tenzro:human:alice");
for owner in ["did:tenzro:human:bob", "did:tenzro:human:carol"] {
let previous = anchor.controller_did().expect("a controller").to_string();
let t = transfer(
owner,
TransferAuthority::Controller {
controller_did: previous.clone(),
},
);
anchor = t.authorize(&anchor, 1_000).expect("each hop authorises");
assert_eq!(anchor.controller_did(), Some(owner));
}
let stale = transfer(
"did:tenzro:human:mallory",
TransferAuthority::Controller {
controller_did: "did:tenzro:human:alice".into(),
},
);
assert!(stale.authorize(&anchor, 1_000).is_err());
}
#[test]
fn the_resulting_anchor_is_always_valid() {
let from_hardware = transfer(
"did:tenzro:human:buyer",
TransferAuthority::HardwareRoot {
hardware_root_hex: root_hex(),
},
)
.authorize(&hardware(), 1_000)
.expect("authorised");
assert!(from_hardware.is_valid());
assert!(from_hardware.rejection_reason().is_none());
}
}
#[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());
}
}