diff --git a/Cargo.lock b/Cargo.lock index fa86bb60ea..4dd6db0bcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -550,9 +550,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "base64ct" @@ -675,6 +675,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +[[package]] +name = "bs58" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.12.2" @@ -1371,14 +1380,28 @@ dependencies = [ name = "did_doc" version = "0.1.0" dependencies = [ + "base64 0.21.2", + "bs58 0.5.0", "did_parser", + "hex", "multibase", + "pem 2.0.1", "serde", "serde_json", "uniresid", "url", ] +[[package]] +name = "did_doc_sov" +version = "0.1.0" +dependencies = [ + "did_doc", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "did_parser" version = "0.1.0" @@ -1415,6 +1438,7 @@ dependencies = [ "aries_vcx_core", "async-trait", "chrono", + "did_doc_sov", "did_resolver", "mockall", "serde", @@ -3224,6 +3248,16 @@ dependencies = [ "regex", ] +[[package]] +name = "pem" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b13fe415cdf3c8e44518e18a7c95a13431d9bdf6d15367d82b23c377fdd441a" +dependencies = [ + "base64 0.21.2", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.2.4" @@ -3686,7 +3720,7 @@ version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64 0.21.0", + "base64 0.21.2", "bytes", "encoding_rs", "futures-core", @@ -3778,7 +3812,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "pem", + "pem 0.8.3", "rand 0.8.5", "simple_asn1", "subtle", diff --git a/Cargo.toml b/Cargo.toml index 6c25befb23..35eacab4f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "aries_vcx_core", "uniffi_aries_vcx/core", "did_doc", + "did_doc_sov", "did_parser", "did_resolver", "did_resolver_registry", diff --git a/did_doc/Cargo.toml b/did_doc/Cargo.toml index aeae41512a..38aa946160 100644 --- a/did_doc/Cargo.toml +++ b/did_doc/Cargo.toml @@ -4,8 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] +base64 = "0.21.2" +bs58 = "0.5.0" did_parser = { path = "../did_parser" } +hex = "0.4.3" multibase = "0.9.1" +pem = "2.0.1" serde = { version = "1.0.159", default-features = false, features = ["derive"] } serde_json = "1.0.95" uniresid = { version = "0.1.4", default-features = false, features = ["serde"] } diff --git a/did_doc/src/error.rs b/did_doc/src/error.rs index b1c623fdd3..d1cacb7dd1 100644 --- a/did_doc/src/error.rs +++ b/did_doc/src/error.rs @@ -4,7 +4,12 @@ use url::ParseError; pub enum DidDocumentBuilderError { InvalidInput(String), MissingField(&'static str), + UnsupportedPublicKeyField(&'static str), JsonError(serde_json::Error), + PemError(pem::PemError), + Base58DecodeError(bs58::decode::Error), + Base64DecodeError(base64::DecodeError), + HexDecodeError(hex::FromHexError), } impl std::fmt::Display for DidDocumentBuilderError { @@ -16,9 +21,24 @@ impl std::fmt::Display for DidDocumentBuilderError { DidDocumentBuilderError::MissingField(field) => { write!(f, "Missing field: {}", field) } + DidDocumentBuilderError::UnsupportedPublicKeyField(field) => { + write!(f, "Unsupported public key field: {}", field) + } DidDocumentBuilderError::JsonError(error) => { write!(f, "(De)serialization error: {}", error) } + DidDocumentBuilderError::PemError(error) => { + write!(f, "PEM error: {}", error) + } + DidDocumentBuilderError::Base58DecodeError(error) => { + write!(f, "Base58 decode error: {}", error) + } + DidDocumentBuilderError::Base64DecodeError(error) => { + write!(f, "Base64 decode error: {}", error) + } + DidDocumentBuilderError::HexDecodeError(error) => { + write!(f, "Hex decode error: {}", error) + } } } } @@ -26,9 +46,12 @@ impl std::fmt::Display for DidDocumentBuilderError { impl std::error::Error for DidDocumentBuilderError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { - DidDocumentBuilderError::InvalidInput(_) => None, - DidDocumentBuilderError::MissingField(_) => None, DidDocumentBuilderError::JsonError(error) => Some(error), + DidDocumentBuilderError::PemError(error) => Some(error), + DidDocumentBuilderError::Base58DecodeError(error) => Some(error), + DidDocumentBuilderError::Base64DecodeError(error) => Some(error), + DidDocumentBuilderError::HexDecodeError(error) => Some(error), + _ => None, } } } @@ -39,6 +62,30 @@ impl From for DidDocumentBuilderError { } } +impl From for DidDocumentBuilderError { + fn from(error: pem::PemError) -> Self { + DidDocumentBuilderError::PemError(error) + } +} + +impl From for DidDocumentBuilderError { + fn from(error: bs58::decode::Error) -> Self { + DidDocumentBuilderError::Base58DecodeError(error) + } +} + +impl From for DidDocumentBuilderError { + fn from(error: base64::DecodeError) -> Self { + DidDocumentBuilderError::Base64DecodeError(error) + } +} + +impl From for DidDocumentBuilderError { + fn from(error: hex::FromHexError) -> Self { + DidDocumentBuilderError::HexDecodeError(error) + } +} + impl From for DidDocumentBuilderError { fn from(error: ParseError) -> Self { DidDocumentBuilderError::InvalidInput(error.to_string()) diff --git a/did_doc/src/schema/did_doc.rs b/did_doc/src/schema/did_doc.rs index 2b34e8fc78..3dfa8c6399 100644 --- a/did_doc/src/schema/did_doc.rs +++ b/did_doc/src/schema/did_doc.rs @@ -13,7 +13,7 @@ use super::{ verification_method::{VerificationMethod, VerificationMethodKind}, }; -type ControllerAlias = OneOrList; +pub type ControllerAlias = OneOrList; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] #[serde(default)] @@ -92,6 +92,13 @@ impl DidDocument { self.extra.get(key) } + pub fn dereference_key(&self, reference: &DidUrl) -> Option<&VerificationMethod> { + // TODO: Should check controller (if present) + self.verification_method + .iter() + .find(|vm| vm.id().fragment() == reference.fragment()) + } + pub fn validate(&self) -> Result<(), DidDocumentBuilderError> { Ok(()) } @@ -183,7 +190,7 @@ impl DidDocumentBuilder { self } - pub fn add_key_agreement_refrence(mut self, reference: DidUrl) -> Self { + pub fn add_key_agreement_reference(mut self, reference: DidUrl) -> Self { self.key_agreement .push(VerificationMethodKind::Resolvable(reference)); self @@ -195,7 +202,7 @@ impl DidDocumentBuilder { self } - pub fn add_capability_invocation_refrence(mut self, reference: DidUrl) -> Self { + pub fn add_capability_invocation_reference(mut self, reference: DidUrl) -> Self { self.capability_invocation .push(VerificationMethodKind::Resolvable(reference)); self @@ -248,7 +255,7 @@ impl DidDocumentBuilder { #[cfg(test)] mod tests { use super::*; - use crate::schema::service::ServiceBuilder; + use crate::schema::{service::ServiceBuilder, verification_method::VerificationMethodType}; #[test] fn test_did_document_builder() { @@ -256,18 +263,21 @@ mod tests { let also_known_as = Uri::new("https://example.com").unwrap(); let controller = Did::parse("did:example:controller".to_string()).unwrap(); + let vm1_id = DidUrl::parse("did:example:vm1#vm1".to_string()).unwrap(); let verification_method = VerificationMethod::builder( - DidUrl::parse("did:example:vm1".to_string()).unwrap(), - Did::parse("did:example:vm2".to_string()).unwrap(), - "typevm".to_string(), + vm1_id.clone(), + Did::parse("did:example:vm1".to_string()).unwrap(), + VerificationMethodType::Ed25519VerificationKey2018, ) + .add_public_key_base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".to_string()) .build(); let authentication_reference = DidUrl::parse("did:example:authref".to_string()).unwrap(); let assertion_method = VerificationMethod::builder( DidUrl::parse("did:example:am1".to_string()).unwrap(), Did::parse("did:example:am2".to_string()).unwrap(), - "typeam".to_string(), + VerificationMethodType::Ed25519VerificationKey2018, ) + .add_public_key_base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".to_string()) .build(); let service_id = Uri::new("did:example:123456789abcdefghi;service-1").unwrap(); @@ -291,9 +301,9 @@ mod tests { .add_assertion_method(assertion_method.clone()) .add_assertion_method_reference(authentication_reference.clone()) .add_key_agreement(verification_method.clone()) - .add_key_agreement_refrence(authentication_reference.clone()) + .add_key_agreement_reference(authentication_reference.clone()) .add_capability_invocation(verification_method.clone()) - .add_capability_invocation_refrence(authentication_reference.clone()) + .add_capability_invocation_reference(authentication_reference.clone()) .add_capability_delegation(verification_method.clone()) .add_capability_delegation_refrence(authentication_reference.clone()) .add_service(service.clone()) @@ -345,5 +355,12 @@ mod tests { ] ); assert_eq!(document.service(), &[service]); + + let vm = document.dereference_key(&vm1_id); + if let Some(vm) = vm { + assert_eq!(vm.id(), &vm1_id); + } else { + panic!("Verification method not found") + }; } } diff --git a/did_doc/src/schema/mod.rs b/did_doc/src/schema/mod.rs index f20046f575..bca3088b80 100644 --- a/did_doc/src/schema/mod.rs +++ b/did_doc/src/schema/mod.rs @@ -1,5 +1,5 @@ pub mod did_doc; pub mod service; pub mod types; -pub(crate) mod utils; +pub mod utils; pub mod verification_method; diff --git a/did_doc/src/schema/service.rs b/did_doc/src/schema/service.rs index 5323048e3e..e1cb6ae3df 100644 --- a/did_doc/src/schema/service.rs +++ b/did_doc/src/schema/service.rs @@ -9,7 +9,7 @@ use super::{ utils::OneOrList, }; -type ServiceTypeAlias = OneOrList; +pub type ServiceTypeAlias = OneOrList; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(rename_all = "camelCase")] diff --git a/did_doc/src/schema/types/jsonwebkey.rs b/did_doc/src/schema/types/jsonwebkey.rs index 3748348518..ce33744901 100644 --- a/did_doc/src/schema/types/jsonwebkey.rs +++ b/did_doc/src/schema/types/jsonwebkey.rs @@ -27,6 +27,10 @@ impl JsonWebKey { pub fn new(jwk: &str) -> Result { Ok(serde_json::from_str(jwk)?) } + + pub fn to_vec(&self) -> Result, DidDocumentBuilderError> { + serde_json::to_vec(self).map_err(|e| e.into()) + } } impl FromStr for JsonWebKey { diff --git a/did_doc/src/schema/verification_method.rs b/did_doc/src/schema/verification_method.rs index cba8c27f61..d311036a51 100644 --- a/did_doc/src/schema/verification_method.rs +++ b/did_doc/src/schema/verification_method.rs @@ -1,8 +1,8 @@ -use std::collections::HashMap; - +use base64::{engine::general_purpose, Engine}; use did_parser::{Did, DidUrl}; use serde::{Deserialize, Serialize}; -use serde_json::Value; + +use crate::error::DidDocumentBuilderError; use super::types::{jsonwebkey::JsonWebKey, multibase::Multibase}; @@ -15,30 +15,79 @@ pub enum VerificationMethodKind { Resolvable(DidUrl), } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(untagged)] +#[serde(deny_unknown_fields)] +pub enum PublicKeyField { + #[serde(rename_all = "camelCase")] + Multibase { public_key_multibase: Multibase }, + #[serde(rename_all = "camelCase")] + Jwk { public_key_jwk: JsonWebKey }, + #[serde(rename_all = "camelCase")] + Base58 { public_key_base58: String }, + #[serde(rename_all = "camelCase")] + Base64 { public_key_base64: String }, + #[serde(rename_all = "camelCase")] + Hex { public_key_hex: String }, + #[serde(rename_all = "camelCase")] + Pem { public_key_pem: String }, + #[serde(rename_all = "camelCase")] + Pgp { public_key_pgp: String }, +} + +impl PublicKeyField { + pub fn key_decoded(&self) -> Result, DidDocumentBuilderError> { + match self { + PublicKeyField::Multibase { + public_key_multibase, + } => Ok(public_key_multibase.as_ref().to_vec()), + PublicKeyField::Jwk { public_key_jwk } => public_key_jwk.to_vec(), + PublicKeyField::Base58 { public_key_base58 } => { + Ok(bs58::decode(public_key_base58).into_vec()?) + } + PublicKeyField::Base64 { public_key_base64 } => { + Ok(general_purpose::STANDARD.decode(public_key_base64.as_bytes())?) + } + PublicKeyField::Hex { public_key_hex } => Ok(hex::decode(public_key_hex)?), + PublicKeyField::Pem { public_key_pem } => { + Ok(pem::parse(public_key_pem.as_bytes())?.contents().to_vec()) + } + PublicKeyField::Pgp { public_key_pgp: _ } => Err( + DidDocumentBuilderError::UnsupportedPublicKeyField("publicKeyPgp"), + ), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum VerificationMethodType { + JsonWebKey2020, + EcdsaSecp256k1VerificationKey2019, + Ed25519VerificationKey2018, + Bls12381G1Key2020, + Bls12381G2Key2020, + PgpVerificationKey2021, + RsaVerificationKey2018, + X25519KeyAgreementKey2019, + EcdsaSecp256k1RecoveryMethod2020, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub struct VerificationMethod { id: DidUrl, controller: Did, #[serde(rename = "type")] - verification_method_type: String, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(default)] - public_key_multibase: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(default)] - public_key_jwk: Option, + verification_method_type: VerificationMethodType, #[serde(flatten)] - #[serde(skip_serializing_if = "HashMap::is_empty")] - #[serde(default)] - extra: HashMap, + public_key: PublicKeyField, } impl VerificationMethod { pub fn builder( id: DidUrl, controller: Did, - verification_method_type: String, + verification_method_type: VerificationMethodType, ) -> IncompleteVerificationMethodBuilder { IncompleteVerificationMethodBuilder::new(id, controller, verification_method_type) } @@ -51,48 +100,40 @@ impl VerificationMethod { &self.controller } - pub fn verification_method_type(&self) -> &str { - self.verification_method_type.as_ref() - } - - pub fn public_key_multibase(&self) -> Option<&Multibase> { - self.public_key_multibase.as_ref() - } - - pub fn public_key_jwk(&self) -> Option<&JsonWebKey> { - self.public_key_jwk.as_ref() + pub fn verification_method_type(&self) -> &VerificationMethodType { + &self.verification_method_type } - pub fn extra_field(&self, key: &str) -> Option<&Value> { - self.extra.get(key) + pub fn public_key(&self) -> &PublicKeyField { + &self.public_key } } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct IncompleteVerificationMethodBuilder { id: DidUrl, controller: Did, - verification_method_type: String, - extra: HashMap, + verification_method_type: VerificationMethodType, } #[derive(Debug)] pub struct CompleteVerificationMethodBuilder { id: DidUrl, controller: Did, - verification_method_type: String, - public_key_multibase: Option, - public_key_jwk: Option, - extra: HashMap, + verification_method_type: VerificationMethodType, + public_key: Option, } impl IncompleteVerificationMethodBuilder { - pub fn new(id: DidUrl, controller: Did, verification_method_type: String) -> Self { + pub fn new( + id: DidUrl, + controller: Did, + verification_method_type: VerificationMethodType, + ) -> Self { Self { id, verification_method_type, controller, - ..Default::default() } } @@ -104,9 +145,9 @@ impl IncompleteVerificationMethodBuilder { id: self.id, controller: self.controller, verification_method_type: self.verification_method_type, - public_key_multibase: Some(public_key_multibase), - public_key_jwk: None, - extra: self.extra, + public_key: Some(PublicKeyField::Multibase { + public_key_multibase, + }), } } @@ -118,43 +159,60 @@ impl IncompleteVerificationMethodBuilder { id: self.id, controller: self.controller, verification_method_type: self.verification_method_type, - public_key_multibase: None, - public_key_jwk: Some(public_key_jwk), - extra: self.extra, + public_key: Some(PublicKeyField::Jwk { public_key_jwk }), } } - pub fn add_extra_field(mut self, key: String, value: Value) -> Self { - self.extra.insert(key, value); - self + pub fn add_public_key_base58( + self, + public_key_base58: String, + ) -> CompleteVerificationMethodBuilder { + CompleteVerificationMethodBuilder { + id: self.id, + controller: self.controller, + verification_method_type: self.verification_method_type, + public_key: Some(PublicKeyField::Base58 { public_key_base58 }), + } } - pub fn build(self) -> VerificationMethod { - VerificationMethod { + pub fn add_public_key_base64( + self, + public_key_base64: String, + ) -> CompleteVerificationMethodBuilder { + CompleteVerificationMethodBuilder { id: self.id, controller: self.controller, verification_method_type: self.verification_method_type, - public_key_multibase: None, - public_key_jwk: None, - extra: self.extra, + public_key: Some(PublicKeyField::Base64 { public_key_base64 }), } } -} -impl CompleteVerificationMethodBuilder { - pub fn add_extra_field(mut self, key: String, value: Value) -> Self { - self.extra.insert(key, value); - self + pub fn add_public_key_hex(self, public_key_hex: String) -> CompleteVerificationMethodBuilder { + CompleteVerificationMethodBuilder { + id: self.id, + controller: self.controller, + verification_method_type: self.verification_method_type, + public_key: Some(PublicKeyField::Hex { public_key_hex }), + } } + pub fn add_public_key_pem(self, public_key_pem: String) -> CompleteVerificationMethodBuilder { + CompleteVerificationMethodBuilder { + id: self.id, + controller: self.controller, + verification_method_type: self.verification_method_type, + public_key: Some(PublicKeyField::Pem { public_key_pem }), + } + } +} + +impl CompleteVerificationMethodBuilder { pub fn build(self) -> VerificationMethod { VerificationMethod { id: self.id, controller: self.controller, verification_method_type: self.verification_method_type, - public_key_multibase: self.public_key_multibase, - public_key_jwk: self.public_key_jwk, - extra: self.extra, + public_key: self.public_key.unwrap(), // SAFETY: The builder will always set the public key } } } @@ -162,6 +220,7 @@ impl CompleteVerificationMethodBuilder { #[cfg(test)] mod tests { use super::*; + use serde_json::Value; fn create_valid_did() -> Did { Did::parse("did:example:123456789abcdefghi".to_string()).unwrap() @@ -175,8 +234,31 @@ mod tests { Multibase::new("zQmWvQxTqbG2Z9HPJgG57jjwR154cKhbtJenbyYTWkjgF3e".to_string()).unwrap() } - fn create_valid_verification_key_type() -> String { - "Ed25519VerificationKey2018".to_string() + fn create_valid_verification_key_type() -> VerificationMethodType { + VerificationMethodType::Ed25519VerificationKey2018 + } + + fn create_valid_verification_method_value() -> Value { + serde_json::json!({ + "id": "did:example:123456789abcdefghi#key-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:example:123456789abcdefghi", + "publicKeyMultibase": "zQmWvQxTqbG2Z9HPJgG57jjwR154cKhbtJenbyYTWkjgF3e" + }) + } + + fn create_verification_method_multiple_keys() -> Value { + serde_json::json!({ + "id": "did:example:123456789abcdefghi#key-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:example:123456789abcdefghi", + "publicKeyMultibase": "zQmWvQxTqbG2Z9HPJgG57jjwR154cKhbtJenbyYTWkjgF3e", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "zQmWvQxTqbG2Z9HPJgG57jjwR154cKhbtJenbyYTWkjgF3e" + } + }) } #[test] @@ -209,21 +291,14 @@ mod tests { assert_eq!(vm.id(), &id); assert_eq!(vm.controller(), &controller); assert_eq!(vm.verification_method_type(), &verification_method_type); - assert_eq!(vm.public_key_multibase().unwrap(), &public_key_multibase); - } - - #[test] - fn test_verification_method_extra() { - let id = create_valid_did_url(); - let controller = create_valid_did(); - let verification_method_type = create_valid_verification_key_type(); - let extra_key = "foo".to_string(); - let extra_value = Value::String("bar".to_string()); - - let vm = VerificationMethod::builder(id, controller, verification_method_type) - .add_extra_field(extra_key.clone(), extra_value.clone()) - .build(); - assert_eq!(vm.extra_field(&extra_key).unwrap(), &extra_value); + match vm.public_key() { + PublicKeyField::Multibase { + public_key_multibase, + } => { + assert_eq!(public_key_multibase, public_key_multibase) + } + _ => panic!("Expected public key to be multibase"), + } } #[test] @@ -232,8 +307,6 @@ mod tests { let controller = create_valid_did(); let verification_method_type = create_valid_verification_key_type(); let public_key_multibase = create_valid_multibase(); - let extra_key = "foo".to_string(); - let extra_value = Value::String("bar".to_string()); let vm = VerificationMethod::builder( id.clone(), @@ -241,13 +314,38 @@ mod tests { verification_method_type.clone(), ) .add_public_key_multibase(public_key_multibase.clone()) - .add_extra_field(extra_key.clone(), extra_value.clone()) .build(); assert_eq!(vm.id(), &id); assert_eq!(vm.controller(), &controller); assert_eq!(vm.verification_method_type(), &verification_method_type); - assert_eq!(vm.public_key_multibase().unwrap(), &public_key_multibase); - assert_eq!(vm.extra_field(&extra_key).unwrap(), &extra_value); + match vm.public_key() { + PublicKeyField::Multibase { + public_key_multibase, + } => { + assert_eq!(public_key_multibase, public_key_multibase) + } + _ => panic!("Expected public key to be multibase"), + } + } + + #[test] + fn test_verification_method_deserialization() { + let vm: Result = serde_json::from_str( + create_valid_verification_method_value() + .to_string() + .as_str(), + ); + assert!(vm.is_ok()); + } + + #[test] + fn test_verification_method_deserialization_fails_with_multiple_keys() { + let vm: Result = serde_json::from_str( + create_verification_method_multiple_keys() + .to_string() + .as_str(), + ); + assert!(vm.is_err()); } } diff --git a/did_doc/tests/serde.rs b/did_doc/tests/serde.rs index ce3cd94ee7..d7c6aeb3c9 100644 --- a/did_doc/tests/serde.rs +++ b/did_doc/tests/serde.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use did_doc::schema::{ did_doc::DidDocument, types::{jsonwebkey::JsonWebKey, uri::Uri}, - verification_method::{VerificationMethod, VerificationMethodKind}, + verification_method::{VerificationMethod, VerificationMethodKind, VerificationMethodType}, }; use did_parser::{Did, DidUrl}; use serde_json::Value; @@ -92,24 +92,28 @@ fn test_deserialization() { .unwrap(); let vm1_id = DidUrl::parse("#g1".to_string()).unwrap(); - let vm1 = VerificationMethod::builder(vm1_id, controller.clone(), "JsonWebKey2020".to_string()) - .add_public_key_jwk( - JsonWebKey::from_str( - r#"{ + let vm1 = VerificationMethod::builder( + vm1_id, + controller.clone(), + VerificationMethodType::JsonWebKey2020, + ) + .add_public_key_jwk( + JsonWebKey::from_str( + r#"{ "kty": "EC", "crv": "BLS12381_G1", "x": "hxF12gtsn9ju4-kJq2-nUjZQKVVWpcBAYX5VHnUZMDilClZsGuOaDjlXS8pFE1GG" }"#, - ) - .unwrap(), ) - .build(); + .unwrap(), + ) + .build(); let vm2_id = DidUrl::parse("#g2".to_string()).unwrap(); let vm2 = VerificationMethod::builder( vm2_id, controller.clone(), - "JsonWebKey2020".to_string(), + VerificationMethodType::JsonWebKey2020, ) .add_public_key_jwk( JsonWebKey::from_str( @@ -165,13 +169,13 @@ fn test_deserialization() { "did:web:did-actor-alice#zC8GybikEfyNaausDA4mkT4egP7SNLx2T1d1kujLQbcP6h".to_string(), ) .unwrap(); - let ka1 = - VerificationMethod::builder(ka1_id, controller, "X25519KeyAgreementKey2019".to_string()) - .add_extra_field( - "publicKeyBase58".to_string(), - Value::String("CaSHXEvLKS6SfN9aBfkVGBpp15jSnaHazqHgLHp8KZ3Y".to_string()), - ) - .build(); + let ka1 = VerificationMethod::builder( + ka1_id, + controller, + VerificationMethodType::X25519KeyAgreementKey2019, + ) + .add_public_key_base58("CaSHXEvLKS6SfN9aBfkVGBpp15jSnaHazqHgLHp8KZ3Y".to_string()) + .build(); assert_eq!( did_doc.key_agreement(), diff --git a/did_doc_sov/Cargo.toml b/did_doc_sov/Cargo.toml new file mode 100644 index 0000000000..b5a6534ea8 --- /dev/null +++ b/did_doc_sov/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "did_doc_sov" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +did_doc = { path = "../did_doc" } +serde = { version = "1.0.159", default-features = false, features = ["derive"] } +serde_json = "1.0.95" +thiserror = "1.0.40" diff --git a/did_doc_sov/src/error.rs b/did_doc_sov/src/error.rs new file mode 100644 index 0000000000..42a1fa6ef3 --- /dev/null +++ b/did_doc_sov/src/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DidDocumentSovError { + #[error("Attempted to access empty collection: {0}")] + EmptyCollection(&'static str), + #[error("DID document builder error: {0}")] + DidDocumentBuilderError(#[from] did_doc::error::DidDocumentBuilderError), + #[error("Unexpected service type: {0}")] + UnexpectedServiceType(String), + #[error("Index out of bounds: {0}")] + IndexOutOfBounds(usize), +} diff --git a/did_doc_sov/src/extra_fields/aip1.rs b/did_doc_sov/src/extra_fields/aip1.rs new file mode 100644 index 0000000000..e8f9155724 --- /dev/null +++ b/did_doc_sov/src/extra_fields/aip1.rs @@ -0,0 +1,4 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] +pub struct ExtraFieldsAIP1 {} diff --git a/did_doc_sov/src/extra_fields/didcommv1.rs b/did_doc_sov/src/extra_fields/didcommv1.rs new file mode 100644 index 0000000000..54e0f57d3c --- /dev/null +++ b/did_doc_sov/src/extra_fields/didcommv1.rs @@ -0,0 +1,67 @@ +use serde::{Deserialize, Serialize}; + +use super::{AcceptType, KeyKind}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct ExtraFieldsDidCommV1 { + priority: u32, + recipient_keys: Vec, + routing_keys: Vec, + accept: Vec, +} + +impl ExtraFieldsDidCommV1 { + pub fn builder() -> ExtraFieldsDidCommV1Builder { + ExtraFieldsDidCommV1Builder::default() + } + + pub fn priority(&self) -> u32 { + self.priority + } + + pub fn recipient_keys(&self) -> &[KeyKind] { + self.recipient_keys.as_ref() + } + + pub fn routing_keys(&self) -> &[KeyKind] { + self.routing_keys.as_ref() + } + + pub fn accept(&self) -> &[AcceptType] { + self.accept.as_ref() + } +} + +#[derive(Default)] +pub struct ExtraFieldsDidCommV1Builder { + priority: u32, + recipient_keys: Vec, + routing_keys: Vec, +} + +impl ExtraFieldsDidCommV1Builder { + pub fn set_priority(mut self, priority: u32) -> Self { + self.priority = priority; + self + } + + pub fn set_recipient_keys(mut self, recipient_keys: Vec) -> Self { + self.recipient_keys = recipient_keys; + self + } + + pub fn set_routing_keys(mut self, routing_keys: Vec) -> Self { + self.routing_keys = routing_keys; + self + } + + pub fn build(self) -> ExtraFieldsDidCommV1 { + ExtraFieldsDidCommV1 { + priority: self.priority, + recipient_keys: self.recipient_keys, + routing_keys: self.routing_keys, + accept: vec![AcceptType::DIDCommV1], + } + } +} diff --git a/did_doc_sov/src/extra_fields/didcommv2.rs b/did_doc_sov/src/extra_fields/didcommv2.rs new file mode 100644 index 0000000000..85c9c34299 --- /dev/null +++ b/did_doc_sov/src/extra_fields/didcommv2.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; + +use super::{AcceptType, KeyKind}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct ExtraFieldsDidCommV2 { + accept: Vec, + routing_keys: Vec, +} + +impl ExtraFieldsDidCommV2 { + pub fn builder() -> ExtraFieldsDidCommV2Builder { + ExtraFieldsDidCommV2Builder::default() + } + + pub fn accept(&self) -> &[AcceptType] { + self.accept.as_ref() + } + + pub fn routing_keys(&self) -> &[KeyKind] { + self.routing_keys.as_ref() + } +} + +#[derive(Default)] +pub struct ExtraFieldsDidCommV2Builder { + routing_keys: Vec, +} + +impl ExtraFieldsDidCommV2Builder { + pub fn set_routing_keys(mut self, routing_keys: Vec) -> Self { + self.routing_keys = routing_keys; + self + } + + pub fn build(self) -> ExtraFieldsDidCommV2 { + ExtraFieldsDidCommV2 { + accept: vec![AcceptType::DIDCommV2], + routing_keys: self.routing_keys, + } + } +} diff --git a/did_doc_sov/src/extra_fields/mod.rs b/did_doc_sov/src/extra_fields/mod.rs new file mode 100644 index 0000000000..2d8f6c3f4c --- /dev/null +++ b/did_doc_sov/src/extra_fields/mod.rs @@ -0,0 +1,117 @@ +use std::fmt::Display; + +use did_doc::did_parser::DidUrl; +use serde::{Deserialize, Deserializer, Serialize}; + +use crate::error::DidDocumentSovError; + +pub mod aip1; +pub mod didcommv1; +pub mod didcommv2; + +#[derive(Serialize, Clone, Debug, PartialEq)] +pub enum AcceptType { + DIDCommV1, + DIDCommV2, + Other(String), +} + +impl Display for AcceptType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AcceptType::DIDCommV1 => write!(f, "didcomm/aip2;env=rfc19"), + AcceptType::DIDCommV2 => write!(f, "didcomm/v2"), + AcceptType::Other(other) => write!(f, "{}", other), + } + } +} + +impl<'de> Deserialize<'de> for AcceptType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "didcomm/aip2;env=rfc19" => Ok(AcceptType::DIDCommV1), + "didcomm/v2" => Ok(AcceptType::DIDCommV2), + _ => Ok(AcceptType::Other(s)), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(untagged)] +pub enum KeyKind { + Reference(DidUrl), + Value(String), +} + +impl Display for KeyKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + KeyKind::Reference(did_url) => write!(f, "{}", did_url), + KeyKind::Value(value) => write!(f, "{}", value), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(untagged)] +pub enum ExtraFieldsSov { + DIDCommV1(didcommv1::ExtraFieldsDidCommV1), + DIDCommV2(didcommv2::ExtraFieldsDidCommV2), + AIP1(aip1::ExtraFieldsAIP1), +} + +impl Default for ExtraFieldsSov { + fn default() -> Self { + ExtraFieldsSov::AIP1(aip1::ExtraFieldsAIP1::default()) + } +} + +impl ExtraFieldsSov { + pub fn recipient_keys(&self) -> Result<&[KeyKind], DidDocumentSovError> { + match self { + ExtraFieldsSov::DIDCommV1(extra) => Ok(extra.recipient_keys()), + ExtraFieldsSov::AIP1(_) | ExtraFieldsSov::DIDCommV2(_) => { + Err(DidDocumentSovError::EmptyCollection("recipient_keys")) + } + } + } + + pub fn routing_keys(&self) -> Result<&[KeyKind], DidDocumentSovError> { + match self { + ExtraFieldsSov::DIDCommV1(extra) => Ok(extra.routing_keys()), + ExtraFieldsSov::DIDCommV2(extra) => Ok(extra.routing_keys()), + ExtraFieldsSov::AIP1(_) => Err(DidDocumentSovError::EmptyCollection("routing_keys")), + } + } + + pub fn first_recipient_key(&self) -> Result<&KeyKind, DidDocumentSovError> { + self.recipient_keys()? + .first() + .ok_or(DidDocumentSovError::EmptyCollection("recipient_keys")) + } + + pub fn first_routing_key(&self) -> Result<&KeyKind, DidDocumentSovError> { + self.routing_keys()? + .first() + .ok_or(DidDocumentSovError::EmptyCollection("routing_keys")) + } + + pub fn accept(&self) -> Result<&[AcceptType], DidDocumentSovError> { + match self { + ExtraFieldsSov::DIDCommV1(extra) => Ok(extra.accept()), + ExtraFieldsSov::DIDCommV2(extra) => Ok(extra.accept()), + ExtraFieldsSov::AIP1(_) => Err(DidDocumentSovError::EmptyCollection("accept")), + } + } + + pub fn priority(&self) -> Result { + match self { + ExtraFieldsSov::DIDCommV1(extra) => Ok(extra.priority()), + _ => Err(DidDocumentSovError::EmptyCollection("priority")), + } + } +} diff --git a/did_doc_sov/src/lib.rs b/did_doc_sov/src/lib.rs new file mode 100644 index 0000000000..454a3bb8ad --- /dev/null +++ b/did_doc_sov/src/lib.rs @@ -0,0 +1,141 @@ +pub mod error; +pub mod extra_fields; +pub mod service; + +use did_doc::{ + did_parser::{Did, DidUrl}, + schema::{ + did_doc::{ControllerAlias, DidDocument, DidDocumentBuilder}, + types::uri::Uri, + verification_method::{VerificationMethod, VerificationMethodKind}, + }, +}; +use extra_fields::ExtraFieldsSov; +use serde::{de, Deserialize, Deserializer, Serialize}; +use serde_json::Value; +use service::ServiceSov; + +#[derive(Serialize, Clone, Debug, PartialEq)] +pub struct DidDocumentSov { + did_doc: DidDocument, + services: Vec, +} + +impl DidDocumentSov { + pub fn builder() -> DidDocumentSovBuilder { + DidDocumentSovBuilder::default() + } + + pub fn id(&self) -> &Did { + self.did_doc.id() + } + + pub fn also_known_as(&self) -> &[Uri] { + self.did_doc.also_known_as() + } + + pub fn controller(&self) -> Option<&ControllerAlias> { + self.did_doc.controller() + } + + pub fn verification_method(&self) -> &[VerificationMethod] { + self.did_doc.verification_method() + } + + pub fn authentication(&self) -> &[VerificationMethodKind] { + self.did_doc.authentication() + } + + pub fn service(&self) -> &[ServiceSov] { + self.services.as_ref() + } + + pub fn assertion_method(&self) -> &[VerificationMethodKind] { + self.did_doc.assertion_method() + } + + pub fn key_agreement(&self) -> &[VerificationMethodKind] { + self.did_doc.key_agreement() + } + + pub fn capability_invocation(&self) -> &[VerificationMethodKind] { + self.did_doc.capability_invocation() + } + + pub fn capability_delegation(&self) -> &[VerificationMethodKind] { + self.did_doc.capability_delegation() + } + + pub fn extra_field(&self, key: &str) -> Option<&Value> { + self.did_doc.extra_field(key) + } + + pub fn dereference_key(&self, reference: &DidUrl) -> Option<&VerificationMethod> { + self.did_doc.dereference_key(reference) + } +} + +#[derive(Default)] +pub struct DidDocumentSovBuilder { + ddo_builder: DidDocumentBuilder, + services: Vec, +} + +impl DidDocumentSovBuilder { + pub fn new(id: Did) -> Self { + Self { + ddo_builder: DidDocumentBuilder::new(id), + services: Vec::new(), + } + } + + pub fn add_controller(mut self, controller: Did) -> Self { + self.ddo_builder = self.ddo_builder.add_controller(controller); + self + } + + pub fn add_verification_method(mut self, verification_method: VerificationMethod) -> Self { + self.ddo_builder = self.ddo_builder.add_verification_method(verification_method); + self + } + + pub fn add_service(mut self, service: ServiceSov) -> Self { + self.services.push(service); + self + } + + pub fn build(self) -> DidDocumentSov { + DidDocumentSov { + did_doc: self.ddo_builder.build(), + services: self.services, + } + } +} + +impl<'de> Deserialize<'de> for DidDocumentSov { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize, Clone, Debug, PartialEq)] + struct TempDidDocumentSov { + #[serde(flatten)] + did_doc: DidDocument, + } + + let temp = TempDidDocumentSov::deserialize(deserializer)?; + + let services = temp + .did_doc + .service() + .iter() + .map(|s| ServiceSov::try_from(s.clone())) + .collect::, _>>() + .map_err(|_| de::Error::custom("Failed to convert service"))?; + + Ok(DidDocumentSov { + did_doc: temp.did_doc, + services, + }) + } +} diff --git a/did_doc_sov/src/service/aip1.rs b/did_doc_sov/src/service/aip1.rs new file mode 100644 index 0000000000..43dcfd858f --- /dev/null +++ b/did_doc_sov/src/service/aip1.rs @@ -0,0 +1,83 @@ +use did_doc::schema::{ + service::Service, + types::{uri::Uri, url::Url}, + utils::OneOrList, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + error::DidDocumentSovError, + extra_fields::{aip1::ExtraFieldsAIP1, ExtraFieldsSov}, +}; + +use super::ServiceType; + +#[derive(Serialize, Clone, Debug, PartialEq)] +pub struct ServiceAIP1 { + #[serde(flatten)] + service: Service, +} + +impl ServiceAIP1 { + pub fn new(id: Uri, service_endpoint: Url, extra: ExtraFieldsAIP1) -> Result { + Ok(Self { + service: Service::builder(id, service_endpoint, extra) + .add_service_type(ServiceType::AIP1.to_string())? + .build(), + }) + } + + pub fn id(&self) -> &Uri { + self.service.id() + } + + pub fn service_type(&self) -> ServiceType { + ServiceType::AIP1 + } + + pub fn service_endpoint(&self) -> &Url { + self.service.service_endpoint() + } + + pub fn extra(&self) -> &ExtraFieldsAIP1 { + self.service.extra() + } +} + +impl TryFrom> for ServiceAIP1 { + type Error = DidDocumentSovError; + + fn try_from(service: Service) -> Result { + match service.extra() { + ExtraFieldsSov::AIP1(extra) => { + Self::new(service.id().clone(), service.service_endpoint().clone(), extra.clone()) + } + _ => Err(DidDocumentSovError::UnexpectedServiceType( + service.service_type().to_string(), + )), + } + } +} + +impl<'de> Deserialize<'de> for ServiceAIP1 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let service = Service::::deserialize(deserializer)?; + match service.service_type() { + OneOrList::One(service_type) if *service_type == ServiceType::AIP1.to_string() => {} + OneOrList::List(service_types) if service_types.contains(&ServiceType::AIP1.to_string()) => {} + _ => return Err(serde::de::Error::custom("Extra fields don't match service type")), + }; + match service.extra() { + ExtraFieldsSov::AIP1(extra) => Ok(Self { + service: Service::builder(service.id().clone(), service.service_endpoint().clone(), extra.clone()) + .add_service_type(ServiceType::AIP1.to_string()) + .map_err(serde::de::Error::custom)? + .build(), + }), + _ => Err(serde::de::Error::custom("Extra fields don't match service type")), + } + } +} diff --git a/did_doc_sov/src/service/didcommv1.rs b/did_doc_sov/src/service/didcommv1.rs new file mode 100644 index 0000000000..bae374c3da --- /dev/null +++ b/did_doc_sov/src/service/didcommv1.rs @@ -0,0 +1,83 @@ +use did_doc::schema::{ + service::Service, + types::{uri::Uri, url::Url}, + utils::OneOrList, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + error::DidDocumentSovError, + extra_fields::{didcommv1::ExtraFieldsDidCommV1, ExtraFieldsSov}, +}; + +use super::ServiceType; + +#[derive(Serialize, Clone, Debug, PartialEq)] +pub struct ServiceDidCommV1 { + #[serde(flatten)] + service: Service, +} + +impl ServiceDidCommV1 { + pub fn new(id: Uri, service_endpoint: Url, extra: ExtraFieldsDidCommV1) -> Result { + Ok(Self { + service: Service::builder(id, service_endpoint, extra) + .add_service_type(ServiceType::DIDCommV1.to_string())? + .build(), + }) + } + + pub fn id(&self) -> &Uri { + self.service.id() + } + + pub fn service_type(&self) -> ServiceType { + ServiceType::DIDCommV1 + } + + pub fn service_endpoint(&self) -> &Url { + self.service.service_endpoint() + } + + pub fn extra(&self) -> &ExtraFieldsDidCommV1 { + self.service.extra() + } +} + +impl TryFrom> for ServiceDidCommV1 { + type Error = DidDocumentSovError; + + fn try_from(service: Service) -> Result { + match service.extra() { + ExtraFieldsSov::DIDCommV1(extra) => { + Self::new(service.id().clone(), service.service_endpoint().clone(), extra.clone()) + } + _ => Err(DidDocumentSovError::UnexpectedServiceType( + service.service_type().to_string(), + )), + } + } +} + +impl<'de> Deserialize<'de> for ServiceDidCommV1 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let service = Service::::deserialize(deserializer)?; + match service.service_type() { + OneOrList::One(service_type) if *service_type == ServiceType::DIDCommV1.to_string() => {} + OneOrList::List(service_types) if service_types.contains(&ServiceType::DIDCommV1.to_string()) => {} + _ => return Err(serde::de::Error::custom("Extra fields don't match service type")), + }; + match service.extra() { + ExtraFieldsSov::DIDCommV1(extra) => Ok(Self { + service: Service::builder(service.id().clone(), service.service_endpoint().clone(), extra.clone()) + .add_service_type(ServiceType::DIDCommV1.to_string()) + .map_err(serde::de::Error::custom)? + .build(), + }), + _ => Err(serde::de::Error::custom("Extra fields don't match service type")), + } + } +} diff --git a/did_doc_sov/src/service/didcommv2.rs b/did_doc_sov/src/service/didcommv2.rs new file mode 100644 index 0000000000..583d3297a0 --- /dev/null +++ b/did_doc_sov/src/service/didcommv2.rs @@ -0,0 +1,83 @@ +use did_doc::schema::{ + service::Service, + types::{uri::Uri, url::Url}, + utils::OneOrList, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + error::DidDocumentSovError, + extra_fields::{didcommv2::ExtraFieldsDidCommV2, ExtraFieldsSov}, +}; + +use super::ServiceType; + +#[derive(Serialize, Clone, Debug, PartialEq)] +pub struct ServiceDidCommV2 { + #[serde(flatten)] + service: Service, +} + +impl ServiceDidCommV2 { + pub fn new(id: Uri, service_endpoint: Url, extra: ExtraFieldsDidCommV2) -> Result { + Ok(Self { + service: Service::builder(id, service_endpoint, extra) + .add_service_type(ServiceType::DIDCommV2.to_string())? + .build(), + }) + } + + pub fn id(&self) -> &Uri { + self.service.id() + } + + pub fn service_type(&self) -> ServiceType { + ServiceType::DIDCommV2 + } + + pub fn service_endpoint(&self) -> &Url { + self.service.service_endpoint() + } + + pub fn extra(&self) -> &ExtraFieldsDidCommV2 { + self.service.extra() + } +} + +impl TryFrom> for ServiceDidCommV2 { + type Error = DidDocumentSovError; + + fn try_from(service: Service) -> Result { + match service.extra() { + ExtraFieldsSov::DIDCommV2(extra) => { + Self::new(service.id().clone(), service.service_endpoint().clone(), extra.clone()) + } + _ => Err(DidDocumentSovError::UnexpectedServiceType( + service.service_type().to_string(), + )), + } + } +} + +impl<'de> Deserialize<'de> for ServiceDidCommV2 { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let service = Service::::deserialize(deserializer)?; + match service.service_type() { + OneOrList::One(service_type) if *service_type == ServiceType::DIDCommV2.to_string() => {} + OneOrList::List(service_types) if service_types.contains(&ServiceType::DIDCommV2.to_string()) => {} + _ => return Err(serde::de::Error::custom("Extra fields don't match service type")), + }; + match service.extra() { + ExtraFieldsSov::DIDCommV2(extra) => Ok(Self { + service: Service::builder(service.id().clone(), service.service_endpoint().clone(), extra.clone()) + .add_service_type(ServiceType::DIDCommV2.to_string()) + .map_err(serde::de::Error::custom)? + .build(), + }), + _ => Err(serde::de::Error::custom("Extra fields don't match service type")), + } + } +} diff --git a/did_doc_sov/src/service/mod.rs b/did_doc_sov/src/service/mod.rs new file mode 100644 index 0000000000..1155174ed6 --- /dev/null +++ b/did_doc_sov/src/service/mod.rs @@ -0,0 +1,114 @@ +use std::fmt::Display; + +use did_doc::schema::{ + service::Service, + types::{uri::Uri, url::Url}, +}; +use serde::{Deserialize, Serialize}; + +use crate::{error::DidDocumentSovError, extra_fields::ExtraFieldsSov}; + +pub mod aip1; +pub mod didcommv1; +pub mod didcommv2; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum ServiceType { + AIP1, + DIDCommV1, + DIDCommV2, +} + +impl Display for ServiceType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ServiceType::AIP1 => write!(f, "endpoint"), + ServiceType::DIDCommV1 => write!(f, "did-communication"), + ServiceType::DIDCommV2 => write!(f, "DIDComm"), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(untagged)] +pub enum ServiceSov { + AIP1(aip1::ServiceAIP1), + DIDCommV1(didcommv1::ServiceDidCommV1), + DIDCommV2(didcommv2::ServiceDidCommV2), +} + +impl ServiceSov { + pub fn id(&self) -> &Uri { + match self { + ServiceSov::AIP1(service) => service.id(), + ServiceSov::DIDCommV1(service) => service.id(), + ServiceSov::DIDCommV2(service) => service.id(), + } + } + + pub fn service_type(&self) -> ServiceType { + match self { + ServiceSov::AIP1(service) => service.service_type(), + ServiceSov::DIDCommV1(service) => service.service_type(), + ServiceSov::DIDCommV2(service) => service.service_type(), + } + } + + pub fn service_endpoint(&self) -> &Url { + match self { + ServiceSov::AIP1(service) => service.service_endpoint(), + ServiceSov::DIDCommV1(service) => service.service_endpoint(), + ServiceSov::DIDCommV2(service) => service.service_endpoint(), + } + } + + pub fn extra(&self) -> ExtraFieldsSov { + match self { + ServiceSov::AIP1(service) => ExtraFieldsSov::AIP1(service.extra().to_owned()), + ServiceSov::DIDCommV1(service) => ExtraFieldsSov::DIDCommV1(service.extra().to_owned()), + ServiceSov::DIDCommV2(service) => ExtraFieldsSov::DIDCommV2(service.extra().to_owned()), + } + } +} + +impl TryFrom> for ServiceSov { + type Error = DidDocumentSovError; + + fn try_from(service: Service) -> Result { + match service.extra() { + ExtraFieldsSov::AIP1(_extra) => Ok(ServiceSov::AIP1(service.try_into()?)), + ExtraFieldsSov::DIDCommV1(_extra) => Ok(ServiceSov::DIDCommV1(service.try_into()?)), + ExtraFieldsSov::DIDCommV2(_extra) => Ok(ServiceSov::DIDCommV2(service.try_into()?)), + } + } +} + +impl TryFrom for Service { + type Error = DidDocumentSovError; + + fn try_from(service: ServiceSov) -> Result { + match service { + ServiceSov::AIP1(service) => Ok(Service::builder( + service.id().clone(), + service.service_endpoint().clone(), + ExtraFieldsSov::AIP1(service.extra().to_owned()), + ) + .add_service_type(service.service_type().to_string())? + .build()), + ServiceSov::DIDCommV1(service) => Ok(Service::builder( + service.id().clone(), + service.service_endpoint().clone(), + ExtraFieldsSov::DIDCommV1(service.extra().to_owned()), + ) + .add_service_type(service.service_type().to_string())? + .build()), + ServiceSov::DIDCommV2(service) => Ok(Service::builder( + service.id().clone(), + service.service_endpoint().clone(), + ExtraFieldsSov::DIDCommV2(service.extra().to_owned()), + ) + .add_service_type(service.service_type().to_string())? + .build()), + } + } +} diff --git a/did_doc_sov/tests/builder.rs b/did_doc_sov/tests/builder.rs new file mode 100644 index 0000000000..6e1e32828e --- /dev/null +++ b/did_doc_sov/tests/builder.rs @@ -0,0 +1,93 @@ +use did_doc::schema::types::{uri::Uri, url::Url}; +use did_doc_sov::{ + extra_fields::{aip1::ExtraFieldsAIP1, didcommv1::ExtraFieldsDidCommV1, didcommv2::ExtraFieldsDidCommV2, KeyKind}, + service::{aip1::ServiceAIP1, didcommv1::ServiceDidCommV1, didcommv2::ServiceDidCommV2, ServiceSov}, + DidDocumentSov, +}; + +const ID: &'static str = "did:sov:WRfXPg8dantKVubE3HX8pw"; +const SERVICE_ENDPOINT: &'static str = "https://example.com"; + +#[test] +fn test_service_build_aip1() { + let service = ServiceAIP1::new( + ID.parse().unwrap(), + SERVICE_ENDPOINT.parse().unwrap(), + ExtraFieldsAIP1::default(), + ) + .unwrap(); + let did_doc = DidDocumentSov::builder().add_service(ServiceSov::AIP1(service)).build(); + let services = did_doc.service(); + assert_eq!(services.len(), 1); + let first_service = services.get(0).unwrap(); + assert_eq!(first_service.id().clone(), ID.parse::().unwrap()); + assert_eq!( + first_service.service_endpoint().clone(), + SERVICE_ENDPOINT.parse::().unwrap() + ); + let first_extra = first_service.extra(); + assert!(first_extra.priority().is_err()); + assert!(first_extra.recipient_keys().is_err()); + assert!(first_extra.routing_keys().is_err()); +} + +#[test] +fn test_service_build_didcommv1() { + let extra_fields_didcommv1 = ExtraFieldsDidCommV1::builder() + .set_priority(1) + .set_routing_keys(vec![KeyKind::Value("foo".to_owned())]) + .set_recipient_keys(vec![KeyKind::Value("bar".to_owned())]) + .build(); + let service = ServiceDidCommV1::new( + ID.parse().unwrap(), + SERVICE_ENDPOINT.parse().unwrap(), + extra_fields_didcommv1, + ) + .unwrap(); + let did_doc = DidDocumentSov::builder() + .add_service(ServiceSov::DIDCommV1(service)) + .build(); + let services = did_doc.service(); + assert_eq!(services.len(), 1); + let first_service = services.get(0).unwrap(); + assert_eq!(first_service.id().clone(), ID.parse::().unwrap()); + assert_eq!( + first_service.service_endpoint().clone(), + SERVICE_ENDPOINT.parse::().unwrap() + ); + let first_extra = first_service.extra(); + assert_eq!(first_extra.priority().unwrap(), 1); + assert_eq!( + first_extra.recipient_keys().unwrap(), + &[KeyKind::Value("bar".to_owned())] + ); + assert_eq!(first_extra.routing_keys().unwrap(), &[KeyKind::Value("foo".to_owned())]); +} + +#[test] +fn test_service_build_didcommv2() { + let extra_fields_didcommv2 = ExtraFieldsDidCommV2::builder() + .set_routing_keys(vec![KeyKind::Value("foo".to_owned())]) + .build(); + let service = ServiceDidCommV2::new( + ID.parse().unwrap(), + SERVICE_ENDPOINT.parse().unwrap(), + extra_fields_didcommv2, + ) + .unwrap(); + let did_doc = DidDocumentSov::builder() + .add_service(ServiceSov::DIDCommV2(service)) + .build(); + let services = did_doc.service(); + assert_eq!(services.len(), 1); + let first_service = services.get(0).unwrap(); + assert_eq!(first_service.id().clone(), ID.parse::().unwrap()); + assert_eq!( + first_service.service_endpoint().clone(), + SERVICE_ENDPOINT.parse::().unwrap() + ); + let first_extra = first_service.extra(); + assert!(first_extra.priority().is_err()); + assert!(first_extra.recipient_keys().is_err()); + assert_eq!(first_extra.routing_keys().unwrap(), &[KeyKind::Value("foo".to_owned())]); +} diff --git a/did_doc_sov/tests/serde.rs b/did_doc_sov/tests/serde.rs new file mode 100644 index 0000000000..17fe1cb454 --- /dev/null +++ b/did_doc_sov/tests/serde.rs @@ -0,0 +1,138 @@ +use did_doc_sov::{ + extra_fields::{AcceptType, KeyKind}, + service::ServiceType, + DidDocumentSov, +}; + +const DID_DOC_DATA: &'static str = r#" +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM#key-1", + "controller": "did:sov:HR6vs6GEZ8rHaVgjg2WodM", + "publicKeyBase58": "9wvq2i4xUa5umXoThe83CDgx1e5bsjZKJL4DEWvTP9qe" + }, + { + "type": "X25519KeyAgreementKey2019", + "id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM#key-agreement-1", + "controller": "did:sov:HR6vs6GEZ8rHaVgjg2WodM", + "publicKeyBase58": "3mHtKcQFEzqeUcnce5BAuzAgLEbqKaV542pUf9xQ5Pf8" + } + ], + "authentication": [ + "did:sov:HR6vs6GEZ8rHaVgjg2WodM#key-1" + ], + "assertionMethod": [ + "did:sov:HR6vs6GEZ8rHaVgjg2WodM#key-1" + ], + "keyAgreement": [ + "did:sov:HR6vs6GEZ8rHaVgjg2WodM#key-agreement-1" + ], + "service": [ + { + "id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://example.com/endpoint" + }, + { + "id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM#did-communication", + "type": "did-communication", + "priority": 0, + "recipientKeys": [ + "did:sov:HR6vs6GEZ8rHaVgjg2WodM#key-agreement-1" + ], + "routingKeys": [], + "accept": [ + "didcomm/aip2;env=rfc19" + ], + "serviceEndpoint": "https://example.com/endpoint" + }, + { + "id": "did:sov:HR6vs6GEZ8rHaVgjg2WodM#didcomm-1", + "type": "DIDComm", + "accept": [ + "didcomm/v2" + ], + "routingKeys": [], + "serviceEndpoint": "https://example.com/endpoint" + } + ] +} +"#; + +#[test] +fn test_serde() { + let did_doc = serde_json::from_str::(DID_DOC_DATA).unwrap(); + assert_eq!(did_doc.id().to_string(), "did:sov:HR6vs6GEZ8rHaVgjg2WodM"); + assert_eq!(did_doc.verification_method().len(), 2); + assert_eq!(did_doc.authentication().len(), 1); + assert_eq!(did_doc.assertion_method().len(), 1); + assert_eq!(did_doc.key_agreement().len(), 1); + assert_eq!(did_doc.service().len(), 3); + + let services = did_doc.service(); + let first_service = services.get(0).unwrap(); + assert_eq!( + first_service.service_endpoint().to_string(), + "https://example.com/endpoint" + ); + assert_eq!(first_service.service_type(), ServiceType::AIP1); + + let second_service = services.get(1).unwrap(); + assert_eq!( + second_service.id().to_string(), + "did:sov:HR6vs6GEZ8rHaVgjg2WodM#did-communication" + ); + assert_eq!(second_service.service_type(), ServiceType::DIDCommV1); + assert_eq!( + second_service.service_endpoint().to_string(), + "https://example.com/endpoint" + ); + + let third_service = services.get(2).unwrap(); + assert_eq!( + third_service.id().to_string(), + "did:sov:HR6vs6GEZ8rHaVgjg2WodM#didcomm-1" + ); + assert_eq!(third_service.service_type(), ServiceType::DIDCommV2); + assert_eq!( + third_service.service_endpoint().to_string(), + "https://example.com/endpoint" + ); + + let second_extra = second_service.extra(); + assert!(!second_extra.recipient_keys().unwrap().is_empty()); + assert_eq!(second_extra.routing_keys().unwrap().len(), 0); + assert!(second_extra.first_recipient_key().is_ok()); + assert!(second_extra.first_routing_key().is_err()); + assert_eq!( + second_extra.accept().unwrap().get(0).unwrap().clone(), + AcceptType::DIDCommV1 + ); + assert_eq!(second_extra.priority().unwrap(), 0); + + let third_extra = third_service.extra(); + assert!(third_extra.recipient_keys().is_err()); + assert_eq!(third_extra.routing_keys().unwrap().len(), 0); + assert!(third_extra.first_recipient_key().is_err()); + assert!(third_extra.first_routing_key().is_err()); + assert_eq!( + third_extra.accept().unwrap().get(0).unwrap().clone(), + AcceptType::DIDCommV2 + ); + assert!(third_extra.priority().is_err()); + + if let KeyKind::Reference(reference) = second_extra.first_recipient_key().unwrap() { + let vm = did_doc.dereference_key(&reference).unwrap(); + assert_eq!(vm.id().to_string(), "did:sov:HR6vs6GEZ8rHaVgjg2WodM#key-agreement-1"); + } else { + panic!("Expected reference key kind"); + } +} diff --git a/did_resolver_sov/Cargo.toml b/did_resolver_sov/Cargo.toml index 9a137bf07c..7536698f65 100644 --- a/did_resolver_sov/Cargo.toml +++ b/did_resolver_sov/Cargo.toml @@ -11,6 +11,7 @@ modular_libs = ["aries_vcx_core/modular_libs"] [dependencies] did_resolver = { path = "../did_resolver" } aries_vcx_core = { path = "../aries_vcx_core" } +did_doc_sov = { path = "../did_doc_sov" } async-trait = "0.1.68" mockall = "0.11.4" serde_json = "1.0.96" diff --git a/did_resolver_sov/src/dereferencing/utils.rs b/did_resolver_sov/src/dereferencing/utils.rs index 93e7a7b41a..73161f85d0 100644 --- a/did_resolver_sov/src/dereferencing/utils.rs +++ b/did_resolver_sov/src/dereferencing/utils.rs @@ -94,6 +94,7 @@ mod tests { use super::*; use did_resolver::did_doc::schema::did_doc::DidDocumentBuilder; + use did_resolver::did_doc::schema::verification_method::VerificationMethodType; use did_resolver::did_parser::DidUrl; use did_resolver::traits::resolvable::resolution_output::DidResolutionOutput; use serde_json::Value; @@ -105,12 +106,9 @@ mod tests { .to_string() .try_into() .unwrap(), - "Ed25519VerificationKey2018".to_string(), - ) - .add_extra_field( - "publicKeyBase58".to_string(), - Value::String("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".to_string()), + VerificationMethodType::Ed25519VerificationKey2018, ) + .add_public_key_base58("H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV".to_string()) .build(); let agent_service = Service::builder( diff --git a/did_resolver_sov/src/resolution/extra_fields.rs b/did_resolver_sov/src/resolution/extra_fields.rs deleted file mode 100644 index 86e3c93e31..0000000000 --- a/did_resolver_sov/src/resolution/extra_fields.rs +++ /dev/null @@ -1,90 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -pub struct ExtraFieldsSov { - #[serde(default)] - priority: u32, - #[serde(default)] - recipient_keys: Vec, - #[serde(default)] - routing_keys: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - accept: Vec, -} - -impl ExtraFieldsSov { - pub fn builder() -> ExtraFieldsSovBuilder { - ExtraFieldsSovBuilder::default() - } - - pub fn priority(&self) -> u32 { - self.priority - } - - pub fn recipient_keys(&self) -> &[String] { - self.recipient_keys.as_ref() - } - - pub fn routing_keys(&self) -> &[String] { - self.routing_keys.as_ref() - } - - pub fn accept(&self) -> &[String] { - self.accept.as_ref() - } -} - -#[derive(Default)] -pub struct ExtraFieldsSovBuilder { - priority: u32, - recipient_keys: Vec, - routing_keys: Vec, - accept: Vec, -} - -impl ExtraFieldsSovBuilder { - pub fn set_priority(&mut self, priority: u32) -> &mut Self { - self.priority = priority; - self - } - - pub fn set_recipient_keys(&mut self, recipient_keys: Vec) -> &mut Self { - self.recipient_keys = recipient_keys; - self - } - - pub fn add_recipient_key(&mut self, recipient_key: String) -> &mut Self { - self.recipient_keys.push(recipient_key); - self - } - - pub fn set_routing_keys(&mut self, routing_keys: Vec) -> &mut Self { - self.routing_keys = routing_keys; - self - } - - pub fn add_routing_key(&mut self, routing_key: String) -> &mut Self { - self.routing_keys.push(routing_key); - self - } - - pub fn set_accept(&mut self, accept: Vec) -> &mut Self { - self.accept = accept; - self - } - - pub fn add_accept(&mut self, accept: String) -> &mut Self { - self.accept.push(accept); - self - } - - pub fn build(&self) -> ExtraFieldsSov { - ExtraFieldsSov { - priority: self.priority, - recipient_keys: self.recipient_keys.clone(), - routing_keys: self.routing_keys.clone(), - accept: self.accept.clone(), - } - } -} diff --git a/did_resolver_sov/src/resolution/mod.rs b/did_resolver_sov/src/resolution/mod.rs index 8e5ea2a20d..054d33feef 100644 --- a/did_resolver_sov/src/resolution/mod.rs +++ b/did_resolver_sov/src/resolution/mod.rs @@ -1,6 +1,4 @@ -mod extra_fields; mod resolver; mod utils; -pub use extra_fields::ExtraFieldsSov; pub use resolver::DidSovResolver; diff --git a/did_resolver_sov/src/resolution/resolver.rs b/did_resolver_sov/src/resolution/resolver.rs index 6db941969e..8bc1e1966b 100644 --- a/did_resolver_sov/src/resolution/resolver.rs +++ b/did_resolver_sov/src/resolution/resolver.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use async_trait::async_trait; +use did_doc_sov::extra_fields::ExtraFieldsSov; use did_resolver::{ did_parser::Did, error::GenericError, @@ -17,10 +18,7 @@ use crate::{ reader::AttrReader, }; -use super::{ - utils::{is_valid_sovrin_did_id, ledger_response_to_ddo}, - ExtraFieldsSov, -}; +use super::utils::{is_valid_sovrin_did_id, ledger_response_to_ddo}; pub struct DidSovResolver { ledger: Arc, @@ -34,8 +32,7 @@ impl DidSovResolver { #[async_trait] impl DidResolvable for DidSovResolver { - // TODO: Change to ExtraFields from Sovrin-specific DDO wrapper - type ExtraFields = (); + type ExtraFields = ExtraFieldsSov; async fn resolve( &self, diff --git a/did_resolver_sov/src/resolution/utils.rs b/did_resolver_sov/src/resolution/utils.rs index d15a8217d6..a38a80f009 100644 --- a/did_resolver_sov/src/resolution/utils.rs +++ b/did_resolver_sov/src/resolution/utils.rs @@ -1,8 +1,10 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use did_resolver::{ did_doc::schema::{ - did_doc::DidDocument, service::Service, types::uri::Uri, - verification_method::VerificationMethod, + did_doc::DidDocument, + service::Service, + types::uri::Uri, + verification_method::{VerificationMethod, VerificationMethodType}, }, did_parser::Did, shared_types::did_document_metadata::DidDocumentMetadata, @@ -92,12 +94,9 @@ pub(super) async fn ledger_response_to_ddo( let verification_method = VerificationMethod::builder( did.to_string().try_into()?, did.to_string().try_into()?, - "Ed25519VerificationKey2018".to_string(), - ) - .add_extra_field( - "publicKeyBase58".to_string(), - Value::String(verkey.to_string()), + VerificationMethodType::Ed25519VerificationKey2018, ) + .add_public_key_base58(verkey.to_string()) .build(); let ddo = DidDocument::builder(ddo_id) @@ -127,6 +126,7 @@ pub(super) async fn ledger_response_to_ddo( mod tests { use super::*; use chrono::TimeZone; + use did_resolver::did_doc::schema::verification_method::PublicKeyField; #[test] fn test_prepare_ids() { @@ -191,13 +191,6 @@ mod tests { ddo.service()[0].service_endpoint().as_ref(), "https://example.com/" ); - assert_eq!( - ddo.verification_method()[0] - .extra_field("publicKeyBase58") - .unwrap() - .clone(), - Value::String("9wvq2i4xUa5umXoThe83CDgx1e5bsjZKJL4DEWvTP9qe".to_string()) - ); assert_eq!( resolution_output.did_document_metadata().updated().unwrap(), chrono::Utc.timestamp_opt(1629272938, 0).unwrap() @@ -209,5 +202,15 @@ mod tests { .unwrap(), "application/did+json" ); + if let PublicKeyField::Base58 { public_key_base58 } = + ddo.verification_method()[0].public_key() + { + assert_eq!( + public_key_base58, + "9wvq2i4xUa5umXoThe83CDgx1e5bsjZKJL4DEWvTP9qe" + ); + } else { + panic!("Unexpected public key type"); + } } }