use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum TenzroCredentialType {
KycAttestation,
AgeVerification,
ResidencyProof,
AccreditedInvestor,
InstitutionalMember,
PaymentAuthorization,
ModelProvider,
TeeOperator,
Custom(String),
}
impl TenzroCredentialType {
pub fn type_name(&self) -> &str {
match self {
Self::KycAttestation => "KycAttestation",
Self::AgeVerification => "AgeVerification",
Self::ResidencyProof => "ResidencyProof",
Self::AccreditedInvestor => "AccreditedInvestor",
Self::InstitutionalMember => "InstitutionalMember",
Self::PaymentAuthorization => "PaymentAuthorization",
Self::ModelProvider => "ModelProvider",
Self::TeeOperator => "TeeOperator",
Self::Custom(name) => name,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CredentialProof {
pub proof_type: String,
pub created: DateTime<Utc>,
pub verification_method: String,
pub proof_purpose: String,
pub proof_value: Vec<u8>,
}
impl CredentialProof {
pub fn new(
proof_type: impl Into<String>,
verification_method: impl Into<String>,
proof_value: Vec<u8>,
) -> Self {
Self {
proof_type: proof_type.into(),
created: Utc::now(),
verification_method: verification_method.into(),
proof_purpose: "assertionMethod".to_string(),
proof_value,
}
}
pub fn verify(&self, message: &[u8], issuer_pubkey: &[u8]) -> crate::error::Result<bool> {
use tenzro_crypto::{KeyType, PublicKey, Signature};
match self.proof_type.as_str() {
"Ed25519Signature2020" | "Ed25519" => {
if issuer_pubkey.len() != 32 {
return Err(crate::error::IdentityError::VerificationFailed(format!(
"Invalid Ed25519 public key length: expected 32, got {}",
issuer_pubkey.len()
)));
}
if self.proof_value.len() != 64 {
return Err(crate::error::IdentityError::VerificationFailed(format!(
"Invalid Ed25519 signature length: expected 64, got {}",
self.proof_value.len()
)));
}
let pubkey = PublicKey::new(KeyType::Ed25519, issuer_pubkey.to_vec());
let signature = Signature::new(KeyType::Ed25519, self.proof_value.clone());
match tenzro_crypto::signatures::verify(&pubkey, message, &signature) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
"EcdsaSecp256k1Signature2019" | "Secp256k1" => {
let pubkey = PublicKey::new(KeyType::Secp256k1, issuer_pubkey.to_vec());
let signature = Signature::new(KeyType::Secp256k1, self.proof_value.clone());
match tenzro_crypto::signatures::verify(&pubkey, message, &signature) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
"MlDsa65Signature2026" | "ML-DSA-65" => {
use tenzro_crypto::pq::{ml_dsa_verify, ML_DSA_65_VK_LEN};
if issuer_pubkey.len() != ML_DSA_65_VK_LEN {
return Err(crate::error::IdentityError::VerificationFailed(format!(
"Invalid ML-DSA-65 verifying key length: expected {}, got {}",
ML_DSA_65_VK_LEN,
issuer_pubkey.len()
)));
}
match ml_dsa_verify(issuer_pubkey, message, &self.proof_value) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
other => Err(crate::error::IdentityError::VerificationFailed(format!("Unsupported proof type: {}", other))),
}
}
pub fn verify_hybrid(
&self,
message: &[u8],
issuer_pubkey: &tenzro_crypto::composite::CompositePublicKey,
) -> crate::error::Result<bool> {
use tenzro_crypto::composite::{
CompositeSignature, HybridVerifier, StandardHybridVerifier,
};
if self.proof_type != "HybridEd25519MlDsa65Signature2026" {
return Err(crate::error::IdentityError::VerificationFailed(format!(
"expected HybridEd25519MlDsa65Signature2026 proof type, got {}",
self.proof_type
)));
}
let sig: CompositeSignature = match bincode::deserialize(&self.proof_value) {
Ok(s) => s,
Err(e) => {
return Err(crate::error::IdentityError::VerificationFailed(format!(
"could not decode hybrid signature: {}",
e
)));
}
};
let verifier = StandardHybridVerifier::new(issuer_pubkey.clone());
match verifier.verify(message, &sig) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
}
pub fn sign_credential_hybrid(
signer: &dyn tenzro_crypto::composite::HybridSigner,
message: &[u8],
verification_method: impl Into<String>,
) -> crate::error::Result<CredentialProof> {
let composite = signer.sign(message).map_err(|e| {
crate::error::IdentityError::CryptoError(format!(
"hybrid credential sign failed: {}",
e
))
})?;
let bytes = bincode::serialize(&composite).map_err(|e| {
crate::error::IdentityError::SerializationError(format!(
"failed to encode composite signature: {}",
e
))
})?;
Ok(CredentialProof::new(
"HybridEd25519MlDsa65Signature2026",
verification_method,
bytes,
))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifiableCredential {
#[serde(rename = "@context")]
pub context: Vec<String>,
pub id: String,
#[serde(rename = "type")]
pub credential_type: Vec<String>,
pub tenzro_type: TenzroCredentialType,
pub issuer: String,
pub issuance_date: DateTime<Utc>,
pub expiration_date: Option<DateTime<Utc>>,
pub credential_subject: CredentialSubject,
pub proof: Option<CredentialProof>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredentialSubject {
pub id: String,
pub claims: HashMap<String, serde_json::Value>,
}
impl VerifiableCredential {
pub fn new(
tenzro_type: TenzroCredentialType,
issuer: impl Into<String>,
subject_did: impl Into<String>,
) -> Self {
Self {
context: vec![
"https://www.w3.org/2018/credentials/v1".to_string(),
"https://identity.tenzro.com/credentials/v1".to_string(),
],
id: format!("urn:uuid:{}", uuid::Uuid::new_v4()),
credential_type: vec![
"VerifiableCredential".to_string(),
format!("Tenzro{}", tenzro_type.type_name()),
],
tenzro_type,
issuer: issuer.into(),
issuance_date: Utc::now(),
expiration_date: None,
credential_subject: CredentialSubject {
id: subject_did.into(),
claims: HashMap::new(),
},
proof: None,
}
}
pub fn with_expiration(mut self, expiration: DateTime<Utc>) -> Self {
self.expiration_date = Some(expiration);
self
}
pub fn with_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.credential_subject.claims.insert(key.into(), value);
self
}
pub fn with_proof(mut self, proof: CredentialProof) -> Self {
self.proof = Some(proof);
self
}
pub fn is_valid(&self) -> bool {
match self.expiration_date {
Some(exp) => Utc::now() < exp,
None => true,
}
}
pub fn is_signed(&self) -> bool {
self.proof.is_some()
}
pub fn verify_proof(&self, issuer_pubkey: &[u8]) -> crate::error::Result<bool> {
if !self.is_valid() {
return Ok(false);
}
let proof = match &self.proof {
Some(p) => p,
None => return Err(crate::error::IdentityError::CredentialError("No proof attached to credential".to_string())),
};
let message = serde_json::to_vec(&self.credential_subject)?;
proof.verify(&message, issuer_pubkey)
}
pub fn verify_proof_hybrid(
&self,
issuer_pubkey: &tenzro_crypto::composite::CompositePublicKey,
) -> crate::error::Result<bool> {
if !self.is_valid() {
return Ok(false);
}
let proof = match &self.proof {
Some(p) => p,
None => {
return Err(crate::error::IdentityError::CredentialError(
"No proof attached to credential".to_string(),
));
}
};
let message = serde_json::to_vec(&self.credential_subject)?;
proof.verify_hybrid(&message, issuer_pubkey)
}
pub fn sign_hybrid(
mut self,
signer: &dyn tenzro_crypto::composite::HybridSigner,
verification_method: impl Into<String>,
) -> crate::error::Result<Self> {
let message = serde_json::to_vec(&self.credential_subject)?;
let proof = sign_credential_hybrid(signer, &message, verification_method)?;
self.proof = Some(proof);
Ok(self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredentialSchema {
pub schema_id: String,
pub name: String,
pub version: String,
pub required_fields: Vec<String>,
pub optional_fields: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_credential() {
let cred = VerifiableCredential::new(
TenzroCredentialType::KycAttestation,
"did:tenzro:human:issuer-id",
"did:tenzro:human:subject-id",
);
assert!(cred.is_valid());
assert!(!cred.is_signed());
assert!(cred.context.contains(&"https://www.w3.org/2018/credentials/v1".to_string()));
assert_eq!(cred.issuer, "did:tenzro:human:issuer-id");
assert_eq!(cred.credential_subject.id, "did:tenzro:human:subject-id");
}
#[test]
fn test_credential_with_claims() {
let cred = VerifiableCredential::new(
TenzroCredentialType::AccreditedInvestor,
"did:tenzro:human:issuer",
"did:tenzro:human:alice",
)
.with_claim("status", serde_json::Value::String("verified".to_string()))
.with_claim("tier", serde_json::json!(3));
assert_eq!(cred.credential_subject.claims.len(), 2);
assert_eq!(
cred.credential_subject.claims.get("status"),
Some(&serde_json::Value::String("verified".to_string()))
);
}
#[test]
fn test_credential_expiration() {
let expired = VerifiableCredential::new(
TenzroCredentialType::AgeVerification,
"issuer",
"subject",
)
.with_expiration(Utc::now() - chrono::Duration::days(1));
assert!(!expired.is_valid());
let valid = VerifiableCredential::new(
TenzroCredentialType::AgeVerification,
"issuer",
"subject",
)
.with_expiration(Utc::now() + chrono::Duration::days(365));
assert!(valid.is_valid());
}
#[test]
fn test_credential_with_proof() {
let proof = CredentialProof::new(
"Ed25519Signature2020",
"did:tenzro:human:issuer#key-1",
vec![1, 2, 3, 4],
);
let cred = VerifiableCredential::new(
TenzroCredentialType::KycAttestation,
"did:tenzro:human:issuer",
"did:tenzro:human:subject",
)
.with_proof(proof);
assert!(cred.is_signed());
}
#[test]
fn test_credential_type_name() {
assert_eq!(TenzroCredentialType::KycAttestation.type_name(), "KycAttestation");
assert_eq!(TenzroCredentialType::ModelProvider.type_name(), "ModelProvider");
assert_eq!(
TenzroCredentialType::Custom("MyType".to_string()).type_name(),
"MyType"
);
}
#[test]
fn test_credential_proof_verification() {
use tenzro_crypto::{KeyPair, KeyType};
use tenzro_crypto::signatures::{Signer, Ed25519SignerImpl};
let keypair = KeyPair::generate(KeyType::Ed25519).unwrap();
let signer = Ed25519SignerImpl::new(keypair).unwrap();
let message = b"test credential data";
let signature = signer.sign(message).unwrap();
let proof = CredentialProof::new(
"Ed25519Signature2020",
"did:tenzro:human:issuer#key-1",
signature.to_bytes(),
);
let result = proof.verify(message, signer.public_key().as_bytes()).unwrap();
assert!(result, "Valid signature should verify");
let wrong_message = b"wrong message";
let result = proof.verify(wrong_message, signer.public_key().as_bytes()).unwrap();
assert!(!result, "Invalid signature should not verify");
}
#[test]
fn test_verifiable_credential_proof_verification() {
use tenzro_crypto::{KeyPair, KeyType};
use tenzro_crypto::signatures::{Signer, Ed25519SignerImpl};
let keypair = KeyPair::generate(KeyType::Ed25519).unwrap();
let signer = Ed25519SignerImpl::new(keypair).unwrap();
let mut cred = VerifiableCredential::new(
TenzroCredentialType::KycAttestation,
"did:tenzro:human:issuer",
"did:tenzro:human:subject",
);
let message = serde_json::to_vec(&cred.credential_subject).unwrap();
let signature = signer.sign(&message).unwrap();
let proof = CredentialProof::new(
"Ed25519Signature2020",
"did:tenzro:human:issuer#key-1",
signature.to_bytes(),
);
cred = cred.with_proof(proof);
let result = cred.verify_proof(signer.public_key().as_bytes()).unwrap();
assert!(result, "Valid credential should verify");
let wrong_keypair = KeyPair::generate(KeyType::Ed25519).unwrap();
let result = cred.verify_proof(wrong_keypair.public_key().as_bytes()).unwrap();
assert!(!result, "Credential with wrong key should not verify");
}
#[test]
fn test_hybrid_credential_sign_and_verify_roundtrip() {
use tenzro_crypto::composite::{HybridSigner, InMemoryHybridSigner};
use tenzro_crypto::pq::MlDsaSigningKey;
use tenzro_crypto::signatures::Ed25519SignerImpl;
use tenzro_crypto::{KeyPair, KeyType};
let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
let classical = Ed25519SignerImpl::new(kp).unwrap();
let signer = InMemoryHybridSigner::new(Box::new(classical), MlDsaSigningKey::generate());
let issuer_pk = signer.public_key().clone();
let cred = VerifiableCredential::new(
TenzroCredentialType::KycAttestation,
"did:tenzro:human:issuer",
"did:tenzro:human:subject",
)
.sign_hybrid(&signer, "did:tenzro:human:issuer#pq-key-1")
.unwrap();
assert!(cred.is_signed());
let proof = cred.proof.as_ref().unwrap();
assert_eq!(proof.proof_type, "HybridEd25519MlDsa65Signature2026");
assert_eq!(
proof.verification_method,
"did:tenzro:human:issuer#pq-key-1"
);
assert!(cred.verify_proof_hybrid(&issuer_pk).unwrap());
let wrong = InMemoryHybridSigner::new(
Box::new(
Ed25519SignerImpl::new(KeyPair::generate(KeyType::Ed25519).unwrap()).unwrap(),
),
MlDsaSigningKey::generate(),
);
assert!(!cred.verify_proof_hybrid(wrong.public_key()).unwrap());
}
#[test]
fn test_hybrid_credential_rejects_legacy_verify_path() {
use tenzro_crypto::composite::{HybridSigner, InMemoryHybridSigner};
use tenzro_crypto::pq::MlDsaSigningKey;
use tenzro_crypto::signatures::Ed25519SignerImpl;
use tenzro_crypto::{KeyPair, KeyType};
let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
let classical = Ed25519SignerImpl::new(kp).unwrap();
let signer = InMemoryHybridSigner::new(Box::new(classical), MlDsaSigningKey::generate());
let cred = VerifiableCredential::new(
TenzroCredentialType::KycAttestation,
"did:tenzro:human:issuer",
"did:tenzro:human:subject",
)
.sign_hybrid(&signer, "did:tenzro:human:issuer#pq-key-1")
.unwrap();
let classical_bytes = signer.public_key().classical.as_bytes().to_vec();
let err = cred.verify_proof(&classical_bytes).unwrap_err();
match err {
crate::error::IdentityError::VerificationFailed(msg) => {
assert!(msg.contains("HybridEd25519MlDsa65Signature2026") || msg.contains("Unsupported"));
}
other => panic!("expected VerificationFailed, got {:?}", other),
}
}
#[test]
fn test_expired_credential_fails_verification() {
use tenzro_crypto::{KeyPair, KeyType};
use tenzro_crypto::signatures::{Signer, Ed25519SignerImpl};
let keypair = KeyPair::generate(KeyType::Ed25519).unwrap();
let signer = Ed25519SignerImpl::new(keypair).unwrap();
let mut cred = VerifiableCredential::new(
TenzroCredentialType::AgeVerification,
"issuer",
"subject",
)
.with_expiration(Utc::now() - chrono::Duration::days(1));
let message = serde_json::to_vec(&cred.credential_subject).unwrap();
let signature = signer.sign(&message).unwrap();
cred = cred.with_proof(CredentialProof::new(
"Ed25519",
"issuer#key-1",
signature.to_bytes(),
));
let result = cred.verify_proof(signer.public_key().as_bytes()).unwrap();
assert!(!result, "Expired credential should not verify");
}
}