use serde::{Deserialize, Serialize};
use crate::delegation::DelegationScope;
use crate::identity::{IdentityData, IdentityStatus, KeyPurpose, PublicKeyInfo, TenzroIdentity};
pub const SERVICE_TYPE_MASTERCARD_KYA: &str = "MastercardKYA";
pub const SERVICE_TYPE_VISA_TAP: &str = "VisaTAP";
pub const SERVICE_TYPE_STRIPE_SPT: &str = "StripeSPT";
pub const SERVICE_TYPE_TEMPO_ACCOUNT: &str = "TempoAccount";
pub fn is_kya_service_type(service_type: &str) -> bool {
matches!(
service_type,
SERVICE_TYPE_MASTERCARD_KYA
| SERVICE_TYPE_VISA_TAP
| SERVICE_TYPE_STRIPE_SPT
| SERVICE_TYPE_TEMPO_ACCOUNT
)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuthenticatorBinding {
pub key_id: String,
pub key_type: String,
pub public_key: Vec<u8>,
pub tee_attested: bool,
}
impl AuthenticatorBinding {
pub fn from_public_key(key: &PublicKeyInfo, tee_attested: bool) -> Self {
Self {
key_id: key.key_id.clone(),
key_type: key.key_type.clone(),
public_key: key.public_key.clone(),
tee_attested,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum KyaLevel {
Unverified = 0,
Basic = 1,
Enhanced = 2,
Full = 3,
}
impl std::fmt::Display for KyaLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KyaLevel::Unverified => write!(f, "unverified"),
KyaLevel::Basic => write!(f, "basic"),
KyaLevel::Enhanced => write!(f, "enhanced"),
KyaLevel::Full => write!(f, "full"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KyaRecord {
pub agent_did: String,
pub controller_did: Option<String>,
pub authenticator: Option<AuthenticatorBinding>,
pub delegation_scope: DelegationScope,
pub status: IdentityStatus,
pub is_seed_agent: bool,
pub kya_level: KyaLevel,
}
impl KyaRecord {
pub fn from_identity(identity: &TenzroIdentity) -> Option<Self> {
let IdentityData::Machine {
delegation_scope,
controller_did,
is_seed_agent,
..
} = &identity.identity_data
else {
return None;
};
let tee_attested = *is_seed_agent;
let authenticator = identity
.public_keys
.iter()
.find(|k| k.purposes.contains(&KeyPurpose::Authentication))
.map(|k| AuthenticatorBinding::from_public_key(k, tee_attested));
let kya_level = compute_kya_level(identity.status, controller_did, delegation_scope);
Some(Self {
agent_did: identity.did_string(),
controller_did: controller_did.clone(),
authenticator,
delegation_scope: delegation_scope.clone(),
status: identity.status,
is_seed_agent: *is_seed_agent,
kya_level,
})
}
}
pub fn compute_kya_level(
status: IdentityStatus,
controller_did: &Option<String>,
delegation_scope: &DelegationScope,
) -> KyaLevel {
if status != IdentityStatus::Active {
return KyaLevel::Unverified;
}
let Some(_) = controller_did else {
return KyaLevel::Basic;
};
if delegation_scope.max_transaction_value.is_some()
|| !delegation_scope.allowed_operations.is_empty()
|| !delegation_scope.allowed_payment_protocols.is_empty()
{
KyaLevel::Full
} else {
KyaLevel::Enhanced
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::did::TenzroDid;
use crate::identity::{KeyPurpose, PublicKeyInfo, ServiceEndpoint};
use chrono::Utc;
use std::collections::HashMap;
use tenzro_types::primitives::Address;
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 machine_identity_with_scope(
controller: Option<&str>,
scope: DelegationScope,
is_seed_agent: bool,
status: IdentityStatus,
) -> TenzroIdentity {
TenzroIdentity {
did: TenzroDid::parse("did:tenzro:machine:ctrl:bot1").unwrap(),
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::Machine {
capabilities: vec!["inference".to_string()],
delegation_scope: scope,
controller_did: controller.map(String::from),
reputation: 0,
tenzro_agent_id: None,
erc8004_agent_id: None,
is_seed_agent,
},
status,
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,
}
}
#[test]
fn test_kya_service_type_constants() {
assert_eq!(SERVICE_TYPE_MASTERCARD_KYA, "MastercardKYA");
assert_eq!(SERVICE_TYPE_VISA_TAP, "VisaTAP");
assert_eq!(SERVICE_TYPE_STRIPE_SPT, "StripeSPT");
assert_eq!(SERVICE_TYPE_TEMPO_ACCOUNT, "TempoAccount");
assert!(is_kya_service_type(SERVICE_TYPE_MASTERCARD_KYA));
assert!(is_kya_service_type(SERVICE_TYPE_VISA_TAP));
assert!(is_kya_service_type(SERVICE_TYPE_STRIPE_SPT));
assert!(is_kya_service_type(SERVICE_TYPE_TEMPO_ACCOUNT));
assert!(!is_kya_service_type("InferenceEndpoint"));
assert!(!is_kya_service_type(""));
}
#[test]
fn test_kya_record_returns_none_for_human() {
let human = TenzroIdentity {
did: TenzroDid::parse("did:tenzro:human:alice").unwrap(),
public_keys: vec![],
identity_data: IdentityData::Human {
display_name: "Alice".to_string(),
kyc_tier: tenzro_types::identity::KycTier::Unverified,
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,
};
assert!(KyaRecord::from_identity(&human).is_none());
}
#[test]
fn test_kya_record_autonomous_basic() {
let id = machine_identity_with_scope(
None,
DelegationScope::default(),
false,
IdentityStatus::Active,
);
let rec = KyaRecord::from_identity(&id).expect("machine record");
assert_eq!(rec.kya_level, KyaLevel::Basic);
assert!(rec.controller_did.is_none());
assert!(rec.authenticator.is_some());
assert!(!rec.authenticator.as_ref().unwrap().tee_attested);
}
#[test]
fn test_kya_record_controlled_default_scope_enhanced() {
let id = machine_identity_with_scope(
Some("did:tenzro:human:ctrl"),
DelegationScope::default(),
false,
IdentityStatus::Active,
);
let rec = KyaRecord::from_identity(&id).unwrap();
assert_eq!(rec.kya_level, KyaLevel::Enhanced);
assert_eq!(rec.controller_did.as_deref(), Some("did:tenzro:human:ctrl"));
}
#[test]
fn test_kya_record_controlled_with_scope_full() {
let scope = DelegationScope {
max_transaction_value: Some(1000),
allowed_operations: vec!["transfer".to_string()],
..Default::default()
};
let id = machine_identity_with_scope(
Some("did:tenzro:human:ctrl"),
scope,
false,
IdentityStatus::Active,
);
let rec = KyaRecord::from_identity(&id).unwrap();
assert_eq!(rec.kya_level, KyaLevel::Full);
}
#[test]
fn test_kya_record_suspended_unverified() {
let id = machine_identity_with_scope(
Some("did:tenzro:human:ctrl"),
DelegationScope::default(),
false,
IdentityStatus::Suspended,
);
let rec = KyaRecord::from_identity(&id).unwrap();
assert_eq!(rec.kya_level, KyaLevel::Unverified);
}
#[test]
fn test_kya_record_revoked_unverified() {
let id = machine_identity_with_scope(
None,
DelegationScope::default(),
false,
IdentityStatus::Revoked,
);
let rec = KyaRecord::from_identity(&id).unwrap();
assert_eq!(rec.kya_level, KyaLevel::Unverified);
}
#[test]
fn test_kya_record_seed_agent_marks_tee_attested() {
let id = machine_identity_with_scope(
None,
DelegationScope::default(),
true,
IdentityStatus::Active,
);
let rec = KyaRecord::from_identity(&id).unwrap();
assert!(rec.is_seed_agent);
assert!(rec.authenticator.unwrap().tee_attested);
}
#[test]
fn test_kya_record_no_auth_key_no_authenticator() {
let mut id = machine_identity_with_scope(
None,
DelegationScope::default(),
false,
IdentityStatus::Active,
);
id.public_keys = vec![PublicKeyInfo {
key_id: "key-1".to_string(),
key_type: "Ed25519".to_string(),
public_key: vec![1; 32],
purposes: vec![KeyPurpose::AssertionMethod],
}];
let rec = KyaRecord::from_identity(&id).unwrap();
assert!(rec.authenticator.is_none());
}
#[test]
fn test_kya_record_serde_round_trip() {
let id = machine_identity_with_scope(
Some("did:tenzro:human:ctrl"),
DelegationScope::default(),
true,
IdentityStatus::Active,
);
let rec = KyaRecord::from_identity(&id).unwrap();
let json = serde_json::to_string(&rec).unwrap();
let parsed: KyaRecord = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.agent_did, rec.agent_did);
assert_eq!(parsed.kya_level, rec.kya_level);
assert_eq!(parsed.is_seed_agent, rec.is_seed_agent);
assert_eq!(
parsed.authenticator.as_ref().unwrap().tee_attested,
rec.authenticator.as_ref().unwrap().tee_attested
);
}
#[test]
fn test_compute_kya_level_pure_function() {
let scope = DelegationScope::default();
assert_eq!(
compute_kya_level(IdentityStatus::Active, &None, &scope),
KyaLevel::Basic
);
assert_eq!(
compute_kya_level(IdentityStatus::Active, &Some("x".to_string()), &scope),
KyaLevel::Enhanced
);
assert_eq!(
compute_kya_level(IdentityStatus::Suspended, &None, &scope),
KyaLevel::Unverified
);
}
#[test]
fn test_kya_level_ord() {
assert!(KyaLevel::Unverified < KyaLevel::Basic);
assert!(KyaLevel::Basic < KyaLevel::Enhanced);
assert!(KyaLevel::Enhanced < KyaLevel::Full);
}
#[test]
fn test_did_document_can_carry_kya_service_type() {
let svc = ServiceEndpoint {
id: "did:tenzro:machine:abc#mastercard-kya".to_string(),
service_type: SERVICE_TYPE_MASTERCARD_KYA.to_string(),
service_endpoint: "https://kya.mastercard.com/agents/abc".to_string(),
};
let json = serde_json::to_string(&svc).unwrap();
assert!(json.contains("MastercardKYA"));
assert!(json.contains("kya.mastercard.com"));
}
}