use exo_core::{Did, PqPublicKey, PublicKey, Signature, Timestamp, crypto};
use serde::{Deserialize, Serialize};
pub fn did_from_public_key(public_key: &PublicKey) -> exo_core::Result<Did> {
let hash = blake3::hash(public_key.as_bytes());
let encoded = bs58::encode(hash.as_bytes()).into_string();
Did::new(&format!("did:exo:{encoded}"))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthenticationMethod {
pub id: String,
pub method_type: String,
pub public_key: PublicKey,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceEndpoint {
pub id: String,
pub service_type: String,
pub endpoint: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerificationMethod {
pub id: String,
pub key_type: String,
pub controller: Did,
pub public_key_multibase: String,
pub version: u64,
pub active: bool,
pub valid_from: u64,
pub revoked_at: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HybridVerificationMethod {
pub id: String,
pub key_type: String,
pub controller: Did,
pub classical_public_key_multibase: String,
pub pq_public_key_multibase: String,
pub pq_public_key: PqPublicKey,
pub classical_public_key: PublicKey,
pub version: u64,
pub active: bool,
pub valid_from: u64,
pub revoked_at: Option<u64>,
}
fn decode_base58btc_multibase_key(public_key_multibase: &str) -> Option<Vec<u8>> {
let encoded = public_key_multibase.strip_prefix('z')?;
bs58::decode(encoded).into_vec().ok()
}
impl HybridVerificationMethod {
#[must_use]
pub fn key_material_matches_multibase(&self) -> bool {
let Some(classical_public_key) =
decode_base58btc_multibase_key(&self.classical_public_key_multibase)
else {
return false;
};
let Some(pq_public_key) = decode_base58btc_multibase_key(&self.pq_public_key_multibase)
else {
return false;
};
classical_public_key.as_slice() == self.classical_public_key.as_bytes()
&& pq_public_key.as_slice() == self.pq_public_key.as_bytes()
}
#[must_use]
pub fn verify(&self, message: &[u8], signature: &Signature) -> bool {
if !self.active {
return false;
}
if !self.key_material_matches_multibase() {
return false;
}
crypto::verify_hybrid(
message,
signature,
&self.classical_public_key,
&self.pq_public_key,
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RevocationProof {
pub did: Did,
pub signature: Signature,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DidRegistrationProof {
pub public_key: PublicKey,
pub signature: Signature,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DidDocument {
pub id: Did,
pub public_keys: Vec<PublicKey>,
pub authentication: Vec<AuthenticationMethod>,
#[serde(default)]
pub verification_methods: Vec<VerificationMethod>,
#[serde(default)]
pub hybrid_verification_methods: Vec<HybridVerificationMethod>,
pub service_endpoints: Vec<ServiceEndpoint>,
pub created: Timestamp,
pub updated: Timestamp,
pub revoked: bool,
}
#[cfg(test)]
mod tests {
use bs58;
use exo_core::crypto::{generate_keypair, sign};
use super::*;
use crate::{
error::IdentityError,
registry::{
DidRegistry, LocalDidRegistry, key_rotation_proof_payload, revocation_proof_payload,
},
};
fn make_did(label: &str) -> Did {
Did::new(&format!("did:exo:{label}")).expect("valid did")
}
fn make_doc(did: Did, pk: PublicKey) -> DidDocument {
DidDocument {
id: did,
public_keys: vec![pk],
authentication: vec![],
verification_methods: vec![],
hybrid_verification_methods: vec![],
service_endpoints: vec![],
created: Timestamp::new(1000, 0),
updated: Timestamp::new(1000, 0),
revoked: false,
}
}
#[test]
fn did_from_public_key_is_deterministic_and_key_bound() {
let (first_pk, _first_sk) = generate_keypair();
let (second_pk, _second_sk) = generate_keypair();
let first = did_from_public_key(&first_pk).unwrap();
let repeat = did_from_public_key(&first_pk).unwrap();
let second = did_from_public_key(&second_pk).unwrap();
assert_eq!(first, repeat);
assert_ne!(first, second);
assert!(first.as_str().starts_with("did:exo:"));
}
fn rotation_signature(
did: &Did,
new_key: &PublicKey,
updated: Timestamp,
secret_key: &exo_core::SecretKey,
) -> Signature {
let payload = key_rotation_proof_payload(did, new_key, updated).expect("rotation payload");
sign(&payload, secret_key)
}
#[test]
fn register_and_resolve() {
let (pk, _sk) = generate_keypair();
let did = make_did("alice");
let doc = make_doc(did.clone(), pk);
let mut reg = LocalDidRegistry::new();
reg.register(doc).unwrap();
assert_eq!(reg.len(), 1);
assert!(!reg.is_empty());
let resolved = reg.resolve(&did).unwrap();
assert_eq!(resolved.id, did);
}
#[test]
fn duplicate_registration_fails() {
let (pk, _sk) = generate_keypair();
let did = make_did("bob");
let doc = make_doc(did.clone(), pk);
let mut reg = LocalDidRegistry::new();
reg.register(doc.clone()).unwrap();
let err = reg.register(doc).unwrap_err();
assert!(matches!(err, IdentityError::DuplicateDid(_)));
}
#[test]
fn resolve_unknown_did_returns_none() {
let reg = LocalDidRegistry::new();
assert!(reg.is_empty());
let did = make_did("nonexistent");
assert!(reg.resolve(&did).is_none());
}
#[test]
fn revoke_did() {
let (pk, sk) = generate_keypair();
let did = make_did("charlie");
let doc = make_doc(did.clone(), pk);
let mut reg = LocalDidRegistry::new();
reg.register(doc).unwrap();
let payload = revocation_proof_payload(&did).expect("revocation payload");
let proof = RevocationProof {
did: did.clone(),
signature: sign(&payload, &sk),
};
reg.revoke(&did, &proof).unwrap();
assert!(reg.resolve(&did).is_none());
}
#[test]
fn revoke_unknown_did_fails() {
let (_pk, sk) = generate_keypair();
let did = make_did("unknown");
let payload = revocation_proof_payload(&did).expect("revocation payload");
let proof = RevocationProof {
did: did.clone(),
signature: sign(&payload, &sk),
};
let mut reg = LocalDidRegistry::new();
let err = reg.revoke(&did, &proof).unwrap_err();
assert!(matches!(err, IdentityError::DidNotFound(_)));
}
#[test]
fn revoke_with_invalid_proof_fails() {
let (pk, _sk) = generate_keypair();
let did = make_did("dave");
let doc = make_doc(did.clone(), pk);
let mut reg = LocalDidRegistry::new();
reg.register(doc).unwrap();
let (_pk2, sk2) = generate_keypair();
let payload = revocation_proof_payload(&did).expect("revocation payload");
let proof = RevocationProof {
did: did.clone(),
signature: sign(&payload, &sk2),
};
let err = reg.revoke(&did, &proof).unwrap_err();
assert!(matches!(err, IdentityError::InvalidRevocationProof(_)));
}
#[test]
fn rotate_key_success() {
let (pk, sk) = generate_keypair();
let did = make_did("eve");
let doc = make_doc(did.clone(), pk);
let mut reg = LocalDidRegistry::new();
reg.register(doc).unwrap();
let (new_pk, _new_sk) = generate_keypair();
let proof = rotation_signature(&did, &new_pk, Timestamp::new(1001, 0), &sk);
reg.rotate_key(&did, &new_pk, &proof, Timestamp::new(1001, 0))
.unwrap();
let resolved = reg.resolve(&did).unwrap();
assert_eq!(resolved.public_keys, vec![new_pk]);
assert!(resolved.updated.physical_ms > 1000);
}
#[test]
fn rotate_key_unknown_did_fails() {
let (_pk, sk) = generate_keypair();
let did = make_did("unknown2");
let (new_pk, _) = generate_keypair();
let proof = rotation_signature(&did, &new_pk, Timestamp::new(1001, 0), &sk);
let mut reg = LocalDidRegistry::new();
let err = reg
.rotate_key(&did, &new_pk, &proof, Timestamp::new(1001, 0))
.unwrap_err();
assert!(matches!(err, IdentityError::DidNotFound(_)));
}
#[test]
fn rotate_key_revoked_did_fails() {
let (pk, sk) = generate_keypair();
let did = make_did("frank");
let doc = make_doc(did.clone(), pk);
let mut reg = LocalDidRegistry::new();
reg.register(doc).unwrap();
let payload = revocation_proof_payload(&did).expect("revocation payload");
let revocation = RevocationProof {
did: did.clone(),
signature: sign(&payload, &sk),
};
reg.revoke(&did, &revocation).unwrap();
let (new_pk, _) = generate_keypair();
let proof = rotation_signature(&did, &new_pk, Timestamp::new(1001, 0), &sk);
let err = reg
.rotate_key(&did, &new_pk, &proof, Timestamp::new(1001, 0))
.unwrap_err();
assert!(matches!(err, IdentityError::DidRevoked(_)));
}
#[test]
fn rotate_key_invalid_signature_fails() {
let (pk, _sk) = generate_keypair();
let did = make_did("grace");
let doc = make_doc(did.clone(), pk);
let mut reg = LocalDidRegistry::new();
reg.register(doc).unwrap();
let (new_pk, _) = generate_keypair();
let (_other_pk, other_sk) = generate_keypair();
let bad_proof = rotation_signature(&did, &new_pk, Timestamp::new(1001, 0), &other_sk);
let err = reg
.rotate_key(&did, &new_pk, &bad_proof, Timestamp::new(1001, 0))
.unwrap_err();
assert!(matches!(err, IdentityError::InvalidSignature));
}
#[test]
fn authentication_method_and_service_endpoint() {
let (pk, _sk) = generate_keypair();
let did = make_did("heidi");
let doc = DidDocument {
id: did.clone(),
public_keys: vec![pk],
authentication: vec![AuthenticationMethod {
id: "auth-1".into(),
method_type: "Ed25519VerificationKey2020".into(),
public_key: pk,
}],
verification_methods: vec![],
hybrid_verification_methods: vec![],
service_endpoints: vec![ServiceEndpoint {
id: "svc-1".into(),
service_type: "ExochainMessaging".into(),
endpoint: "https://example.com/msg".into(),
}],
created: Timestamp::new(1000, 0),
updated: Timestamp::new(1000, 0),
revoked: false,
};
let mut reg = LocalDidRegistry::new();
reg.register(doc).unwrap();
let resolved = reg.resolve(&did).unwrap();
assert_eq!(resolved.authentication.len(), 1);
assert_eq!(resolved.authentication[0].id, "auth-1");
assert_eq!(resolved.service_endpoints.len(), 1);
assert_eq!(
resolved.service_endpoints[0].endpoint,
"https://example.com/msg"
);
}
#[test]
fn list_dids_returns_sorted_ids() {
let (pk1, _) = generate_keypair();
let (pk2, _) = generate_keypair();
let mut reg = LocalDidRegistry::new();
reg.register(make_doc(make_did("charlie"), pk1)).unwrap();
reg.register(make_doc(make_did("alice"), pk2)).unwrap();
let dids = reg.list_dids();
assert_eq!(dids, vec!["did:exo:alice", "did:exo:charlie"]);
}
fn make_hybrid_method(
did: &Did,
pk: PublicKey,
pq_pk: PqPublicKey,
) -> HybridVerificationMethod {
let classical_mb = format!("z{}", bs58::encode(pk.as_bytes()).into_string());
let pq_mb = format!("z{}", bs58::encode(pq_pk.as_bytes()).into_string());
HybridVerificationMethod {
id: format!("{}#hybrid-key-1", did.as_str()),
key_type: "HybridKeyEd25519MlDsa652020".into(),
controller: did.clone(),
classical_public_key_multibase: classical_mb,
pq_public_key_multibase: pq_mb,
pq_public_key: pq_pk,
classical_public_key: pk,
version: 1,
active: true,
valid_from: 1000,
revoked_at: None,
}
}
#[test]
fn hybrid_method_verify_roundtrip() {
use exo_core::crypto::{generate_pq_keypair, sign_hybrid};
let (classical_pk, classical_sk) = generate_keypair();
let (pq_pk, pq_sk) = generate_pq_keypair();
let did = make_did("hybrid-alice");
let method = make_hybrid_method(&did, classical_pk, pq_pk);
let message = b"hybrid DID verification";
let sig = sign_hybrid(message, &classical_sk, &pq_sk).expect("sign_hybrid");
assert!(
method.verify(message, &sig),
"HybridVerificationMethod::verify must accept valid Hybrid signature"
);
}
#[test]
fn hybrid_method_verify_rejects_mismatched_multibase_key_material() {
use exo_core::crypto::{generate_pq_keypair, sign_hybrid};
let (classical_pk, classical_sk) = generate_keypair();
let (declared_classical_pk, _declared_classical_sk) = generate_keypair();
let (pq_pk, pq_sk) = generate_pq_keypair();
let (declared_pq_pk, _declared_pq_sk) = generate_pq_keypair();
let did = make_did("hybrid-key-alias");
let mut method = make_hybrid_method(&did, classical_pk, pq_pk);
method.classical_public_key_multibase = format!(
"z{}",
bs58::encode(declared_classical_pk.as_bytes()).into_string()
);
method.pq_public_key_multibase =
format!("z{}", bs58::encode(declared_pq_pk.as_bytes()).into_string());
let message = b"hybrid DID verification key alias";
let sig = sign_hybrid(message, &classical_sk, &pq_sk).expect("sign_hybrid");
assert!(
!method.verify(message, &sig),
"hybrid verification must reject raw keys that do not match the advertised multibase keys"
);
}
#[test]
fn hybrid_method_verify_fails_wrong_message() {
use exo_core::crypto::{generate_pq_keypair, sign_hybrid};
let (classical_pk, classical_sk) = generate_keypair();
let (pq_pk, pq_sk) = generate_pq_keypair();
let did = make_did("hybrid-bob");
let method = make_hybrid_method(&did, classical_pk, pq_pk);
let sig = sign_hybrid(b"original", &classical_sk, &pq_sk).expect("sign_hybrid");
assert!(!method.verify(b"tampered", &sig));
}
#[test]
fn hybrid_method_verify_fails_wrong_key() {
use exo_core::crypto::{generate_pq_keypair, sign_hybrid};
let (_pk1, sk1) = generate_keypair();
let (pk2, _sk2) = generate_keypair();
let (_pq1, pq_sk1) = generate_pq_keypair();
let (pq2, _pq_sk2) = generate_pq_keypair();
let did = make_did("hybrid-carol");
let method = make_hybrid_method(&did, pk2, pq2);
let sig = sign_hybrid(b"msg", &sk1, &pq_sk1).expect("sign_hybrid");
assert!(!method.verify(b"msg", &sig));
}
#[test]
fn hybrid_method_inactive_always_fails() {
use exo_core::crypto::{generate_pq_keypair, sign_hybrid};
let (classical_pk, classical_sk) = generate_keypair();
let (pq_pk, pq_sk) = generate_pq_keypair();
let did = make_did("hybrid-dave");
let mut method = make_hybrid_method(&did, classical_pk, pq_pk);
method.active = false;
let sig = sign_hybrid(b"msg", &classical_sk, &pq_sk).expect("sign_hybrid");
assert!(
!method.verify(b"msg", &sig),
"inactive hybrid method must always reject verification"
);
}
#[test]
fn hybrid_method_rejects_non_hybrid_signature() {
use exo_core::crypto::generate_pq_keypair;
let (classical_pk, classical_sk) = generate_keypair();
let (pq_pk, _pq_sk) = generate_pq_keypair();
let did = make_did("hybrid-eve");
let method = make_hybrid_method(&did, classical_pk, pq_pk);
let ed_sig = exo_core::crypto::sign(b"msg", &classical_sk);
assert!(
!method.verify(b"msg", &ed_sig),
"hybrid method must reject plain Ed25519 signature (no downgrade)"
);
}
#[test]
fn did_document_with_hybrid_method_roundtrip() {
use exo_core::crypto::{generate_pq_keypair, sign_hybrid};
let (classical_pk, classical_sk) = generate_keypair();
let (pq_pk, pq_sk) = generate_pq_keypair();
let did = make_did("hybrid-frank");
let hybrid_method = make_hybrid_method(&did, classical_pk, pq_pk);
let doc = DidDocument {
id: did.clone(),
public_keys: vec![classical_pk],
authentication: vec![],
verification_methods: vec![],
hybrid_verification_methods: vec![hybrid_method],
service_endpoints: vec![],
created: Timestamp::new(1000, 0),
updated: Timestamp::new(1000, 0),
revoked: false,
};
let mut reg = LocalDidRegistry::new();
reg.register(doc).unwrap();
let resolved = reg.resolve(&did).unwrap();
assert_eq!(resolved.hybrid_verification_methods.len(), 1);
let method = &resolved.hybrid_verification_methods[0];
let msg = b"hybrid DID doc test";
let sig = sign_hybrid(msg, &classical_sk, &pq_sk).expect("sign_hybrid");
assert!(method.verify(msg, &sig));
}
}