diff --git a/vdr/Cargo.lock b/vdr/Cargo.lock index 8fd7e009..fabff503 100644 --- a/vdr/Cargo.lock +++ b/vdr/Cargo.lock @@ -1436,6 +1436,7 @@ dependencies = [ "mockall", "once_cell", "rand", + "regex-lite", "rstest", "secp256k1", "serde", @@ -2178,9 +2179,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -2199,6 +2200,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" + [[package]] name = "regex-syntax" version = "0.8.2" diff --git a/vdr/Cargo.toml b/vdr/Cargo.toml index 1a9cc891..917be855 100644 --- a/vdr/Cargo.toml +++ b/vdr/Cargo.toml @@ -46,6 +46,7 @@ web3 = { version = "0.20.0", optional = true } web-sys = { version = "0.3.64", optional = true, features = ["Window"] } web3-wasm = { package = "web3", version = "0.20.0", default-features = false, features = ["wasm", "http", "http-tls"], optional = true } jsonrpc-core = "18.0.0" +regex-lite = "0.1.5" [dev-dependencies] rstest = "0.18.2" diff --git a/vdr/src/contracts/did/did_ethr_registry.rs b/vdr/src/contracts/did/did_ethr_registry.rs index cdec330d..170b03d7 100644 --- a/vdr/src/contracts/did/did_ethr_registry.rs +++ b/vdr/src/contracts/did/did_ethr_registry.rs @@ -641,12 +641,12 @@ pub mod test { contracts::{ did::types::{ did::DID, - did_doc::test::{SERVICE_ENDPOINT, SERVICE_TYPE}, + did_doc::test::SERVICE_ENDPOINT, did_doc_attribute::{ PublicKeyAttribute, PublicKeyPurpose, PublicKeyType, ServiceAttribute, }, }, - ServiceEndpoint, + ServiceEndpoint, ServiceType, }, }; @@ -656,7 +656,7 @@ pub mod test { pub fn service() -> DidDocAttribute { DidDocAttribute::Service(ServiceAttribute { - type_: SERVICE_TYPE.to_string(), + type_: ServiceType::LinkedDomains, service_endpoint: ServiceEndpoint::String(SERVICE_ENDPOINT.to_string()), }) } @@ -826,13 +826,13 @@ pub mod test { data: vec![ 122, 212, 176, 164, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 226, 219, 108, 141, 198, 198, 129, 187, 93, 106, 209, 33, 161, 7, 243, 0, 233, 178, 181, 100, - 105, 100, 47, 115, 118, 99, 47, 83, 101, 114, 118, 105, 99, 101, 0, 0, 0, 0, 0, + 105, 100, 47, 115, 118, 99, 47, 76, 105, 110, 107, 101, 100, 68, 111, 109, 97, + 105, 110, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 232, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 18, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, - 46, 99, 111, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 3, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 18, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, + 112, 108, 101, 46, 99, 111, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], signature: None, hash: None, @@ -896,12 +896,12 @@ pub mod test { data: vec![ 0, 192, 35, 218, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 226, 219, 108, 141, 198, 198, 129, 187, 93, 106, 209, 33, 161, 7, 243, 0, 233, 178, 181, 100, 105, - 100, 47, 115, 118, 99, 47, 83, 101, 114, 118, 105, 99, 101, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 104, 116, - 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 100, 47, 115, 118, 99, 47, 76, 105, 110, 107, 101, 100, 68, 111, 109, 97, 105, + 110, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, + 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, + 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], signature: None, hash: None, diff --git a/vdr/src/contracts/did/did_indy_registry.rs b/vdr/src/contracts/did/did_indy_registry.rs index eccde1ac..bfa7457d 100644 --- a/vdr/src/contracts/did/did_indy_registry.rs +++ b/vdr/src/contracts/did/did_indy_registry.rs @@ -43,6 +43,7 @@ pub async fn build_create_did_transaction( did: &DID, did_doc: &DidDocument, ) -> VdrResult { + did_doc.validate()?; TransactionBuilder::new() .set_contract(CONTRACT_NAME) .set_method(METHOD_CREATE_DID) @@ -70,6 +71,7 @@ pub async fn build_create_did_endorsing_data( did: &DID, did_doc: &DidDocument, ) -> VdrResult { + did_doc.validate()?; TransactionEndorsingDataBuilder::new() .set_contract(CONTRACT_NAME) .set_identity(&Address::try_from(did)?) @@ -98,6 +100,7 @@ pub async fn build_update_did_transaction( did: &DID, did_doc: &DidDocument, ) -> VdrResult { + did_doc.validate()?; TransactionBuilder::new() .set_contract(CONTRACT_NAME) .set_method(METHOD_UPDATE_DID) @@ -125,6 +128,7 @@ pub async fn build_update_did_endorsing_data( did: &DID, did_doc: &DidDocument, ) -> VdrResult { + did_doc.validate()?; TransactionEndorsingDataBuilder::new() .set_contract(CONTRACT_NAME) .set_identity(&Address::try_from(did)?) @@ -220,10 +224,14 @@ pub async fn build_resolve_did_transaction( #[logfn(Info)] #[logfn_inputs(Debug)] pub fn parse_resolve_did_result(client: &LedgerClient, bytes: &[u8]) -> VdrResult { - TransactionParser::new() + let did_record = TransactionParser::new() .set_contract(CONTRACT_NAME) .set_method(METHOD_RESOLVE_DID) - .parse::(client, bytes) + .parse::(client, bytes)?; + + did_record.document.validate()?; + + Ok(did_record) } #[cfg(test)] @@ -260,7 +268,7 @@ pub mod test { 198, 198, 129, 187, 93, 106, 209, 33, 161, 7, 243, 0, 233, 178, 181, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 2, 22, 123, 34, 64, 99, 111, 110, 116, 101, 120, 116, 34, + 0, 0, 0, 0, 0, 0, 0, 2, 16, 123, 34, 64, 99, 111, 110, 116, 101, 120, 116, 34, 58, 91, 34, 104, 116, 116, 112, 115, 58, 47, 47, 119, 119, 119, 46, 119, 51, 46, 111, 114, 103, 47, 110, 115, 47, 100, 105, 100, 47, 118, 49, 34, 93, 44, 34, 105, 100, 34, 58, 34, 100, 105, 100, 58, 105, 110, 100, 121, 98, 101, 115, @@ -280,18 +288,17 @@ pub mod test { 100, 121, 98, 101, 115, 117, 58, 100, 105, 100, 58, 101, 116, 104, 114, 58, 116, 101, 115, 116, 110, 101, 116, 58, 48, 120, 102, 48, 101, 50, 100, 98, 54, 99, 56, 100, 99, 54, 99, 54, 56, 49, 98, 98, 53, 100, 54, 97, 100, 49, 50, 49, - 97, 49, 48, 55, 102, 51, 48, 48, 101, 57, 98, 50, 98, 53, 35, 75, 69, 89, 45, - 49, 34, 44, 34, 112, 117, 98, 108, 105, 99, 75, 101, 121, 77, 117, 108, 116, - 105, 98, 97, 115, 101, 34, 58, 34, 122, 65, 75, 74, 80, 51, 102, 55, 66, 68, - 54, 87, 52, 105, 87, 69, 81, 57, 106, 119, 110, 100, 86, 84, 67, 66, 113, 56, - 117, 97, 50, 85, 116, 116, 56, 69, 69, 106, 74, 54, 86, 120, 115, 102, 34, 125, - 93, 44, 34, 97, 117, 116, 104, 101, 110, 116, 105, 99, 97, 116, 105, 111, 110, - 34, 58, 91, 34, 100, 105, 100, 58, 105, 110, 100, 121, 98, 101, 115, 117, 58, - 100, 105, 100, 58, 101, 116, 104, 114, 58, 116, 101, 115, 116, 110, 101, 116, - 58, 48, 120, 102, 48, 101, 50, 100, 98, 54, 99, 56, 100, 99, 54, 99, 54, 56, - 49, 98, 98, 53, 100, 54, 97, 100, 49, 50, 49, 97, 49, 48, 55, 102, 51, 48, 48, - 101, 57, 98, 50, 98, 53, 35, 75, 69, 89, 45, 49, 34, 93, 125, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, + 97, 49, 48, 55, 102, 51, 48, 48, 101, 57, 98, 50, 98, 53, 34, 44, 34, 112, 117, + 98, 108, 105, 99, 75, 101, 121, 77, 117, 108, 116, 105, 98, 97, 115, 101, 34, + 58, 34, 122, 65, 75, 74, 80, 51, 102, 55, 66, 68, 54, 87, 52, 105, 87, 69, 81, + 57, 106, 119, 110, 100, 86, 84, 67, 66, 113, 56, 117, 97, 50, 85, 116, 116, 56, + 69, 69, 106, 74, 54, 86, 120, 115, 102, 34, 125, 93, 44, 34, 97, 117, 116, 104, + 101, 110, 116, 105, 99, 97, 116, 105, 111, 110, 34, 58, 91, 34, 100, 105, 100, + 58, 105, 110, 100, 121, 98, 101, 115, 117, 58, 100, 105, 100, 58, 101, 116, + 104, 114, 58, 116, 101, 115, 116, 110, 101, 116, 58, 48, 120, 102, 48, 101, 50, + 100, 98, 54, 99, 56, 100, 99, 54, 99, 54, 56, 49, 98, 98, 53, 100, 54, 97, 100, + 49, 50, 49, 97, 49, 48, 55, 102, 51, 48, 48, 101, 57, 98, 50, 98, 53, 35, 75, + 69, 89, 45, 49, 34, 93, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], signature: None, hash: None, @@ -343,7 +350,7 @@ pub mod test { 0, 0, 0, 0, 101, 207, 153, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 210, 123, 34, 64, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 204, 123, 34, 64, 99, 111, 110, 116, 101, 120, 116, 34, 58, 91, 34, 104, 116, 116, 112, 115, 58, 47, 47, 119, 119, 119, 46, 119, 51, 46, 111, 114, 103, 47, 110, 115, 47, 100, 105, 100, 47, 118, 49, 34, 93, 44, 34, 105, 100, 34, 58, 34, 100, 105, 100, 58, 105, 110, @@ -360,16 +367,16 @@ pub mod test { 101, 114, 34, 58, 34, 100, 105, 100, 58, 105, 110, 100, 121, 98, 101, 115, 117, 58, 48, 120, 102, 48, 101, 50, 100, 98, 54, 99, 56, 100, 99, 54, 99, 54, 56, 49, 98, 98, 53, 100, 54, 97, 100, 49, 50, 49, 97, 49, 48, 55, 102, 51, 48, 48, 101, 57, 98, - 50, 98, 53, 35, 75, 69, 89, 45, 49, 34, 44, 34, 112, 117, 98, 108, 105, 99, 75, - 101, 121, 77, 117, 108, 116, 105, 98, 97, 115, 101, 34, 58, 34, 122, 65, 75, 74, - 80, 51, 102, 55, 66, 68, 54, 87, 52, 105, 87, 69, 81, 57, 106, 119, 110, 100, 86, - 84, 67, 66, 113, 56, 117, 97, 50, 85, 116, 116, 56, 69, 69, 106, 74, 54, 86, 120, - 115, 102, 34, 125, 93, 44, 34, 97, 117, 116, 104, 101, 110, 116, 105, 99, 97, 116, - 105, 111, 110, 34, 58, 91, 34, 100, 105, 100, 58, 105, 110, 100, 121, 98, 101, 115, - 117, 58, 48, 120, 102, 48, 101, 50, 100, 98, 54, 99, 56, 100, 99, 54, 99, 54, 56, - 49, 98, 98, 53, 100, 54, 97, 100, 49, 50, 49, 97, 49, 48, 55, 102, 51, 48, 48, 101, - 57, 98, 50, 98, 53, 35, 75, 69, 89, 45, 49, 34, 93, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, + 50, 98, 53, 34, 44, 34, 112, 117, 98, 108, 105, 99, 75, 101, 121, 77, 117, 108, + 116, 105, 98, 97, 115, 101, 34, 58, 34, 122, 65, 75, 74, 80, 51, 102, 55, 66, 68, + 54, 87, 52, 105, 87, 69, 81, 57, 106, 119, 110, 100, 86, 84, 67, 66, 113, 56, 117, + 97, 50, 85, 116, 116, 56, 69, 69, 106, 74, 54, 86, 120, 115, 102, 34, 125, 93, 44, + 34, 97, 117, 116, 104, 101, 110, 116, 105, 99, 97, 116, 105, 111, 110, 34, 58, 91, + 34, 100, 105, 100, 58, 105, 110, 100, 121, 98, 101, 115, 117, 58, 48, 120, 102, 48, + 101, 50, 100, 98, 54, 99, 56, 100, 99, 54, 99, 54, 56, 49, 98, 98, 53, 100, 54, 97, + 100, 49, 50, 49, 97, 49, 48, 55, 102, 51, 48, 48, 101, 57, 98, 50, 98, 53, 35, 75, + 69, 89, 45, 49, 34, 93, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, ]; let parsed_did_doc = parse_resolve_did_result(&client, &data).unwrap(); assert_eq!(did_doc(TEST_ACCOUNT.as_ref()), parsed_did_doc.document); diff --git a/vdr/src/contracts/did/did_resolver.rs b/vdr/src/contracts/did/did_resolver.rs index 7cbfa6c0..c722cbff 100644 --- a/vdr/src/contracts/did/did_resolver.rs +++ b/vdr/src/contracts/did/did_resolver.rs @@ -297,6 +297,7 @@ mod ethr { None, None, None, + None, ); match delegate_type { @@ -343,6 +344,7 @@ mod ethr { key.public_key_hex.as_deref(), key.public_key_base58.as_deref(), key.public_key_base64.as_deref(), + None, ); match key.purpose { diff --git a/vdr/src/contracts/did/types/did.rs b/vdr/src/contracts/did/types/did.rs index 5cc8e8fc..b2e4fce7 100644 --- a/vdr/src/contracts/did/types/did.rs +++ b/vdr/src/contracts/did/types/did.rs @@ -1,8 +1,27 @@ use crate::{types::ContractOutput, ContractParam, VdrError, VdrResult}; +use once_cell::sync::Lazy; +use regex_lite::Regex; use serde_derive::{Deserialize, Serialize}; pub const DID_PREFIX: &str = "did"; +const DID_SYNTAX: &str = r"did:(?:indybesu|ethr):(?:[a-zA-Z0-9]+:)*0x[a-fA-F0-9]{40}"; +const PATH: &str = r"\/[^#?]*"; +const QUERY: &str = r"[?][^#]*"; +const FRAGMENT: &str = r"[#].*"; + +static DID_REGEX: Lazy = Lazy::new(|| Regex::new(&format!("^{DID_SYNTAX}$")).unwrap()); + +pub static DID_URL_REGEX: Lazy = Lazy::new(|| { + Regex::new(&format!( + "^{DID_SYNTAX}(?:{PATH})?(?:{QUERY})?(?:{FRAGMENT})?$" + )) + .unwrap() +}); + +pub static RELATIVE_DID_URL_REGEX: Lazy = + Lazy::new(|| Regex::new(&format!("^(?:{PATH})?(?:{QUERY})?(?:{FRAGMENT})?$")).unwrap()); + /// Wrapper structure for DID #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] pub struct DID(String); @@ -19,6 +38,17 @@ impl DID { pub fn without_network(&self) -> VdrResult { Ok(ParsedDid::try_from(self)?.as_short_did()) } + + pub(crate) fn validate(&self) -> VdrResult<()> { + if !DID_REGEX.is_match(&self.0) { + return Err(VdrError::InvalidDidDocument(format!( + "Incorrect DID: {}", + &self.0 + ))); + }; + + Ok(()) + } } impl From<&str> for DID { diff --git a/vdr/src/contracts/did/types/did_doc.rs b/vdr/src/contracts/did/types/did_doc.rs index f9890c8d..62fe235f 100644 --- a/vdr/src/contracts/did/types/did_doc.rs +++ b/vdr/src/contracts/did/types/did_doc.rs @@ -1,12 +1,14 @@ use crate::{ - error::VdrError, + contracts::did::types::did::{DID, DID_URL_REGEX, RELATIVE_DID_URL_REGEX}, + error::{VdrError, VdrResult}, types::{ContractOutput, ContractParam}, + utils::is_unique, Address, Block, }; -use crate::contracts::did::types::did::DID; use log::warn; use serde_derive::{Deserialize, Serialize}; +use serde_json::json; pub const BASE_CONTEXT: &str = "https://www.w3.org/ns/did/v1"; pub const SECPK_CONTEXT: &str = "https://w3id.org/security/suites/secp256k1recovery-2020/v2"; @@ -70,6 +72,67 @@ pub struct DidDocument { pub also_known_as: Option>, } +impl DidDocument { + pub(crate) fn validate(&self) -> VdrResult<()> { + self.id.validate()?; + + // Validate verification methods + for verification_method in &self.verification_method { + verification_method.validate(&self.id)?; + } + + let verification_relationships = self + .assertion_method + .iter() + .chain(self.authentication.iter()) + .chain(self.capability_delegation.iter()) + .chain(self.capability_invocation.iter()) + .chain(self.key_agreement.iter()) + .collect::>(); + + // Validate verification relationships + verification_relationships + .iter() + .try_for_each(|relationship| { + relationship.validate(&self.id, &self.verification_method) + })?; + + // Check for unique verification method IDs + let verification_method_ids = verification_relationships + .iter() + .filter_map(|relationship| { + if let VerificationMethodOrReference::VerificationMethod(vm) = relationship { + Some(vm) + } else { + None + } + }) + .chain(self.verification_method.iter()) + .map(|vm| &vm.id); + + if !is_unique(verification_method_ids) { + return Err(VdrError::InvalidDidDocument( + "Verification method ID must be unique".to_string(), + )); + } + + // Check for unique service IDs + let service_ids = self.service.iter().map(|value| &value.id); + if !is_unique(service_ids) { + return Err(VdrError::InvalidDidDocument( + "Service ID must be unique".to_string(), + )); + } + + // Validate services + for service in self.service.iter() { + service.validate(&self.id)?; + } + + Ok(()) + } +} + /// DID Record stored in the IndyBesu DID Registry #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -114,6 +177,55 @@ pub struct VerificationMethod { pub public_key_base58: Option, #[serde(skip_serializing_if = "Option::is_none")] pub public_key_base64: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub public_key_jwk: Option, +} + +impl VerificationMethod { + fn validate(&self, did: &DID) -> VdrResult<()> { + if !self.id.starts_with(did.as_ref()) { + return Err(VdrError::InvalidDidDocument(format!( + "Invalid verefication method ID: {}", + self.id + ))); + } + + DID::from(self.controller.as_ref()) + .validate() + .map_err(|_| { + VdrError::InvalidDidDocument(format!( + "Invalid controller syntax in the verification method: {}", + json!(self).to_string() + )) + })?; + + let key_materials = [ + &self.blockchain_account_id, + &self.public_key_multibase, + &self.public_key_hex, + &self.public_key_base58, + &self.public_key_base64, + &self.public_key_jwk, + ]; + + let key_materials_count = key_materials.iter().filter(|key| key.is_some()).count(); + + if key_materials_count == 0 { + return Err(VdrError::InvalidDidDocument(format!( + "No public key was found for the verification method with ID: {}", + self.id + ))); + } + + if key_materials_count > 1 { + return Err(VdrError::InvalidDidDocument(format!( + "Multiple public keys detected in the verification method with ID: {}", + self.id + ))); + } + + Ok(()) + } } #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] @@ -196,15 +308,122 @@ pub enum VerificationMethodOrReference { VerificationMethod(VerificationMethod), } +impl VerificationMethodOrReference { + fn validate(&self, did: &DID, verification_methods: &[VerificationMethod]) -> VdrResult<()> { + match &self { + VerificationMethodOrReference::VerificationMethod(verification_method) => { + verification_method.validate(did)?; + } + VerificationMethodOrReference::String(verification_reference) => { + Self::validate_verification_reference( + did.as_ref(), + verification_reference, + verification_methods, + )?; + } + }; + + Ok(()) + } + + fn validate_verification_reference( + did: &str, + verification_reference: &str, + verification_methods: &[VerificationMethod], + ) -> VdrResult<()> { + let full_reference = if RELATIVE_DID_URL_REGEX.is_match(verification_reference) { + format!("{}{}", did, verification_reference) + } else { + verification_reference.to_string() + }; + + if full_reference.starts_with(did) { + let exist = verification_methods + .iter() + .any(|vm| vm.id == full_reference); + if !exist { + return Err(VdrError::InvalidDidDocument(format!( + "Verification method not found for reference ID: {verification_reference}", + ))); + } + } else if !DID_URL_REGEX.is_match(verification_reference.as_ref()) { + return Err(VdrError::InvalidDidDocument(format!( + "Invalid verification reference ID: {verification_reference}" + ))); + } + + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Service { pub id: String, #[serde(rename = "type")] - pub type_: String, + pub type_: ServiceType, pub service_endpoint: ServiceEndpoint, } +impl Service { + fn validate(&self, did: &DID) -> VdrResult<()> { + if !(self.id.starts_with(did.as_ref())) { + return Err(VdrError::InvalidDidDocument(format!( + "Invalid service ID: {}", + self.id + ))); + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub enum ServiceType { + LinkedDomains, + DIDCommMessaging, + CredentialRegistry, + OID4VCI, + OID4VP, +} + +impl ToString for ServiceType { + fn to_string(&self) -> String { + match self { + ServiceType::LinkedDomains => "LinkedDomains".to_string(), + ServiceType::DIDCommMessaging => "DIDCommMessaging".to_string(), + ServiceType::CredentialRegistry => "CredentialRegistry".to_string(), + ServiceType::OID4VCI => "OID4VCI".to_string(), + ServiceType::OID4VP => "OID4VP".to_string(), + } + } +} + +impl TryFrom<&str> for ServiceType { + type Error = VdrError; + + fn try_from(value: &str) -> Result { + match value { + "LinkedDomains" => Ok(ServiceType::LinkedDomains), + "DIDCommMessaging" => Ok(ServiceType::DIDCommMessaging), + "CredentialRegistry" => Ok(ServiceType::CredentialRegistry), + "OID4VCI" => Ok(ServiceType::OID4VCI), + "OID4VP" => Ok(ServiceType::OID4VP), + _type => Err({ + let vdr_error = + VdrError::CommonInvalidData(format!("Unexpected service type {}", _type)); + + warn!( + "Error: {} during converting ServiceType from String: {} ", + vdr_error, value + ); + + vdr_error + }), + } + } +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(untagged)] pub enum ServiceEndpoint { @@ -330,34 +549,36 @@ pub mod test { use super::*; use crate::{contracts::ETHR_DID_METHOD, did_indy_registry::INDYBESU_DID_METHOD}; - pub const _TEST_INDYBESU_DID: &str = - "did:indybesu:testnet:0xf0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5"; + pub const TEST_IDENTITY: &str = "0xf0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5"; + pub const TEST_INDYBESU_DID: &str = "did:indybesu:0xf0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5"; pub const TEST_ETHR_DID: &str = "did:ethr:testnet:0xf0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5"; pub const TEST_ETHR_DID_WITHOUT_NETWORK: &str = "did:ethr:0xf0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5"; pub const SERVICE_ENDPOINT: &str = "http://example.com"; - pub const SERVICE_TYPE: &str = "Service"; pub const MULTIBASE_KEY: &'static str = "zAKJP3f7BD6W4iWEQ9jwndVTCBq8ua2Utt8EEjJ6Vxsf"; + pub const BASE58_KEY: &'static str = "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV"; pub const KEY_1: &'static str = "KEY-1"; - pub fn _service(id: &str) -> Service { + pub fn service(id: &str) -> Service { Service { id: id.to_string(), - type_: SERVICE_TYPE.to_string(), + type_: ServiceType::LinkedDomains, service_endpoint: ServiceEndpoint::String(SERVICE_ENDPOINT.to_string()), } } pub fn verification_method(id: &str) -> VerificationMethod { + let (controller, _) = id.split_once('#').unwrap_or_default(); VerificationMethod { id: id.to_string(), type_: VerificationKeyType::Ed25519VerificationKey2018, - controller: id.to_string(), + controller: controller.to_string(), blockchain_account_id: None, public_key_multibase: Some(MULTIBASE_KEY.to_string()), public_key_hex: None, public_key_base58: None, public_key_base64: None, + public_key_jwk: None, } } @@ -405,6 +626,7 @@ pub mod test { public_key_hex: None, public_key_base58: None, public_key_base64: None, + public_key_jwk: None, }], authentication: vec![verification_relationship(&kid)], assertion_method: vec![verification_relationship(&kid)], @@ -417,7 +639,175 @@ pub mod test { } fn did_doc_param() -> ContractParam { - ContractParam::Bytes(serde_json::to_vec(&did_doc(TEST_ETHR_DID)).unwrap()) + ContractParam::Bytes(serde_json::to_vec(&did_doc(TEST_IDENTITY)).unwrap()) + } + + mod validate_did_doc { + use super::*; + + #[test] + fn invalid_did_syntax() { + let did_document = did_doc("did:test:example"); + + let expected_error = VdrError::InvalidDidDocument(format!( + "Incorrect DID: {}", + did_document.id.as_ref() + )); + + let actual_error = did_document.validate().unwrap_err(); + + assert_eq!(actual_error, expected_error); + } + + #[test] + fn invalid_verification_method_id() { + let mut did_document = did_doc(TEST_IDENTITY); + did_document.verification_method[0].id = "did:test:example#test".to_string(); + + let expected_error = VdrError::InvalidDidDocument(format!( + "Invalid verefication method ID: did:test:example#test", + )); + + let actual_error = did_document.validate().unwrap_err(); + + assert_eq!(actual_error, expected_error); + } + + #[test] + fn invalid_verification_method_controller_syntax() { + let mut did_document = did_doc(TEST_IDENTITY); + did_document.verification_method[0].controller = "test".to_string(); + + let expected_error = VdrError::InvalidDidDocument(format!( + "Invalid controller syntax in the verification method: {}", + json!(did_document.verification_method[0]) + )); + + let actual_error = did_document.validate().unwrap_err(); + + assert_eq!(actual_error, expected_error); + } + + #[test] + fn invalid_verification_reference() { + let mut did_document = did_doc(TEST_IDENTITY); + did_document + .authentication + .push(verification_relationship("did:test:example#test")); + + let expected_error = VdrError::InvalidDidDocument(format!( + "Invalid verification reference ID: did:test:example#test", + )); + + let actual_error = did_document.validate().unwrap_err(); + + assert_eq!(actual_error, expected_error); + } + + #[test] + fn nonexistent_verification_method() { + let mut did_document = did_doc(TEST_IDENTITY); + let key_id = format!("{}#{}", TEST_INDYBESU_DID, "key-2"); + did_document + .authentication + .push(verification_relationship(&key_id)); + + let expected_error = VdrError::InvalidDidDocument(format!( + "Verification method not found for reference ID: {key_id}" + )); + + let actual_error = did_document.validate().unwrap_err(); + + assert_eq!(actual_error, expected_error); + } + + #[test] + fn duplicate_verification_method_id() { + let mut did_document = did_doc(TEST_IDENTITY); + did_document + .authentication + .push(VerificationMethodOrReference::VerificationMethod( + verification_method(&did_document.verification_method[0].id), + )); + + let expected_error = + VdrError::InvalidDidDocument("Verification method ID must be unique".to_string()); + + let actual_error = did_document.validate().unwrap_err(); + + assert_eq!(actual_error, expected_error); + } + + #[test] + fn verification_method_without_key_material() { + let mut did_document = did_doc(TEST_IDENTITY); + did_document.verification_method[0].public_key_multibase = None; + + let expected_error = VdrError::InvalidDidDocument(format!( + "No public key was found for the verification method with ID: {}", + &did_document.verification_method[0].id + )); + + let actual_error = did_document.validate().unwrap_err(); + + assert_eq!(actual_error, expected_error); + } + + #[test] + fn verification_method_with_multiple_key_materials() { + let mut did_document = did_doc(TEST_IDENTITY); + did_document.verification_method[0].public_key_base58 = Some(BASE58_KEY.to_string()); + + let expected_error = VdrError::InvalidDidDocument(format!( + "Multiple public keys detected in the verification method with ID: {}", + &did_document.verification_method[0].id + )); + + let actual_error = did_document.validate().unwrap_err(); + + assert_eq!(actual_error, expected_error); + } + + #[test] + fn invalid_service_id() { + let mut did_document = did_doc(TEST_IDENTITY); + did_document.service = vec![service("test")]; + + let expected_error = + VdrError::InvalidDidDocument("Invalid service ID: test".to_string()); + + let actual_error = did_document.validate().unwrap_err(); + + assert_eq!(actual_error, expected_error); + } + + #[test] + fn duplicate_service_id() { + let mut did_document = did_doc(TEST_IDENTITY); + let service_id = format!("{}#{}", TEST_INDYBESU_DID, "service"); + did_document.service = vec![service(&service_id), service(&service_id)]; + + let expected_error = + VdrError::InvalidDidDocument("Service ID must be unique".to_string()); + + let actual_error = did_document.validate().unwrap_err(); + + assert_eq!(actual_error, expected_error); + } + + #[test] + fn valid_did_document() { + let mut did_document = did_doc(TEST_IDENTITY); + let kid = format!("#{}", KEY_1); + did_document.assertion_method = vec![verification_relationship(&kid)]; + + let service_id = format!("{}#{}", TEST_INDYBESU_DID, "service"); + did_document.service = vec![service(&service_id)]; + + let result = did_document.validate(); + + assert_eq!(result, Ok(())); + } } mod convert_into_contract_param { @@ -425,7 +815,7 @@ pub mod test { #[test] fn convert_did_doc_into_contract_param_test() { - let param: ContractParam = (&did_doc(TEST_ETHR_DID)).try_into().unwrap(); + let param: ContractParam = (&did_doc(TEST_IDENTITY)).try_into().unwrap(); assert_eq!(did_doc_param(), param); } } @@ -437,7 +827,7 @@ pub mod test { fn convert_contract_output_into_did_doc() { let data = ContractOutput::new(vec![did_doc_param()]); let converted = DidDocument::try_from(&data).unwrap(); - assert_eq!(did_doc(TEST_ETHR_DID), converted); + assert_eq!(did_doc(TEST_IDENTITY), converted); } } } diff --git a/vdr/src/contracts/did/types/did_doc_attribute.rs b/vdr/src/contracts/did/types/did_doc_attribute.rs index 5c28412c..c1470a3b 100644 --- a/vdr/src/contracts/did/types/did_doc_attribute.rs +++ b/vdr/src/contracts/did/types/did_doc_attribute.rs @@ -1,6 +1,9 @@ use crate::{ contracts::{ - did::types::did_events::{DidAttributeChanged, DidEvents}, + did::types::{ + did_doc::ServiceType, + did_events::{DidAttributeChanged, DidEvents}, + }, ServiceEndpoint, }, types::ContractParam, @@ -213,7 +216,7 @@ impl From for VerificationKeyType { #[serde(rename_all = "camelCase")] pub struct ServiceAttribute { #[serde(rename = "type")] - pub type_: String, + pub type_: ServiceType, pub service_endpoint: ServiceEndpoint, } @@ -237,9 +240,10 @@ impl DidDocAttribute { key_encoding ))) } - DidDocAttribute::Service(service) => { - Ok(DidDocAttributeName(format!("did/svc/{}", service.type_))) - } + DidDocAttribute::Service(service) => Ok(DidDocAttributeName(format!( + "did/svc/{}", + service.type_.to_string() + ))), } } @@ -350,7 +354,7 @@ impl TryFrom<&DidAttributeChanged> for DidDocAttribute { }; Ok(DidDocAttribute::Service(ServiceAttribute { - type_: type_.to_string(), + type_: ServiceType::try_from(*type_)?, service_endpoint, })) } diff --git a/vdr/src/contracts/did/types/did_doc_builder.rs b/vdr/src/contracts/did/types/did_doc_builder.rs index 0f6087a7..0498367d 100644 --- a/vdr/src/contracts/did/types/did_doc_builder.rs +++ b/vdr/src/contracts/did/types/did_doc_builder.rs @@ -5,7 +5,7 @@ use crate::{ contracts::{ did::{ types::did_doc::{ - Service, ServiceEndpoint, StringOrVector, VerificationMethod, + Service, ServiceEndpoint, ServiceType, StringOrVector, VerificationMethod, VerificationMethodOrReference, BASE_CONTEXT, }, KEYS_CONTEXT, SECPK_CONTEXT, @@ -66,6 +66,7 @@ impl DidDocumentBuilder { None, None, None, + None, ); did_doc_builder.add_authentication_reference(kid)?; did_doc_builder.add_assertion_method_reference(kid)?; @@ -105,6 +106,7 @@ impl DidDocumentBuilder { public_key_hex: Option<&str>, public_key_base58: Option<&str>, public_key_base64: Option<&str>, + public_key_jwk: Option<&str>, ) { let verification_method = VerificationMethod { id: id.to_string(), @@ -115,6 +117,7 @@ impl DidDocumentBuilder { public_key_hex: public_key_hex.map(String::from), public_key_base58: public_key_base58.map(String::from), public_key_base64: public_key_base64.map(String::from), + public_key_jwk: public_key_jwk.map(String::from), }; self.verification_method .push((key.to_string(), verification_method)); @@ -131,6 +134,7 @@ impl DidDocumentBuilder { public_key_hex: Option<&str>, public_key_base58: Option<&str>, public_key_base64: Option<&str>, + public_key_jwk: Option<&str>, ) { self.key_index += 1; let id = format!("{}#delegate-{}", self.id.as_ref(), self.key_index); @@ -143,6 +147,7 @@ impl DidDocumentBuilder { public_key_hex: public_key_hex.map(String::from), public_key_base58: public_key_base58.map(String::from), public_key_base64: public_key_base64.map(String::from), + public_key_jwk: public_key_jwk.map(String::from), }; self.verification_method .push((key.to_string(), verification_method)); @@ -282,7 +287,7 @@ impl DidDocumentBuilder { &mut self, key: &str, id: Option<&str>, - type_: &str, + type_: &ServiceType, endpoint: &ServiceEndpoint, ) { self.service_index += 1; @@ -291,7 +296,7 @@ impl DidDocumentBuilder { .unwrap_or_else(|| format!("{}#service-{}", self.id.as_ref(), self.service_index)); let service = Service { id, - type_: type_.to_string(), + type_: type_.clone(), service_endpoint: endpoint.clone(), }; self.service.push((key.to_string(), service)); @@ -391,11 +396,15 @@ impl DidDocumentBuilder { #[cfg(test)] pub mod test { use super::*; + use crate::{ client::client::test::TEST_ACCOUNT, - contracts::types::did_doc::test::{ - default_ethr_did_document, SERVICE_ENDPOINT, SERVICE_TYPE, TEST_ETHR_DID, - TEST_ETHR_DID_WITHOUT_NETWORK, + contracts::{ + types::did_doc::test::{ + default_ethr_did_document, SERVICE_ENDPOINT, TEST_ETHR_DID, + TEST_ETHR_DID_WITHOUT_NETWORK, + }, + ServiceType, }, }; @@ -426,6 +435,7 @@ pub mod test { None, None, None, + None, ); builder.add_delegate_key( KEY_2_INDEX, @@ -435,6 +445,7 @@ pub mod test { None, Some("FbQWLPRhTH95MCkQUeFYdiSoQt8zMwetqfWoxqPgaq7x"), None, + None, ); builder.add_delegate_key( KEY_3_INDEX, @@ -444,6 +455,7 @@ pub mod test { Some("02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71"), None, None, + None, ); builder.add_assertion_method_reference(KEY_1_INDEX).unwrap(); builder.add_assertion_method_reference(KEY_2_INDEX).unwrap(); @@ -453,7 +465,7 @@ pub mod test { builder.add_service( SERVICE_1_INDEX, None, - SERVICE_TYPE, + &ServiceType::LinkedDomains, &ServiceEndpoint::String(SERVICE_ENDPOINT.to_string()), ); let did_document = builder.build(); diff --git a/vdr/src/error/mod.rs b/vdr/src/error/mod.rs index b9557ae7..7f0bc5d5 100644 --- a/vdr/src/error/mod.rs +++ b/vdr/src/error/mod.rs @@ -66,6 +66,9 @@ pub enum VdrError { #[error("Could not get transaction: {}", _0)] GetTransactionError(String), + #[error("Invalid DID document: {}", _0)] + InvalidDidDocument(String), + #[error("Invalid schema: {}", _0)] InvalidSchema(String), diff --git a/vdr/src/utils/mod.rs b/vdr/src/utils/mod.rs index fc2cd74a..0e3dfcdc 100644 --- a/vdr/src/utils/mod.rs +++ b/vdr/src/utils/mod.rs @@ -1,6 +1,7 @@ mod common; use log_derive::{logfn, logfn_inputs}; +use std::{collections::HashSet, hash::Hash}; use crate::{VdrError, VdrResult}; #[cfg(test)] @@ -52,3 +53,12 @@ pub fn parse_bytes32_string(bytes: &[u8]) -> VdrResult<&str> { )) }) } + +pub fn is_unique(iter: T) -> bool +where + T: IntoIterator, + T::Item: Eq + Hash, +{ + let mut unique = HashSet::new(); + iter.into_iter().all(|item| unique.insert(item)) +} diff --git a/vdr/wasm/demo/node/src/main.ts b/vdr/wasm/demo/node/src/main.ts index 9bd395c3..f4d5f06d 100644 --- a/vdr/wasm/demo/node/src/main.ts +++ b/vdr/wasm/demo/node/src/main.ts @@ -51,7 +51,7 @@ async function demo() { console.log('2. Publish and Modify DID') const did = 'did:ethr:' + identity.address - const serviceAttribute = {"serviceEndpoint":"http://10.0.0.2","type":"TestService"} + const serviceAttribute = {"serviceEndpoint":"http://10.0.0.2","type":"LinkedDomains"} const validity = BigInt(1000) let endorsingData = await EthrDidRegistry.buildDidSetAttributeEndorsingData(client, did, serviceAttribute, validity) let authorSignature = sign(endorsingData.getSigningBytes(), identity.secret)