use super::DidMethod;
use crate::did::document::VerificationRelationship;
use crate::did::{Did, DidDocument};
use crate::{DidError, DidResult, VerificationMethod};
use async_trait::async_trait;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum IonOperationType {
Create,
Update,
Recover,
Deactivate,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum IonKeyPurpose {
Authentication,
AssertionMethod,
KeyAgreement,
CapabilityInvocation,
CapabilityDelegation,
}
impl IonKeyPurpose {
pub fn as_str(&self) -> &'static str {
match self {
Self::Authentication => "authentication",
Self::AssertionMethod => "assertionMethod",
Self::KeyAgreement => "keyAgreement",
Self::CapabilityInvocation => "capabilityInvocation",
Self::CapabilityDelegation => "capabilityDelegation",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IonKeyDescriptor {
pub id: String,
pub purposes: Vec<IonKeyPurpose>,
pub public_key_jwk: serde_json::Value,
}
impl IonKeyDescriptor {
pub fn ed25519(
id: &str,
public_key_bytes: &[u8],
purposes: Vec<IonKeyPurpose>,
) -> DidResult<Self> {
if public_key_bytes.len() != 32 {
return Err(DidError::InvalidKey(
"Ed25519 public key must be 32 bytes".to_string(),
));
}
let x = URL_SAFE_NO_PAD.encode(public_key_bytes);
let jwk = serde_json::json!({
"kty": "OKP",
"crv": "Ed25519",
"x": x
});
Ok(Self {
id: id.to_string(),
purposes,
public_key_jwk: jwk,
})
}
pub fn secp256k1(
id: &str,
public_key_bytes: &[u8],
purposes: Vec<IonKeyPurpose>,
) -> DidResult<Self> {
if public_key_bytes.len() != 33 {
return Err(DidError::InvalidKey(
"secp256k1 compressed public key must be 33 bytes".to_string(),
));
}
let x = URL_SAFE_NO_PAD.encode(&public_key_bytes[1..]);
let jwk = serde_json::json!({
"kty": "EC",
"crv": "secp256k1",
"x": x
});
Ok(Self {
id: id.to_string(),
purposes,
public_key_jwk: jwk,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IonCreateOperation {
pub recovery_commitment: String,
pub update_commitment: String,
pub document: IonDocument,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IonDocument {
pub public_keys: Vec<IonKeyDescriptor>,
pub services: Vec<IonService>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IonService {
pub id: String,
#[serde(rename = "type")]
pub service_type: String,
pub service_endpoint: serde_json::Value,
}
#[derive(Debug, Clone)]
pub struct DidIon {
pub suffix: String,
pub initial_state: Option<IonCreateOperation>,
pub resolved_document: Option<DidDocument>,
}
impl DidIon {
pub fn new(operation: IonCreateOperation) -> DidResult<Self> {
let suffix = compute_did_suffix(&operation)?;
Ok(Self {
suffix,
initial_state: Some(operation),
resolved_document: None,
})
}
pub fn from_did_string(did: &str) -> DidResult<Self> {
if !did.starts_with("did:ion:") {
return Err(DidError::InvalidFormat(
"DID must start with 'did:ion:'".to_string(),
));
}
let suffix = did["did:ion:".len()..].to_string();
if suffix.is_empty() {
return Err(DidError::InvalidFormat(
"ION DID suffix cannot be empty".to_string(),
));
}
if suffix.contains(':') || suffix.contains('/') {
return Err(DidError::InvalidFormat(
"ION DID suffix must not contain colons or slashes".to_string(),
));
}
Ok(Self {
suffix,
initial_state: None,
resolved_document: None,
})
}
pub fn to_did_string(&self) -> String {
format!("did:ion:{}", self.suffix)
}
pub fn to_did(&self) -> DidResult<Did> {
Did::new(&self.to_did_string())
}
pub fn resolve(&self) -> DidResult<DidDocument> {
if let Some(ref doc) = self.resolved_document {
return Ok(doc.clone());
}
if let Some(ref op) = self.initial_state {
return self.generate_document_from_create_op(op);
}
Err(DidError::ResolutionFailed(format!(
"Cannot resolve did:ion:{} - no initial state or resolved document",
self.suffix
)))
}
pub fn with_resolved_document(mut self, doc: DidDocument) -> Self {
self.resolved_document = Some(doc);
self
}
fn generate_document_from_create_op(&self, op: &IonCreateOperation) -> DidResult<DidDocument> {
let did_str = self.to_did_string();
let did = Did::new(&did_str)?;
let mut doc = DidDocument::new(did);
doc.context = vec![
"https://www.w3.org/ns/did/v1".to_string(),
"https://w3id.org/security/suites/ed25519-2020/v1".to_string(),
"https://w3id.org/security/suites/secp256k1-2019/v1".to_string(),
];
for key_desc in &op.document.public_keys {
let vm_id = format!("{}#{}", did_str, key_desc.id);
let method_type = detect_key_type_from_jwk(&key_desc.public_key_jwk);
let vm = VerificationMethod::jwk(
&vm_id,
&did_str,
&method_type,
key_desc.public_key_jwk.clone(),
);
doc.verification_method.push(vm);
for purpose in &key_desc.purposes {
let rel = VerificationRelationship::Reference(vm_id.clone());
match purpose {
IonKeyPurpose::Authentication => doc.authentication.push(rel),
IonKeyPurpose::AssertionMethod => doc.assertion_method.push(rel),
IonKeyPurpose::KeyAgreement => doc.key_agreement.push(rel),
IonKeyPurpose::CapabilityInvocation => doc.capability_invocation.push(rel),
IonKeyPurpose::CapabilityDelegation => doc.capability_delegation.push(rel),
}
}
}
for svc in &op.document.services {
let endpoint = match &svc.service_endpoint {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
doc.service.push(crate::Service {
id: format!("{}#{}", did_str, svc.id),
service_type: svc.service_type.clone(),
service_endpoint: endpoint,
});
}
Ok(doc)
}
pub fn compute_commitment(key_bytes: &[u8]) -> String {
let inner_hash = sha256(key_bytes);
let mut prefixed = vec![0x00u8];
prefixed.extend_from_slice(&inner_hash);
let commitment = sha256(&prefixed);
URL_SAFE_NO_PAD.encode(&commitment)
}
}
pub struct DidIonMethod;
impl Default for DidIonMethod {
fn default() -> Self {
Self::new()
}
}
impl DidIonMethod {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl DidMethod for DidIonMethod {
fn method_name(&self) -> &str {
"ion"
}
async fn resolve(&self, did: &Did) -> DidResult<DidDocument> {
if !self.supports(did) {
return Err(DidError::UnsupportedMethod(did.method().to_string()));
}
let ion = DidIon::from_did_string(did.as_str())?;
ion.resolve()
}
}
fn sha256(data: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize().to_vec()
}
fn compute_did_suffix(operation: &IonCreateOperation) -> DidResult<String> {
let json = serde_json::to_string(operation)
.map_err(|e| DidError::SerializationError(e.to_string()))?;
let hash = sha256(json.as_bytes());
Ok(URL_SAFE_NO_PAD.encode(&hash))
}
fn detect_key_type_from_jwk(jwk: &serde_json::Value) -> String {
match (jwk.get("kty"), jwk.get("crv")) {
(Some(kty), Some(crv))
if kty.as_str() == Some("OKP") && crv.as_str() == Some("Ed25519") =>
{
"Ed25519VerificationKey2020".to_string()
}
(Some(kty), Some(crv))
if kty.as_str() == Some("EC") && crv.as_str() == Some("secp256k1") =>
{
"EcdsaSecp256k1VerificationKey2019".to_string()
}
(Some(kty), Some(crv)) if kty.as_str() == Some("EC") && crv.as_str() == Some("P-256") => {
"JsonWebKey2020".to_string()
}
(Some(kty), _) if kty.as_str() == Some("RSA") => "JsonWebKey2020".to_string(),
_ => "JsonWebKey2020".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_ion_did() -> (DidIon, IonCreateOperation) {
let key = IonKeyDescriptor::ed25519(
"auth-key",
&[1u8; 32],
vec![
IonKeyPurpose::Authentication,
IonKeyPurpose::AssertionMethod,
],
)
.unwrap();
let service = IonService {
id: "linked-domain".to_string(),
service_type: "LinkedDomains".to_string(),
service_endpoint: serde_json::json!("https://example.com"),
};
let recovery_key = [2u8; 32];
let update_key = [3u8; 32];
let op = IonCreateOperation {
recovery_commitment: DidIon::compute_commitment(&recovery_key),
update_commitment: DidIon::compute_commitment(&update_key),
document: IonDocument {
public_keys: vec![key],
services: vec![service],
},
};
let ion = DidIon::new(op.clone()).unwrap();
(ion, op)
}
#[test]
fn test_create_ion_did() {
let (ion, _) = create_test_ion_did();
assert!(ion.to_did_string().starts_with("did:ion:"));
assert!(!ion.suffix.is_empty());
}
#[test]
fn test_ion_did_suffix_deterministic() {
let (ion1, op) = create_test_ion_did();
let ion2 = DidIon::new(op).unwrap();
assert_eq!(ion1.suffix, ion2.suffix);
}
#[test]
fn test_ion_did_from_string() {
let suffix = "EiClkZMDxPKqC9c-umQceAyopvJFHEWNpTJPCj47A";
let did_str = format!("did:ion:{}", suffix);
let ion = DidIon::from_did_string(&did_str).unwrap();
assert_eq!(ion.suffix, suffix);
assert_eq!(ion.to_did_string(), did_str);
}
#[test]
fn test_ion_did_from_string_invalid() {
assert!(DidIon::from_did_string("did:key:z6Mk123").is_err());
assert!(DidIon::from_did_string("did:ion:").is_err());
}
#[test]
fn test_ion_resolve_with_create_op() {
let (ion, _) = create_test_ion_did();
let doc = ion.resolve().unwrap();
assert_eq!(doc.verification_method.len(), 1);
assert!(!doc.authentication.is_empty());
assert!(!doc.assertion_method.is_empty());
assert_eq!(doc.service.len(), 1);
}
#[test]
fn test_ion_resolve_without_state() {
let ion =
DidIon::from_did_string("did:ion:EiClkZMDxPKqC9c-umQceAyopvJFHEWNpTJPCj47A").unwrap();
assert!(ion.resolve().is_err());
}
#[test]
fn test_ion_key_descriptor_ed25519() {
let key =
IonKeyDescriptor::ed25519("key-1", &[42u8; 32], vec![IonKeyPurpose::Authentication])
.unwrap();
assert_eq!(key.id, "key-1");
assert_eq!(key.public_key_jwk["kty"], "OKP");
assert_eq!(key.public_key_jwk["crv"], "Ed25519");
}
#[test]
fn test_ion_key_descriptor_secp256k1() {
let mut compressed_key = [0u8; 33];
compressed_key[0] = 0x02; let key = IonKeyDescriptor::secp256k1(
"key-1",
&compressed_key,
vec![IonKeyPurpose::Authentication],
)
.unwrap();
assert_eq!(key.public_key_jwk["kty"], "EC");
assert_eq!(key.public_key_jwk["crv"], "secp256k1");
}
#[test]
fn test_ion_key_wrong_size() {
assert!(IonKeyDescriptor::ed25519("k", &[0u8; 31], vec![]).is_err());
assert!(IonKeyDescriptor::secp256k1("k", &[0u8; 32], vec![]).is_err()); }
#[test]
fn test_compute_commitment() {
let commitment1 = DidIon::compute_commitment(&[1u8; 32]);
let commitment2 = DidIon::compute_commitment(&[1u8; 32]);
assert_eq!(commitment1, commitment2);
let commitment3 = DidIon::compute_commitment(&[2u8; 32]);
assert_ne!(commitment1, commitment3);
}
#[test]
fn test_with_resolved_document() {
let ion =
DidIon::from_did_string("did:ion:EiClkZMDxPKqC9c-umQceAyopvJFHEWNpTJPCj47A").unwrap();
let did = Did::new("did:ion:EiClkZMDxPKqC9c-umQceAyopvJFHEWNpTJPCj47A").unwrap();
let pre_resolved_doc = DidDocument::from_key_ed25519(&[0u8; 32]).unwrap();
let ion = ion.with_resolved_document(pre_resolved_doc);
let doc = ion.resolve().unwrap();
assert!(!doc.verification_method.is_empty());
let _ = did; }
#[test]
fn test_detect_key_type_ed25519() {
let jwk = serde_json::json!({ "kty": "OKP", "crv": "Ed25519", "x": "abc" });
assert_eq!(detect_key_type_from_jwk(&jwk), "Ed25519VerificationKey2020");
}
#[test]
fn test_detect_key_type_secp256k1() {
let jwk = serde_json::json!({ "kty": "EC", "crv": "secp256k1", "x": "abc" });
assert_eq!(
detect_key_type_from_jwk(&jwk),
"EcdsaSecp256k1VerificationKey2019"
);
}
#[test]
fn test_detect_key_type_rsa() {
let jwk = serde_json::json!({ "kty": "RSA", "n": "abc", "e": "AQAB" });
assert_eq!(detect_key_type_from_jwk(&jwk), "JsonWebKey2020");
}
#[tokio::test]
async fn test_ion_method_resolver() {
let method = DidIonMethod::new();
assert_eq!(method.method_name(), "ion");
}
#[tokio::test]
async fn test_ion_method_wrong_method_error() {
let method = DidIonMethod::new();
let did = Did::new("did:key:z6Mk123").unwrap();
assert!(method.resolve(&did).await.is_err());
}
#[test]
fn test_multiple_keys_in_document() {
let key1 =
IonKeyDescriptor::ed25519("auth-key", &[1u8; 32], vec![IonKeyPurpose::Authentication])
.unwrap();
let key2 = IonKeyDescriptor::ed25519(
"assert-key",
&[2u8; 32],
vec![
IonKeyPurpose::AssertionMethod,
IonKeyPurpose::CapabilityInvocation,
],
)
.unwrap();
let op = IonCreateOperation {
recovery_commitment: DidIon::compute_commitment(&[10u8; 32]),
update_commitment: DidIon::compute_commitment(&[11u8; 32]),
document: IonDocument {
public_keys: vec![key1, key2],
services: vec![],
},
};
let ion = DidIon::new(op).unwrap();
let doc = ion.resolve().unwrap();
assert_eq!(doc.verification_method.len(), 2);
assert_eq!(doc.authentication.len(), 1);
assert_eq!(doc.assertion_method.len(), 1);
assert_eq!(doc.capability_invocation.len(), 1);
}
#[test]
fn test_ion_service_complex_endpoint() {
let svc = IonService {
id: "id-hub".to_string(),
service_type: "IdentityHub".to_string(),
service_endpoint: serde_json::json!({
"instances": ["https://hub.example.com"]
}),
};
let op = IonCreateOperation {
recovery_commitment: DidIon::compute_commitment(&[10u8; 32]),
update_commitment: DidIon::compute_commitment(&[11u8; 32]),
document: IonDocument {
public_keys: vec![],
services: vec![svc],
},
};
let ion = DidIon::new(op).unwrap();
let doc = ion.resolve().unwrap();
assert_eq!(doc.service.len(), 1);
assert_eq!(doc.service[0].service_type, "IdentityHub");
}
}