From 8fff89ad5e4d258e30667fee00ea0f33c031eaa2 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Tue, 25 Jan 2022 10:11:36 -0500 Subject: [PATCH 1/9] Relax DIDMethod: DIDResolver requirement --- src/did.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/did.rs b/src/did.rs index eddb1bccf..16612c301 100644 --- a/src/did.rs +++ b/src/did.rs @@ -395,7 +395,7 @@ pub struct DIDParameters { /// Registries](https://www.w3.org/TR/did-spec-registries/#did-methods). #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait DIDMethod: DIDResolver { +pub trait DIDMethod: Sync { /// Get the DID method's name. /// /// `method-name` in [DID Syntax](https://w3c.github.io/did-core/#did-syntax). @@ -503,7 +503,7 @@ impl<'a> DIDResolver for DIDMethods<'a> { Ok(method) => method, Err(err) => return (ResolutionMetadata::from_error(err), None, None), }; - method.resolve(did, input_metadata).await + method.to_resolver().resolve(did, input_metadata).await } /// Resolve a DID to a DID document representation, using the corresponding DID method in the @@ -517,7 +517,10 @@ impl<'a> DIDResolver for DIDMethods<'a> { Ok(method) => method, Err(err) => return (ResolutionMetadata::from_error(err), Vec::new(), None), }; - method.resolve_representation(did, input_metadata).await + method + .to_resolver() + .resolve_representation(did, input_metadata) + .await } /// Dereference a DID URL, using the corresponding DID method in the @@ -537,7 +540,10 @@ impl<'a> DIDResolver for DIDMethods<'a> { )) } }; - method.dereference(did_url, input_metadata).await + method + .to_resolver() + .dereference(did_url, input_metadata) + .await } } From c9bc80e38081223b6696e5a619c8fbaa5ca29490 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Thu, 20 Jan 2022 14:18:50 -0500 Subject: [PATCH 2/9] Fix WASM compilation with http-did feature - Use reqwest. - Keep hyper as dev-dependency for testing server. - Remove hyper-tls as unused. --- Cargo.toml | 12 ++--- src/did_resolve.rs | 132 ++++++++++++++++++++++----------------------- 2 files changed, 68 insertions(+), 76 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cb5500401..d4c8cd668 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,8 @@ exclude = ["json-ld-api/*", "json-ld-normalization/*"] [features] default = ["ring"] -http-did = ["hyper", "hyper-tls", "http", "percent-encoding", "tokio"] -libsecp256k1 = ["secp256k1"] # backward compatibility +http-did = ["http", "percent-encoding"] +libsecp256k1 = ["secp256k1"] # backward compatibility secp256k1 = ["k256", "rand", "k256/keccak256"] secp256r1 = ["p256", "rand"] aleosig = ["rand", "blake2", "snarkvm-dpc", "snarkvm-algorithms", "snarkvm-curves", "snarkvm-utilities", "snarkvm-parameters"] @@ -57,13 +57,6 @@ lazy_static = "1.4" combination = "0.1" sha2 = { version = "0.9", optional = true } sha2_old = { package = "sha2", version = "0.8" } -hyper = { version = "0.14", optional = true, features = [ - "server", - "client", - "http1", - "stream", -] } -hyper-tls = { version = "0.5", optional = true } http = { version = "0.2", optional = true } hex = "0.4" serde_urlencoded = "0.7" @@ -123,6 +116,7 @@ uuid = { version = "0.8", features = ["v4", "serde"] } difference = "2.0" did-method-key = { path = "./did-key" } tokio = { version = "1.0", features = ["macros"] } +hyper = { version = "0.14", features = ["server", "http1", "stream"] } [package.metadata.docs.rs] features = ["secp256r1", "secp256k1", "ripemd-160", "http-did"] diff --git a/src/did_resolve.rs b/src/did_resolve.rs index 4e7027be8..00e35a30d 100644 --- a/src/did_resolve.rs +++ b/src/did_resolve.rs @@ -5,9 +5,7 @@ use async_trait::async_trait; use chrono::prelude::{DateTime, Utc}; #[cfg(feature = "http-did")] -use hyper::{header, Client, Request, StatusCode, Uri}; -#[cfg(feature = "http-did")] -use hyper_tls::HttpsConnector; +use reqwest::{header, Client, StatusCode, Url}; use serde::{Deserialize, Serialize}; use serde_json; use serde_json::Value; @@ -930,8 +928,8 @@ impl DIDResolver for HTTPDIDResolver { url.push('?'); url.push_str(&querystring); } - let uri: Uri = match url.parse() { - Ok(uri) => uri, + let url: Url = match url.parse() { + Ok(url) => url, Err(_) => { return ( ResolutionMetadata { @@ -944,54 +942,36 @@ impl DIDResolver for HTTPDIDResolver { ) } }; - let https = HttpsConnector::new(); - let client = Client::builder().build::<_, hyper::Body>(https); - let request = match Request::get(uri) - .header("Accept", TYPE_DID_RESOLUTION) - .header("User-Agent", crate::USER_AGENT) - .body(hyper::Body::default()) - { - Ok(req) => req, + let client = match Client::builder().build() { + Ok(client) => client, Err(err) => { return ( - ResolutionMetadata { - error: Some("Error building HTTP request: ".to_string() + &err.to_string()), - content_type: None, - property_set: None, - }, + ResolutionMetadata::from_error(&format!( + "Error building HTTP client: {}", + err.to_string() + )), None, None, - ) + ); } }; - let mut resp = match client.request(request).await { + let resp = match client + .get(url) + .header("Accept", TYPE_DID_RESOLUTION) + .header("User-Agent", crate::USER_AGENT) + .send() + .await + { Ok(resp) => resp, Err(err) => { return ( - ResolutionMetadata { - error: Some("HTTP Error: ".to_string() + &err.to_string()), - content_type: None, - property_set: None, - }, - None, - None, - ) - } - }; - let res_result_representation = match hyper::body::to_bytes(resp.body_mut()).await { - Ok(vec) => vec, - Err(err) => { - return ( - ResolutionMetadata { - error: Some("Error reading HTTP response: ".to_string() + &err.to_string()), - content_type: None, - property_set: None, - }, + ResolutionMetadata::from_error(&format!("Error sending HTTP request: {}", err)), None, None, ) } }; + let status = resp.status(); let content_type = match resp.headers().get(header::CONTENT_TYPE) { None => None, Some(content_type) => Some(String::from(match content_type.to_str() { @@ -1009,13 +989,27 @@ impl DIDResolver for HTTPDIDResolver { })), } .unwrap_or_else(|| "".to_string()); + let res_result_representation = match resp.bytes().await { + Ok(bytes) => bytes.to_vec(), + Err(err) => { + return ( + ResolutionMetadata { + error: Some("Error reading HTTP response: ".to_string() + &err.to_string()), + content_type: None, + property_set: None, + }, + None, + None, + ) + } + }; if content_type == TYPE_DID_RESOLUTION { // Handle result using DID Resolution Result media type (JSON-LD) return transform_resolution_result(serde_json::from_slice(&res_result_representation)); } - if resp.status() == StatusCode::NOT_FOUND { + if status == StatusCode::NOT_FOUND { return (ResolutionMetadata::from_error(ERROR_NOT_FOUND), None, None); } @@ -1087,8 +1081,8 @@ impl DIDResolver for HTTPDIDResolver { url.push('?'); url.push_str(&querystring); } - let uri: Uri = match url.parse() { - Ok(uri) => uri, + let url: Url = match url.parse() { + Ok(url) => url, Err(_) => { return Some(( DereferencingMetadata::from_error(ERROR_INVALID_DID), @@ -1097,29 +1091,33 @@ impl DIDResolver for HTTPDIDResolver { )) } }; - let https = HttpsConnector::new(); - let client = Client::builder().build::<_, hyper::Body>(https); - let request = match Request::get(uri) - .header("Accept", TYPE_DID_RESOLUTION) - .body(hyper::Body::default()) - { - Ok(req) => req, + let client = match Client::builder().build() { + Ok(client) => client, Err(err) => { return Some(( DereferencingMetadata::from_error(&format!( - "Error building HTTP request: {}", - err + "Error building HTTP client: {}", + err.to_string() )), Content::Null, ContentMetadata::default(), - )) + )); } }; - let mut resp = match client.request(request).await { + let resp = match client + .get(url) + .header("Accept", TYPE_DID_RESOLUTION) + .header("User-Agent", crate::USER_AGENT) + .send() + .await + { Ok(resp) => resp, Err(err) => { return Some(( - DereferencingMetadata::from_error(&format!("HTTP Error: {}", err)), + DereferencingMetadata::from_error(&format!( + "Error sending HTTP request: {}", + err + )), Content::Null, ContentMetadata::default(), )) @@ -1134,19 +1132,6 @@ impl DIDResolver for HTTPDIDResolver { StatusCode::NOT_ACCEPTABLE => Some(ERROR_REPRESENTATION_NOT_SUPPORTED.to_string()), _ => None, }; - let deref_result_bytes = match hyper::body::to_bytes(resp.body_mut()).await { - Ok(vec) => vec, - Err(err) => { - return Some(( - DereferencingMetadata::from_error(&format!( - "Error reading HTTP response: {}", - err - )), - Content::Null, - ContentMetadata::default(), - )) - } - }; let content_type = match resp.headers().get(header::CONTENT_TYPE) { None => None, Some(content_type) => Some(String::from(match content_type.to_str() { @@ -1164,6 +1149,19 @@ impl DIDResolver for HTTPDIDResolver { })), } .unwrap_or_else(|| "".to_string()); + let deref_result_bytes = match resp.bytes().await { + Ok(bytes) => bytes.to_vec(), + Err(err) => { + return Some(( + DereferencingMetadata::from_error(&format!( + "Error reading HTTP response: {}", + err + )), + Content::Null, + ContentMetadata::default(), + )) + } + }; match &content_type[..] { TYPE_DID_LD_JSON | TYPE_DID_JSON => { let doc: Document = match serde_json::from_slice(&deref_result_bytes) { From 685046a9058c94132ce30d967359cc52f8d52816 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Mon, 14 Feb 2022 15:11:44 -0500 Subject: [PATCH 3/9] Add DID operations --- Cargo.toml | 1 + src/did.rs | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index d4c8cd668..92c6e089f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ flate2 = "1.0" bitvec = "0.20" clear_on_drop = "0.2.4" url = { version = "2.2", features = ["serde"] } +anyhow = "1.0" rand_xorshift = "0.3" bech32 = "0.8" snarkvm-dpc = { version = "0.7.9", optional = true } diff --git a/src/did.rs b/src/did.rs index 16612c301..c90d9aabd 100644 --- a/src/did.rs +++ b/src/did.rs @@ -5,6 +5,7 @@ //! [did-core]: https://www.w3.org/TR/did-core/ use crate::caip10::BlockchainAccountId; +use anyhow::{bail, Result as AResult}; use std::collections::BTreeMap as Map; use std::collections::HashMap; use std::convert::TryFrom; @@ -386,6 +387,90 @@ pub struct DIDParameters { pub property_set: Option>, } +/// DID Create Operation +/// +/// +pub struct DIDCreate { + pub update_key: Option, + pub recovery_key: Option, + pub verification_key: Option, + pub options: Value, +} + +/// DID Update Operation +/// +/// +pub struct DIDUpdate { + pub did: String, + pub update_key: Option, + pub new_update_key: Option, + pub operation: DIDDocumentOperation, + pub options: Value, +} + +/// DID Recover Operation +/// +/// +pub struct DIDRecover { + pub did: String, + pub recovery_key: Option, + pub new_update_key: Option, + pub new_recovery_key: Option, + pub new_verification_key: Option, + pub options: Value, +} + +/// DID Deactivate Operation +/// +/// +pub struct DIDDeactivate { + pub did: String, + pub key: Option, + pub options: Value, +} + +/// DID Document Operation +/// +/// This should represent [didDocument][dd] and [didDocumentOperation][ddo] specified by DID +/// Registration. +/// +/// [dd]: https://identity.foundation/did-registration/#diddocumentoperation +/// [ddo]: https://identity.foundation/did-registration/#diddocument +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "didDocumentOperation", content = "didDocument")] +#[serde(rename_all = "camelCase")] +pub enum DIDDocumentOperation { + /// Set the contents of the DID document + /// + /// setDidDocument operation defined by DIF DID Registration + SetDidDocument(Document), + + /// Add properties to the DID document + /// + /// addToDidDocument operation defined by DIF DID Registration + AddToDidDocument(HashMap), + + /// Remove properties from the DID document + /// + /// removeFromDidDocument operation defined by DIF Registration + RemoveFromDidDocument(Vec), + + /// Add or update a verification method in the DID document + SetVerificationMethod { + vmm: VerificationMethodMap, + purposes: Vec, + }, + + /// Add or update a service map in the DID document + SetService(Service), + + /// Remove a verification method in the DID document + RemoveVerificationMethod(DIDURL), + + /// Add or update a service map in the DID document + RemoveService(DIDURL), +} + /// An implementation of a [DID method](https://www.w3.org/TR/did-core/#dfn-did-methods). /// /// Depends on the [DIDResolver][] trait. @@ -407,6 +492,26 @@ pub trait DIDMethod: Sync { None } + /// Create a DID + fn create(&self, _create: DIDCreate) -> AResult { + bail!("Create operation not implemented for DID Method"); + } + + /// Update a DID + fn update(&self, _update: DIDUpdate) -> AResult { + bail!("Update operation not implemented for DID Method"); + } + + /// Recover a DID + fn recover(&self, _recover: DIDRecover) -> AResult { + bail!("Recover operation not implemented for DID Method"); + } + + /// Deactivate a DID + fn deactivate(&self, _deactivate: DIDDeactivate) -> AResult { + bail!("Deactivate operation not implemented for DID Method"); + } + /// Upcast the DID method as a DID resolver. /// /// This is a workaround for [not being able to cast a trait object to a supertrait object](https://github.com/rust-lang/rfcs/issues/2765). From afb1fcd00d88c22a3d027cb87ecb728bd77791a9 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Mon, 14 Feb 2022 15:12:09 -0500 Subject: [PATCH 4/9] Add DID method transactions --- src/did.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/did.rs b/src/did.rs index c90d9aabd..f36b50fd6 100644 --- a/src/did.rs +++ b/src/did.rs @@ -471,6 +471,18 @@ pub enum DIDDocumentOperation { RemoveService(DIDURL), } +/// A transaction for a DID method +#[derive(Debug, Serialize, Deserialize, Builder, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct DIDMethodTransaction { + /// DID method name + pub did_method: String, + + /// Method-specific transaction data + #[serde(flatten)] + pub value: Value, +} + /// An implementation of a [DID method](https://www.w3.org/TR/did-core/#dfn-did-methods). /// /// Depends on the [DIDResolver][] trait. @@ -492,6 +504,16 @@ pub trait DIDMethod: Sync { None } + /// Retrieve a DID from a DID method transaction + fn did_from_transaction(&self, _tx: DIDMethodTransaction) -> AResult { + bail!("DID from transaction not implemented for DID Method"); + } + + /// Submit a DID transaction + async fn submit_transaction(&self, _tx: DIDMethodTransaction) -> AResult { + bail!("Transaction submission not implemented for DID Method"); + } + /// Create a DID fn create(&self, _create: DIDCreate) -> AResult { bail!("Create operation not implemented for DID Method"); From dcb203fcb92d527f2c3813be8892c9fb0890e18b Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Mon, 14 Feb 2022 15:12:49 -0500 Subject: [PATCH 5/9] Add did:ion and Sidetree --- Cargo.toml | 1 + did-ion/Cargo.toml | 33 + did-ion/README.md | 17 + did-ion/src/lib.rs | 26 + did-ion/src/sidetree.rs | 1926 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 2003 insertions(+) create mode 100644 did-ion/Cargo.toml create mode 100644 did-ion/README.md create mode 100644 did-ion/src/lib.rs create mode 100644 did-ion/src/sidetree.rs diff --git a/Cargo.toml b/Cargo.toml index 92c6e089f..71b2d3e64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,7 @@ members = [ "did-sol", "did-pkh", "did-onion", + "did-ion", "did-webkey", "vc-test", "did-test", diff --git a/did-ion/Cargo.toml b/did-ion/Cargo.toml new file mode 100644 index 000000000..d2875509a --- /dev/null +++ b/did-ion/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "did-ion" +version = "0.1.0" +authors = ["Spruce Systems, Inc."] +edition = "2021" +license = "Apache-2.0" +keywords = ["ssi", "did"] +categories = ["web-programming::http-client"] +description = "did:ion DID method implementation, using the ssi crate and ION/Sidetree REST API" +repository = "https://github.com/spruceid/ssi/" +homepage = "https://github.com/spruceid/ssi/tree/main/did-ion/" +documentation = "https://docs.rs/did-ion/" + +[features] + +[dependencies] +ssi = { version = "0.3", path = "../", default-features = false, features = ["http-did", "secp256k1"] } +async-trait = "0.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_jcs = "0.1" +anyhow = "1.0" +base64 = "0.12" +sha2 = "0.10" +json-patch = "0.2.6" +reqwest = { version = "0.11", features = ["json"] } + +[target.'cfg(target_os = "android")'.dependencies.reqwest] +version = "0.11" +features = ["json", "native-tls-vendored"] + +[dev-dependencies] +lazy_static = "1.4" diff --git a/did-ion/README.md b/did-ion/README.md new file mode 100644 index 000000000..490da06c6 --- /dev/null +++ b/did-ion/README.md @@ -0,0 +1,17 @@ +# did-ion + +Rust implementation of the [did:ion][] [DID Method][], based on the [ssi][] library. + +## Requirements + +An ION node is needed, that is a [Sidetree REST API][] provider. The URL for +the ION/Sidetree node is to be stored in config TBD. + +## License + +[Apache License, Version 2.0](http://www.apache.org/licenses/) + +[did:ion]: https://identity.foundation/ion/ +[DID Method]: https://www.w3.org/TR/did-core/#methods +[ssi]: https://github.com/spruceid/ssi/ +[Sidetree REST API]: https://identity.foundation/sidetree/api/ diff --git a/did-ion/src/lib.rs b/did-ion/src/lib.rs new file mode 100644 index 000000000..662caf2a9 --- /dev/null +++ b/did-ion/src/lib.rs @@ -0,0 +1,26 @@ +use anyhow::{ensure, Context, Error, Result}; +use ssi::jwk::{Algorithm, JWK}; + +pub mod sidetree; + +use sidetree::{is_secp256k1, Sidetree, SidetreeClient}; + +pub struct ION; + +/// did:ion Method +pub type DIDION = SidetreeClient; + +impl Sidetree for ION { + fn generate_key() -> Result { + JWK::generate_secp256k1().context("Generate secp256k1 key") + } + + fn validate_key(key: &JWK) -> Result<(), Error> { + ensure!(is_secp256k1(&key), "Key must be Secp256k1 for ION"); + Ok(()) + } + + const SIGNATURE_ALGORITHM: Algorithm = Algorithm::ES256K; + const METHOD: &'static str = "ion"; + const NETWORK: Option<&'static str> = Some("test"); +} diff --git a/did-ion/src/sidetree.rs b/did-ion/src/sidetree.rs new file mode 100644 index 000000000..402e836b7 --- /dev/null +++ b/did-ion/src/sidetree.rs @@ -0,0 +1,1926 @@ +use anyhow::{anyhow, bail, ensure, Context, Error, Result}; +use async_trait::async_trait; +use core::fmt::Debug; +use json_patch::Patch; +use reqwest::{header, Client, StatusCode}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; +use ssi::did::{ + DIDCreate, DIDDeactivate, DIDDocumentOperation, DIDMethod, DIDMethodTransaction, DIDRecover, + DIDUpdate, Document, Service, ServiceEndpoint, VerificationRelationship, +}; +use ssi::did_resolve::{ + DIDResolver, DocumentMetadata, HTTPDIDResolver, ResolutionInputMetadata, ResolutionMetadata, + ERROR_INVALID_DID, +}; +use ssi::jwk::{Algorithm, Base64urlUInt, JWK}; +use ssi::jws::Header; +use ssi::one_or_many::OneOrMany; +use std::convert::TryFrom; +use std::fmt; +use std::marker::PhantomData; +use std::str::FromStr; + +const MULTIHASH_SHA2_256_PREFIX: &[u8] = &[0x12]; +const MULTIHASH_SHA2_256_SIZE: &[u8] = &[0x20]; + +/// Verification method type for Create operation +/// +/// This is used when converting JWK to [verification method map][vmm] for the Create operation. +/// +/// Reference: [Sidetree §12.1.1 `add-public-keys`][apk] Step 3.2 +/// +/// [apk]: https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys +/// [vmm]: https://www.w3.org/TR/did-core/#verification-methods +pub const VERIFICATION_METHOD_TYPE: &str = "JsonWebSignature2020"; + +/// Parameters for a Sidetree client implementation +/// +/// This trait consistest of the subset of parameters defined in [Sidetree §5. Default Parameters][default-params] that are needed to implemented a Sidetree client, that is a client to the [Sidetree REST API][sidetree-rest]. +/// +/// [default-params]: https://identity.foundation/sidetree/spec/v1.0.0/#default-parameters +/// [sidetree-rest]: https://identity.foundation/sidetree/api/ +pub trait Sidetree { + /// [`HASH_PROTOCOL`](https://identity.foundation/sidetree/spec/v1.0.0/#hash-protocol) + /// + /// This should be implemented using [hash_algorithm]. + /// + /// Default implementation calls [hash_protocol_algorithm] and returns the concatenation of the + /// prefix and hash. + /// + /// This function must correspond with [hash_algorithm]. To ensure that correspondence, + /// implementers may want to override [hash_protocol_algorithm] instead of this function. + /// + /// [hash_algorithm]: Self::hash_algorithm + /// [hash_protocol_algorithm]: Self::hash_protocol_algorithm + fn hash_protocol(data: &[u8]) -> Vec { + let (prefix, hash) = Self::hash_protocol_algorithm(data); + [prefix, hash].concat() + } + + /// [`HASH_ALGORITHM`](https://identity.foundation/sidetree/spec/v1.0.0/#hash-algorithm) + /// + /// Default implementation calls [hash_protocol_algorithm] and returns the hash, discarding the + /// prefix. + /// + /// This function must correspond with [hash_protocol]. To ensure that correspondence, + /// implementers may want to override [hash_protocol_algorithm] instead of this function. + /// + /// [hash_protocol]: Self::hash_protocol + /// [hash_protocol_algorithm]: Self::hash_protocol_algorithm + fn hash_algorithm(data: &[u8]) -> Vec { + let (_prefix, hash) = Self::hash_protocol_algorithm(data); + hash + } + + /// Combination of [hash_protocol] and [hash_algorithm] + /// + /// Returns multihash prefix and hash. + /// + /// Default implementation: SHA-256 (`sha2-256`) + /// + /// [hash_protocol] and [hash_algorithm] must correspond, and their default implementations + /// call this function ([hash_protocol_algorithm]). Implementers are therefore encouraged to + /// overwrite this function ([hash_protocol_algorithm]) rather than those ([hash_protocol] and + /// [hash_algorithm]). + /// + /// [hash_protocol]: Self::hash_protocol + /// [hash_algorithm]: Self::hash_algorithm + /// [hash_protocol_algorithm]: Self::hash_protocol_algorithm + fn hash_protocol_algorithm(data: &[u8]) -> (Vec, Vec) { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(data); + let hash = hasher.finalize().to_vec(); + ( + [MULTIHASH_SHA2_256_PREFIX, MULTIHASH_SHA2_256_SIZE].concat(), + hash, + ) + } + + /// [`DATA_ENCODING_SCHEME`](https://identity.foundation/sidetree/spec/v1.0.0/#data-encoding-scheme) + fn data_encoding_scheme(data: &[u8]) -> String { + base64::encode_config(data, base64::URL_SAFE_NO_PAD) + } + + /// [`JSON_CANONICALIZATION_SCHEME`](https://identity.foundation/sidetree/spec/v1.0.0/#json-canonicalization-scheme) + fn json_canonicalization_scheme(value: &T) -> Result { + serde_jcs::to_string(value).context("JSON Canonicalization Scheme (JCS)") + } + + /// Generate a new keypair ([KEY_ALGORITHM][ka]) + /// + /// [ka]: https://identity.foundation/sidetree/spec/v1.0.0/#key-algorithm + fn generate_key() -> Result; + + /// Ensure that a keypair is valid for this Sidetree DID Method + /// + /// Check that the key uses this Sidetree DID method's [KEY_ALGORITHM][ka]. + /// + /// [ka]: https://identity.foundation/sidetree/spec/v1.0.0/#key-algorithm + fn validate_key(key: &JWK) -> Result<(), Error>; + + /// [`SIGNATURE_ALGORITHM`](https://identity.foundation/sidetree/spec/v1.0.0/#sig-algorithm) (JWS alg) + const SIGNATURE_ALGORITHM: Algorithm; + + /// [`REVEAL_VALUE`](https://identity.foundation/sidetree/spec/v1.0.0/#reveal-value) + fn reveal_value(commitment_value: &[u8]) -> String { + // The spec implies that REVEAL_VALUE uses HASH_PROTOCOL, in §6.2.1: + // "Use the implementation’s HASH_PROTOCOL to hash the canonicalized public key to generate the REVEAL_VALUE" + // https://identity.foundation/sidetree/spec/v1.0.0/#public-key-commitment-scheme + let hash = Self::hash_protocol(commitment_value); + Self::data_encoding_scheme(&hash) + } + + /// [`MAX_OPERATION_HASH_LENGTH`](https://identity.foundation/sidetree/spec/v1.0.0/#max-operation-hash-length) + const MAX_OPERATION_HASH_LENGTH: usize = 100; + + /// [`NONCE_SIZE`](https://identity.foundation/sidetree/spec/v1.0.0/#nonce-size) + const NONCE_SIZE: usize = 16; + + /// Method name for Sidetree-based DID + /// + /// Mentioned in [Sidetree §9. DID URI Composition](https://identity.foundation/sidetree/spec/v1.0.0/#did-uri-composition) + const METHOD: &'static str; + + /// Network instance + /// + /// Additional segment after the method-id (METHOD), as a prefix for the method-specific-id + /// (DID Suffix), identifiying a network instance. e.g. "testnet" + /// + /// Mentioned in [Note 1](https://identity.foundation/sidetree/spec/v1.0.0/#note-1) + const NETWORK: Option<&'static str> = None; + + /// Maximum length of `controller` property + /// + /// Reference: [Sidetree §12.1.1 `add-public-keys`](https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys) + const MAX_CONTROLLER_LENGTH: Option = None; + + /// Maximum length of `publicKeyMultibase` property + /// + /// Reference: [Sidetree §12.1.1 `add-public-keys`](https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys) + const MAX_PKMB_LENGTH: Option = None; + + /// Hash and encode data + /// + /// [Sidetree §6.1 Hashing Process](https://identity.foundation/sidetree/spec/#hashing-process) + fn hash(data: &[u8]) -> String { + let hash = Self::hash_protocol(data); + /* + ensure!( + hash.len() <= Self::MAX_OPERATION_HASH_LENGTH, + "Hash is too long" + ); + */ + Self::data_encoding_scheme(&hash) + } + + /// [Public Key Commitment Scheme (Sidetree §6.2.1)][pkcs] + /// + /// [pkcs]: https://identity.foundation/sidetree/spec/v1.0.0/#public-key-commitment-scheme + fn commitment_scheme(pkjwk: &PublicKeyJwk) -> Result { + let canonicalized_public_key = + Self::json_canonicalization_scheme(&pkjwk).context("Canonicalize JWK")?; + // Note: hash_algorithm called here instead of reveal_value, since the underlying hash is + // used, not the encoded/prefixed one. + let reveal_value = Self::hash_algorithm(canonicalized_public_key.as_bytes()); + let commitment = Self::hash(&reveal_value); + Ok(commitment) + } + + /// Create a Sidetree-based DID using existing keys + /// + /// This function creates a Sidetree-based DID using existing public keys for + /// the update key and recovery key and respective + /// [commitments][]. + /// + /// Sidetree specifies in ([§11.1 Create][create]) that creating a Sidetree DID involves + /// generating a Update keypair and Recovery keypair. That is implemented in [Self::create]. + /// + /// **Note**: The Sidetree specification ([§6.2.1 Public Key Commitment + /// Scheme][pkcs]) recommends not reusing public keys across different commitment invocations, and + /// requires not using public key JWK payloads across commitment invocations. + /// + /// [commitments]: https://identity.foundation/sidetree/spec/v1.0.0/#commitment + /// [create]: https://identity.foundation/sidetree/spec/v1.0.0/#create + /// [pkcs]: https://identity.foundation/sidetree/spec/v1.0.0/#public-key-commitment-scheme + fn create_existing( + update_pk: &PublicKeyJwk, + recovery_pk: &PublicKeyJwk, + patches: Vec, + ) -> Result { + ensure!( + update_pk != recovery_pk, + "Update and recovery public key JWK payload must be different." + ); + + let update_commitment = + Self::commitment_scheme(update_pk).context("Generate update commitment")?; + + let create_operation_delta_object = Delta { + patches, + update_commitment, + }; + let delta_string = Self::json_canonicalization_scheme(&create_operation_delta_object) + .context("Canonicalize Create Operation Delta Object")?; + let delta_hash = Self::hash(delta_string.as_bytes()); + + let recovery_commitment = + Self::commitment_scheme(recovery_pk).context("Generate recovery commitment")?; + + let create_operation_suffix_data_object = SuffixData { + r#type: None, + delta_hash, + recovery_commitment, + anchor_origin: None, + }; + + let create_operation = CreateOperation { + suffix_data: create_operation_suffix_data_object, + delta: create_operation_delta_object, + }; + Ok(Operation::Create(create_operation)) + } + + /// Create a Sidetree-based DID + /// + /// Generate keypairs and construct a Create Operation according to [Sidetree §11.1 + /// Create][create]. Returns the private keys and the create operation. + /// + /// [create]: https://identity.foundation/sidetree/spec/v1.0.0/#create + fn create(patches: Vec) -> Result<(Operation, JWK, JWK)> { + let update_keypair = Self::generate_key().context("Generate Update Key Pair")?; + let recovery_keypair = Self::generate_key().context("Generate Recovery Key Pair")?; + let update_pk = + PublicKeyJwk::try_from(update_keypair.to_public()).context("Update public key")?; + let recovery_pk = + PublicKeyJwk::try_from(recovery_keypair.to_public()).context("Recovery public key")?; + let create_op = Self::create_existing(&update_pk, &recovery_pk, patches)?; + Ok((create_op, update_keypair, recovery_keypair)) + } + + /// Create a Sidetree-based DID + /// + /// Construct a DID Update Operation according to [Sidetree §11.2 + /// Update][update]. Returns the update operation. + /// + /// Unlike [Self::create] and [Self::recover], this does not generate keys, since the specification does not + /// call for that here. Instead, the caller must generate a new update keypair, and pass + /// its public key in the `new_update_pk` argument. + /// + /// Using a `update_key` with a [JWK Nonce][jwkn] is not yet supported. + /// + /// [update]: https://identity.foundation/sidetree/spec/v1.0.0/#update + /// [jwkn]: https://identity.foundation/sidetree/spec/#jwk-nonce + fn update( + did_suffix: DIDSuffix, + update_key: &JWK, + new_update_pk: &PublicKeyJwk, + patches: Vec, + ) -> Result { + let update_pk = PublicKeyJwk::try_from(update_key.to_public()) + .context("Convert update key to PublicKeyJwk for Update operation")?; + let canonicalized_update_pk = Self::json_canonicalization_scheme(&update_pk) + .context("Canonicalize update public key for reveal value for Deactivate operation")?; + let update_reveal_value = Self::reveal_value(&canonicalized_update_pk.as_bytes()); + + ensure!( + new_update_pk != &update_pk, + "New update public key must be different." + ); + + let new_update_commitment = + Self::commitment_scheme(&new_update_pk).context("Generate new update commitment")?; + + let update_operation_delta_object = Delta { + patches, + update_commitment: new_update_commitment, + }; + + let delta_string = Self::json_canonicalization_scheme(&update_operation_delta_object) + .context("Canonicalize Update Operation Delta Object")?; + let delta_hash = Self::hash(delta_string.as_bytes()); + + let algorithm = Self::SIGNATURE_ALGORITHM; + let claims = UpdateClaims { + update_key: update_pk, + delta_hash, + }; + let signed_data = ssi::jwt::encode_sign(algorithm, &claims, update_key) + .context("Sign Update Operation")?; + let update_op = UpdateOperation { + did_suffix, + reveal_value: update_reveal_value, + delta: update_operation_delta_object, + signed_data, + }; + Ok(update_op) + } + + /// Recover a Sidetree-based DID using existing keys + /// + /// Like [Self::recover] but does not generate or handle the new update key pair and recovery + /// key pair; instead, their public keys must be provided by the caller in the `new_update_pk` + /// and `new_recovery_pk` arguments. + /// + /// Returns the constructed DID Recover operation. + fn recover_existing( + did_suffix: DIDSuffix, + recovery_key: &JWK, + new_update_pk: &PublicKeyJwk, + new_recovery_pk: &PublicKeyJwk, + patches: Vec, + ) -> Result { + let recovery_pk = PublicKeyJwk::try_from(recovery_key.to_public()) + .context("Convert recovery key to PublicKeyJwk for Recover operation")?; + ensure!( + new_recovery_pk != &recovery_pk, + "New recovery public key must be different." + ); + let canonicalized_recovery_pk = Self::json_canonicalization_scheme(&recovery_pk) + .context("Canonicalize recovery public key for reveal value for Recover operation")?; + let recover_reveal_value = Self::reveal_value(&canonicalized_recovery_pk.as_bytes()); + let new_update_commitment = + Self::commitment_scheme(&new_update_pk).context("Generate new update commitment")?; + let new_recovery_commitment = + Self::commitment_scheme(&new_recovery_pk).context("Generate new update commitment")?; + + let recover_operation_delta_object = Delta { + patches, + update_commitment: new_update_commitment, + }; + + let delta_string = Self::json_canonicalization_scheme(&recover_operation_delta_object) + .context("Canonicalize Recover Operation Delta Object")?; + let delta_hash = Self::hash(delta_string.as_bytes()); + + let algorithm = Self::SIGNATURE_ALGORITHM; + let claims = RecoveryClaims { + recovery_commitment: new_recovery_commitment, + recovery_key: recovery_pk, + delta_hash, + anchor_origin: None, + }; + let signed_data = ssi::jwt::encode_sign(algorithm, &claims, recovery_key) + .context("Sign Recover Operation")?; + let recover_op = RecoverOperation { + did_suffix, + reveal_value: recover_reveal_value, + delta: recover_operation_delta_object, + signed_data, + }; + Ok(Operation::Recover(recover_op)) + } + + /// Recover a Sidetree-based DID + /// + /// Generate keypairs and construct a Recover Operation according to [Sidetree §11.3 + /// Recover][recover]. Returns the recover operation. + /// + /// [recover]: https://identity.foundation/sidetree/spec/v1.0.0/#recover + fn recover( + did_suffix: DIDSuffix, + recovery_key: &JWK, + patches: Vec, + ) -> Result<(Operation, JWK, JWK)> { + let new_update_keypair = Self::generate_key().context("Generate New Update Key Pair")?; + let new_update_pk = PublicKeyJwk::try_from(new_update_keypair.to_public()) + .context("Convert new update public key")?; + + let new_recovery_keypair = + Self::generate_key().context("Generate New Recovery Key Pair")?; + let new_recovery_pk = PublicKeyJwk::try_from(new_recovery_keypair.to_public()) + .context("Convert new recovery public key")?; + + let recover_op = Self::recover_existing( + did_suffix, + recovery_key, + &new_update_pk, + &new_recovery_pk, + patches, + ) + .context("Construct Recover Operation")?; + Ok((recover_op, new_update_keypair, new_recovery_keypair)) + } + + /// Deactivate a Sidetree-based DID + /// + /// Construct a Deactivate Operation according to [Sidetree §11.4 + /// Deactivate][deactivate]. Returns the deactivate operation. + /// + /// [deactivate]: https://identity.foundation/sidetree/spec/v1.0.0/#deactivate + fn deactivate(did_suffix: DIDSuffix, recovery_key: JWK) -> Result { + let recovery_pk = PublicKeyJwk::try_from(recovery_key.to_public()) + .context("Convert recovery key to PublicKeyJwk for Deactivate operation")?; + let canonicalized_recovery_pk = Self::json_canonicalization_scheme(&recovery_pk).context( + "Canonicalize recovery public key for reveal value for Deactivate operation", + )?; + let recover_reveal_value = Self::reveal_value(&canonicalized_recovery_pk.as_bytes()); + let algorithm = Self::SIGNATURE_ALGORITHM; + let claims = DeactivateClaims { + did_suffix: did_suffix.clone(), + recovery_key: recovery_pk, + }; + let signed_data = ssi::jwt::encode_sign(algorithm, &claims, &recovery_key) + .context("Sign Deactivate Operation")?; + let recover_op = DeactivateOperation { + did_suffix, + reveal_value: recover_reveal_value, + signed_data, + }; + Ok(recover_op) + } + + /// Serialize and hash [Suffix Data][SuffixData], to generate a [Short-Form Sidetree + /// DID][SidetreeDID::Short] ([`DIDSuffix`]). + /// + /// Reference: + fn serialize_suffix_data(suffix_data: &SuffixData) -> Result { + let string = + Self::json_canonicalization_scheme(suffix_data).context("Canonicalize Suffix Data")?; + let hash = Self::hash(string.as_bytes()); + Ok(DIDSuffix(hash)) + } + + /// Check that a DID Suffix looks valid + fn validate_did_suffix(suffix: &DIDSuffix) -> Result<()> { + let bytes = + base64::decode_config(&suffix.0, base64::URL_SAFE_NO_PAD).context("Decode Base64")?; + ensure!( + bytes.len() == 34, + "Unexpected length for Sidetree DID Suffix: {}", + bytes.len() + ); + ensure!( + &bytes[0..1] == MULTIHASH_SHA2_256_PREFIX && &bytes[1..2] == MULTIHASH_SHA2_256_SIZE, + "Expected SHA2-256 prefix for Sidetree DID Suffix" + ); + Ok(()) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "type")] +#[serde(rename_all = "camelCase")] +pub enum Operation { + Create(CreateOperation), + Update(UpdateOperation), + Recover(RecoverOperation), + Deactivate(DeactivateOperation), +} + +#[derive(Debug, Clone)] +pub struct PartiallyVerifiedCreateOperation { + did_suffix: DIDSuffix, + r#type: Option, + recovery_commitment: String, + anchor_origin: Option, + hashed_delta: Delta, +} + +#[derive(Debug, Clone)] +pub struct PartiallyVerifiedUpdateOperation { + reveal_value: String, + signed_delta: Delta, + signed_update_key: PublicKeyJwk, +} + +#[derive(Debug, Clone)] +pub struct PartiallyVerifiedRecoverOperation { + reveal_value: String, + signed_delta: Delta, + signed_recovery_commitment: String, + signed_recovery_key: PublicKeyJwk, + signed_anchor_origin: Option, +} + +#[derive(Debug, Clone)] +pub struct PartiallyVerifiedDeactivateOperation { + signed_did_suffix: DIDSuffix, + reveal_value: String, + signed_recovery_key: PublicKeyJwk, +} + +#[derive(Debug, Clone)] +pub enum PartiallyVerifiedOperation { + Create(PartiallyVerifiedCreateOperation), + Update(PartiallyVerifiedUpdateOperation), + Recover(PartiallyVerifiedRecoverOperation), + Deactivate(PartiallyVerifiedDeactivateOperation), +} + +trait SidetreeOperation { + type PartiallyVerifiedForm; + fn partial_verify(self) -> Result; +} + +impl SidetreeOperation for Operation { + type PartiallyVerifiedForm = PartiallyVerifiedOperation; + + fn partial_verify(self) -> Result { + Ok(match self { + Operation::Create(op) => PartiallyVerifiedOperation::Create( + op.partial_verify::() + .context("Partial verify Create operation")?, + ), + Operation::Update(op) => PartiallyVerifiedOperation::Update( + op.partial_verify::() + .context("Partial verify Update operation")?, + ), + Operation::Recover(op) => PartiallyVerifiedOperation::Recover( + op.partial_verify::() + .context("Partial verify Recover operation")?, + ), + Operation::Deactivate(op) => PartiallyVerifiedOperation::Deactivate( + op.partial_verify::() + .context("Partial verify Deactivate operation")?, + ), + }) + } +} + +fn ensure_reveal_commitment( + recovery_commitment: &str, + reveal_value: &str, + pk: &PublicKeyJwk, +) -> Result<()> { + let canonicalized_public_key = + S::json_canonicalization_scheme(&pk).context("Canonicalize JWK")?; + let commitment_value = canonicalized_public_key.as_bytes(); + let computed_reveal_value = S::reveal_value(&commitment_value); + ensure!(&computed_reveal_value == reveal_value); + let computed_commitment = + S::commitment_scheme(&pk).context("Unable to compute public key commitment")?; + ensure!(&computed_commitment == recovery_commitment); + Ok(()) +} + +impl PartiallyVerifiedOperation { + pub fn update_commitment(&self) -> Option<&str> { + match self { + PartiallyVerifiedOperation::Create(create) => { + Some(&create.hashed_delta.update_commitment) + } + PartiallyVerifiedOperation::Update(update) => { + Some(&update.signed_delta.update_commitment) + } + PartiallyVerifiedOperation::Recover(recover) => { + Some(&recover.signed_delta.update_commitment) + } + PartiallyVerifiedOperation::Deactivate(_) => None, + } + } + + pub fn recovery_commitment(&self) -> Option<&str> { + match self { + PartiallyVerifiedOperation::Create(create) => Some(&create.recovery_commitment), + PartiallyVerifiedOperation::Update(_) => None, + PartiallyVerifiedOperation::Recover(recover) => { + Some(&recover.signed_recovery_commitment) + } + PartiallyVerifiedOperation::Deactivate(_) => None, + } + } + + pub fn follows(&self, previous: &PartiallyVerifiedOperation) -> Result<()> { + match self { + PartiallyVerifiedOperation::Create(_) => { + bail!("Create operation cannot follow another operation"); + } + PartiallyVerifiedOperation::Update(update) => { + let update_commitment = previous + .update_commitment() + .ok_or(anyhow!("No update commitment"))?; + ensure_reveal_commitment::( + &update_commitment, + &update.reveal_value, + &update.signed_update_key, + )?; + } + PartiallyVerifiedOperation::Recover(recover) => { + let recovery_commitment = previous + .recovery_commitment() + .ok_or(anyhow!("No recovery commitment"))?; + ensure_reveal_commitment::( + &recovery_commitment, + &recover.reveal_value, + &recover.signed_recovery_key, + )?; + } + PartiallyVerifiedOperation::Deactivate(deactivate) => { + if let PartiallyVerifiedOperation::Create(create) = previous { + ensure!( + deactivate.signed_did_suffix == create.did_suffix, + "DID Suffix mismatch" + ); + } else { + // Note: Recover operations do not sign over the DID suffix. If the deactivate + // operation follows a recover operation rather than a create operation, the + // DID Suffix must be verified by the caller. + } + let recovery_commitment = previous + .recovery_commitment() + .ok_or(anyhow!("No recovery commitment"))?; + ensure_reveal_commitment::( + &recovery_commitment, + &deactivate.reveal_value, + &deactivate.signed_recovery_key, + )?; + } + } + Ok(()) + } +} + +impl SidetreeOperation for CreateOperation { + type PartiallyVerifiedForm = PartiallyVerifiedCreateOperation; + + fn partial_verify(self) -> Result { + let did = SidetreeDID::::from_create_operation(&self) + .context("Unable to derive DID from create operation")?; + let did_suffix = DIDSuffix::from(did); + let delta_string = S::json_canonicalization_scheme(&self.delta) + .context("Unable to Canonicalize Update Operation Delta Object")?; + let delta_hash = S::hash(delta_string.as_bytes()); + ensure!( + delta_hash == self.suffix_data.delta_hash, + "Delta hash mismatch" + ); + Ok(PartiallyVerifiedCreateOperation { + did_suffix, + r#type: self.suffix_data.r#type, + recovery_commitment: self.suffix_data.recovery_commitment, + anchor_origin: self.suffix_data.anchor_origin, + hashed_delta: self.delta, + }) + } +} + +impl SidetreeOperation for UpdateOperation { + type PartiallyVerifiedForm = PartiallyVerifiedUpdateOperation; + + /// Partially verify an [UpdateOperation] + /// + /// Specifically, the following is done: + /// - The operation's [signed data](UpdateOperation::signed_data) is verified against the + /// revealed [public key](UpdateClaims::update_key) that it must contain; + /// - the revealed public key is verified against the operation's + /// [reveal value](UpdateOperation::reveal_value); and + /// - the operation's [delta object](UpdateOperation::delta) is verified against the + /// [delta hash](UpdateClaims::update_key) in the signed data payload. + /// + /// The [DID Suffix](UpdateOperation::did_suffix), and the delta values, are **not** verified + /// by this function. The correspondence of the reveal value's hash to the previous update + /// commitment is not checked either, since that is not known from this function. + + fn partial_verify(self) -> Result { + // Verify JWS against public key in payload. + // Then check public key against its hash (reveal value). + let (header, claims) = + jws_decode_verify_inner(&self.signed_data, |claims: &UpdateClaims| { + &claims.update_key + }) + .context("Verify Signed Update Data")?; + ensure!( + header.algorithm == S::SIGNATURE_ALGORITHM, + "Update Operation must use Sidetree's signature algorithm" + ); + let canonicalized_public_key = S::json_canonicalization_scheme(&claims.update_key) + .context("Canonicalize Update Key")?; + let computed_reveal_value = S::reveal_value(canonicalized_public_key.as_bytes()); + ensure!( + self.reveal_value == computed_reveal_value, + "Reveal value must match hash of update key. Computed: {}. Found: {}", + computed_reveal_value, + self.reveal_value, + ); + let delta_string = S::json_canonicalization_scheme(&self.delta) + .context("Canonicalize Update Operation Delta Object")?; + let delta_hash = S::hash(delta_string.as_bytes()); + ensure!(claims.delta_hash == delta_hash, "Delta hash mismatch"); + // Note: did_suffix is dropped, since it's not signed over. + Ok(PartiallyVerifiedUpdateOperation { + reveal_value: self.reveal_value, + signed_delta: self.delta, + signed_update_key: claims.update_key, + }) + } +} + +impl SidetreeOperation for RecoverOperation { + type PartiallyVerifiedForm = PartiallyVerifiedRecoverOperation; + + /// Partially verify a [RecoverOperation] + fn partial_verify(self) -> Result { + // Verify JWS against public key in payload. + // Then check public key against its hash (reveal value). + let (header, claims) = + jws_decode_verify_inner(&self.signed_data, |claims: &RecoveryClaims| { + &claims.recovery_key + }) + .context("Verify Signed Recover Data")?; + ensure!( + header.algorithm == S::SIGNATURE_ALGORITHM, + "Recover Operation must use Sidetree's signature algorithm" + ); + let canonicalized_public_key = S::json_canonicalization_scheme(&claims.recovery_key) + .context("Canonicalize Recover Key")?; + let computed_reveal_value = S::reveal_value(canonicalized_public_key.as_bytes()); + ensure!( + self.reveal_value == computed_reveal_value, + "Reveal value must match hash of recovery key. Computed: {}. Found: {}", + computed_reveal_value, + self.reveal_value, + ); + let delta_string = S::json_canonicalization_scheme(&self.delta) + .context("Canonicalize Recover Operation Delta Object")?; + let delta_hash = S::hash(delta_string.as_bytes()); + ensure!(claims.delta_hash == delta_hash, "Delta hash mismatch"); + // Note: did_suffix is dropped, since it's not signed over. + Ok(PartiallyVerifiedRecoverOperation { + reveal_value: self.reveal_value, + signed_delta: self.delta, + signed_recovery_commitment: claims.recovery_commitment, + signed_recovery_key: claims.recovery_key, + signed_anchor_origin: claims.anchor_origin, + }) + } +} + +impl SidetreeOperation for DeactivateOperation { + type PartiallyVerifiedForm = PartiallyVerifiedDeactivateOperation; + + /// Partially verify a [DeactivateOperation] + fn partial_verify(self) -> Result { + // Verify JWS against public key in payload. + // Then check public key against its hash (reveal value). + + let (header, claims) = + jws_decode_verify_inner(&self.signed_data, |claims: &DeactivateClaims| { + &claims.recovery_key + }) + .context("Verify Signed Deactivation Data")?; + ensure!( + header.algorithm == S::SIGNATURE_ALGORITHM, + "Deactivate Operation must use Sidetree's signature algorithm" + ); + let canonicalized_public_key = S::json_canonicalization_scheme(&claims.recovery_key) + .context("Canonicalize Recovery Key")?; + let computed_reveal_value = S::reveal_value(canonicalized_public_key.as_bytes()); + ensure!( + self.reveal_value == computed_reveal_value, + "Reveal value must match hash of recovery key. Computed: {}. Found: {}", + computed_reveal_value, + self.reveal_value, + ); + ensure!(self.did_suffix == claims.did_suffix, "DID Suffix mismatch"); + Ok(PartiallyVerifiedDeactivateOperation { + signed_did_suffix: claims.did_suffix, + reveal_value: self.reveal_value, + signed_recovery_key: claims.recovery_key, + }) + } +} + +/// [DID Suffix](https://identity.foundation/sidetree/spec/v1.0.0/#did-suffix) +/// +/// Unique identifier string within a Sidetree DID (short or long-form) +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct DIDSuffix(pub String); + +/// A Sidetree-based DID +/// +/// Reference: [Sidetree §9. DID URI Composition][duc] +/// +/// [duc]: https://identity.foundation/sidetree/spec/v1.0.0/#did-uri-composition +pub enum SidetreeDID { + /// Short-form Sidetree DID + /// + /// Reference: [§9. DID URI Composition](https://identity.foundation/sidetree/spec/v1.0.0/#short-form-did) + Short { did_suffix: DIDSuffix }, + + /// Long-form Sidetree DID + /// + /// Reference: [§9.1 Long-Form DID URIs](https://identity.foundation/sidetree/spec/v1.0.0/#long-form-did-uris) + Long { + did_suffix: DIDSuffix, + create_operation_data: String, + _marker: PhantomData, + }, +} + +/// [Create Operation Suffix Data Object][data] +/// +/// [data]: https://identity.foundation/sidetree/spec/v1.0.0/#create-suffix-data-object +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SuffixData { + /// Implementation-defined type property + #[serde(skip_serializing_if = "Option::is_none")] + pub r#type: Option, + + /// Delta Hash + /// + /// [Hash](Sidetree::hash) of canonicalized [Create Operation Delta Object](Delta). + pub delta_hash: String, + + /// [Recovery commitment](https://identity.foundation/sidetree/spec/v1.0.0/#recovery-commitment) + /// + /// Generated in step 2 of the [Create](https://identity.foundation/sidetree/spec/v1.0.0/#create) process. + pub recovery_commitment: String, + + /// Anchor Origin + /// + /// Implementation-defined identifier for most recent anchor for the DID + #[serde(skip_serializing_if = "Option::is_none")] + pub anchor_origin: Option, + // TODO: extensible by method +} + +/// Public key as JWK or Multibase +/// +/// Property of a public key / verification method containing public key data, +/// as part of a [PublicKeyEntry][]. +/// +/// per [Sidetree §12.1.1 `add-public-keys`: Step 4][apk]. +/// +/// [apk]: https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub enum PublicKey { + /// [`publicKeyJwk`](https://www.w3.org/TR/did-core/#dfn-publickeyjwk) as defined in DID Core. + /// + /// JSON Web Key (JWK) is specified in [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517). + PublicKeyJwk(PublicKeyJwk), + + /// [`publicKeyMultibase`](https://www.w3.org/TR/did-core/#dfn-publickeymultibase) as defined in DID Core. + /// + /// Maximum length may be set in [Sidetree::MAX_PKMB_LENGTH]. + PublicKeyMultibase(String), +} + +/// Public Key Entry +/// +/// Used by the [`add-public-keys`](DIDStatePatch::AddPublicKeys) and +/// [`replace`](DIDStatePatch::Replace) DID state patch actions. +/// +/// Specified in [Sidetree §12.1.1 `add-public-keys`][apk]. +/// +/// [apk]: https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PublicKeyEntry { + /// `id` property + /// + /// Maximum length: 50 in Base64url + pub id: String, + + /// Verification method type + pub r#type: String, + + /// Verification method controller (DID) + /// + /// Maximum length may be set in [Sidetree::MAX_CONTROLLER_LENGTH]. + #[serde(skip_serializing_if = "Option::is_none")] + pub controller: Option, + + /// `publicKeyJwk` or `publicKeyMultibase` property + #[serde(flatten)] + pub public_key: PublicKey, + + /// Verification relationships + /// + /// Defined in [DID Core](https://www.w3.org/TR/did-core/#verification-relationships). + /// + /// Corresponds to [`proofPurpose`](https://www.w3.org/TR/did-core/#verification-relationships) in VC Data Model. + pub purposes: Vec, +} + +impl TryFrom for PublicKeyEntry { + type Error = Error; + fn try_from(jwk: JWK) -> Result { + let id = jwk.thumbprint().context("Compute JWK thumbprint")?; + let pkjwk = PublicKeyJwk::try_from(jwk.to_public()).context("Convert key")?; + let public_key = PublicKey::PublicKeyJwk(pkjwk); + Ok(PublicKeyEntry { + id, + r#type: VERIFICATION_METHOD_TYPE.to_owned(), + controller: None, + public_key, + purposes: vec![ + VerificationRelationship::AssertionMethod, + VerificationRelationship::Authentication, + VerificationRelationship::KeyAgreement, + VerificationRelationship::CapabilityInvocation, + VerificationRelationship::CapabilityDelegation, + ], + }) + } +} + +/// Service Endpoint Entry +/// +/// Used by the [`add-services`](DIDStatePatch::AddServices) and +/// [`replace`](DIDStatePatch::Replace) DID state patch actions. +/// +/// Specified in [Sidetree §12.1.3 `add-services`][as]. +/// +/// [as]: https://identity.foundation/sidetree/spec/v1.0.0/#add-services +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ServiceEndpointEntry { + /// `id` property + /// + /// Maximum length: 50 in Base64Url + pub id: String, + + /// Service type + /// + /// Maximum length: 30 in Base64Url + pub r#type: String, + + /// Service endpoint URL or object + pub service_endpoint: ServiceEndpoint, +} + +/// DID PKI metadata state +/// +/// Used by the [`replace`](DIDStatePatch::Replace) DID state patch. +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub struct DocumentState { + /// Public key entries + + #[serde(skip_serializing_if = "Option::is_none")] + pub public_keys: Option>, + + /// Services + #[serde(skip_serializing_if = "Option::is_none")] + pub services: Option>, +} + +/// [DID State Patch][dsp] using a [Sidetree Standard Patch action][spa] +/// +/// [dsp]: https://identity.foundation/sidetree/spec/v1.0.0/#did-state-patches +/// [spa]: https://identity.foundation/sidetree/spec/v1.0.0/#standard-patch-actions +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "action")] +#[serde(rename_all = "kebab-case")] +pub enum DIDStatePatch { + /// [`add-public-keys`][apk] Patch Action + /// + /// [apk]: https://identity.foundation/sidetree/spec/v1.0.0/#add-public-keys + AddPublicKeys { + /// Keys to add or over overwrite + #[serde(rename = "publicKeys")] + public_keys: Vec, + }, + + /// [`remove-public-keys`][rpk] Patch Action + /// + /// [rpk]: https://identity.foundation/sidetree/spec/v1.0.0/#remove-public-keys + RemovePublicKeys { + /// IDs of keys to remove + ids: Vec, + }, + + /// [`add-services`][as] Patch Action + /// + /// [as]: https://identity.foundation/sidetree/spec/v1.0.0/#add-services + AddServices { + /// Service entries to add + services: Vec, + }, + + /// [`remove-services`][rs] Patch Action + /// + /// [rs]: https://identity.foundation/sidetree/spec/v1.0.0/#remove-services + RemoveServices { + /// IDs of service endpoints to remove + ids: Vec, + }, + + /// [`replace`][r] Patch Action + /// + /// [r]: https://identity.foundation/sidetree/spec/v1.0.0/#replace + Replace { + /// Reset DID state + document: DocumentState, + }, + + /// [`ietf-json-patch`][ijp] Patch Action + /// + /// [ijp]: https://identity.foundation/sidetree/spec/v1.0.0/#ietf-json-patch + /// + IetfJsonPatch { + /// JSON Patches according to [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902). + patches: Patch, + }, +} + +/// Create/Update/Recover Delta Object +/// +/// ### References +/// - [Sidetree §11.1 Create - Create Operation Delta Object][codo] +/// - [Sidetree §11.2 Update - Update Operation Delta Object][uodo] +/// - [Sidetree §11.3 Recover - Recover Operation Delta Object][uodo] +/// +/// [codo]: https://identity.foundation/sidetree/spec/v1.0.0/#create-delta-object +/// [uodo]: https://identity.foundation/sidetree/spec/v1.0.0/#update-delta-object +/// [rodo] https://identity.foundation/sidetree/spec/v1.0.0/#recover-delta-object +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Delta { + /// DID state patches to apply. + pub patches: Vec, + + /// Update commitment generated as part of a Sidetree Create or Update operation. + pub update_commitment: String, +} + +/// Sidetree DID Create operation +/// +/// ### References +/// - [Sidetree §11.1 Create](https://identity.foundation/sidetree/spec/v1.0.0/#create) +/// - [Sidetree REST API §1.2.1 Create](https://identity.foundation/sidetree/api/#create) +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct CreateOperation { + pub suffix_data: SuffixData, + pub delta: Delta, +} + +/// Sidetree DID Update operation +/// +/// ### References +/// - [Sidetree §11.2 Update](https://identity.foundation/sidetree/spec/v1.0.0/#update) +/// - [Sidetree REST API §1.2.2 Update](https://identity.foundation/sidetree/api/#update) +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct UpdateOperation { + pub did_suffix: DIDSuffix, + /// Output of [Sidetree::reveal_value] + pub reveal_value: String, + pub delta: Delta, + /// Compact JWS (RFC 7515) of [UpdateClaims] + /// + /// + pub signed_data: String, +} + +/// Sidetree DID Recover operation +/// +/// ### References +/// - [Sidetree §11.3 Recover](https://identity.foundation/sidetree/spec/v1.0.0/#recover) +/// - [Sidetree REST API §1.2.3 Recover](https://identity.foundation/sidetree/api/#recover) +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct RecoverOperation { + pub did_suffix: DIDSuffix, + /// Output of [Sidetree::reveal_value] + pub reveal_value: String, + pub delta: Delta, + /// Compact JWS (RFC 7515) of [RecoveryClaims] + /// + /// + pub signed_data: String, +} + +/// Sidetree DID Deactivate operation +/// +/// ### References +/// - [Sidetree §11.4 Deactivate](https://identity.foundation/sidetree/spec/v1.0.0/#deactivate) +/// - [Sidetree REST API §1.2.4 Deactivate](https://identity.foundation/sidetree/api/#deactivate) +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct DeactivateOperation { + pub did_suffix: DIDSuffix, + /// Output of [Sidetree::reveal_value] + pub reveal_value: String, + /// Compact JWS (RFC 7515) of [DeactivateClaims] + /// + /// + pub signed_data: String, +} + +/// Payload object for JWS in [UpdateOperation] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct UpdateClaims { + /// Key matching previous Update Commitment + pub update_key: PublicKeyJwk, + + /// [Hash](Sidetree::hash) of canonicalized [Update Operation Delta Object](Delta). + pub delta_hash: String, +} + +/// Payload object for JWS in [RecoverOperation] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RecoveryClaims { + /// [Recovery commitment](https://identity.foundation/sidetree/spec/v1.0.0/#recovery-commitment) + /// + /// Generated in step 9 of the [Recover](https://identity.foundation/sidetree/spec/v1.0.0/#recover) process. + pub recovery_commitment: String, + + /// Key matching previous Recovery Commitment + pub recovery_key: PublicKeyJwk, + + /// [Hash](Sidetree::hash) of canonicalized [Update Operation Delta Object](Delta). + pub delta_hash: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub anchor_origin: Option, +} + +/// Payload object for JWS in [DeactivateOperation] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct DeactivateClaims { + pub did_suffix: DIDSuffix, + /// Key matching previous Recovery Commitment + pub recovery_key: PublicKeyJwk, +} + +/// Public Key JWK (JSON Web Key) +/// +/// Wraps [ssi::jwk::JWK], while allowing a `nonce` property, and disallowing private key +/// properties ("d"). +/// +/// Sidetree may allow a `nonce` property in public key JWKs ([§6.2.2 JWK Nonce][jwkn]). +/// +/// [jwkn]: https://identity.foundation/sidetree/spec/#jwk-nonce +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PublicKeyJwk { + #[serde(skip_serializing_if = "Option::is_none")] + pub nonce: Option, + #[serde(flatten)] + jwk: Value, +} + +impl TryFrom for PublicKeyJwk { + type Error = Error; + fn try_from(jwk: JWK) -> Result { + let jwk_value = serde_json::to_value(jwk).context("Convert JWK to Value")?; + ensure!( + jwk_value.get("d").is_none(), + "Public Key JWK must not contain private key parameters" + ); + Ok(Self { + jwk: jwk_value, + nonce: None, + }) + } +} + +/// Convert [PublicKeyJwk] to [JWK]. +/// +/// Note: `nonce` property is dropped. +impl TryFrom for JWK { + type Error = Error; + fn try_from(pkjwk: PublicKeyJwk) -> Result { + let jwk = serde_json::from_value(pkjwk.jwk).context("Convert Value to JWK")?; + Ok(jwk) + } +} + +impl FromStr for SidetreeDID { + type Err = Error; + fn from_str(did: &str) -> Result { + let mut parts = did.split(':'); + ensure!(parts.next() == Some("did"), "Expected DID URI scheme"); + ensure!(parts.next() == Some(S::METHOD), "DID Method mismatch"); + if let Some(network) = S::NETWORK { + ensure!(parts.next() == Some(network), "Sidetree network mismatch"); + } + let did_suffix_str = parts.next().ok_or(anyhow!("Missing Sidetree DID Suffix"))?; + let did_suffix = DIDSuffix(did_suffix_str.to_string()); + S::validate_did_suffix(&did_suffix).context("Validate Sidetree DID Suffix")?; + let create_operation_data_opt = parts.next(); + ensure!( + parts.next().is_none(), + "Unexpected data after Sidetree Long-Form DID" + ); + Ok(match create_operation_data_opt { + None => Self::Short { did_suffix }, + Some(data) => Self::Long { + did_suffix, + create_operation_data: data.to_string(), + _marker: PhantomData, + }, + }) + } +} + +impl fmt::Display for SidetreeDID { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "did:{}:", S::METHOD)?; + if let Some(network) = S::NETWORK { + write!(f, "{}:", network)?; + } + match self { + Self::Short { did_suffix } => f.write_str(&did_suffix.0), + Self::Long { + did_suffix, + create_operation_data, + _marker, + } => write!(f, "{}:{}", did_suffix.0, create_operation_data), + } + } +} + +impl SidetreeDID { + /// Construct a [Long-Form Sidetree DID][lfdu] from a [Create Operation][CreateOperation] + /// + /// [lfdu]: https://identity.foundation/sidetree/spec/v1.0.0/#long-form-did-uris + pub fn from_create_operation(create_operation: &CreateOperation) -> Result { + let op_json = S::json_canonicalization_scheme(&create_operation) + .context("Canonicalize Create Operation")?; + let op_string = S::data_encoding_scheme(op_json.as_bytes()); + + let did_suffix = S::serialize_suffix_data(&create_operation.suffix_data) + .context("Serialize DID Suffix Data")?; + Ok(Self::Long { + did_suffix, + create_operation_data: op_string, + _marker: PhantomData, + }) + } +} + +/// Convert a DID URL to an object id given a DID +/// +/// Object id is an id of a [ServiceEndpointEntry] or [PublicKeyEntry]. +fn did_url_to_id(did_url: &str, did: &SidetreeDID) -> Result { + let did_string = did.to_string(); + let unprefixed = match did_url.strip_prefix(&did_string) { + Some(s) => s, + None => bail!("DID URL did not begin with expected DID"), + }; + let fragment = match unprefixed.strip_prefix('#') { + Some(s) => s, + None => bail!("Expected DID URL with fragment"), + }; + Ok(fragment.to_string()) +} + +impl From> for DIDSuffix { + fn from(did: SidetreeDID) -> DIDSuffix { + match did { + SidetreeDID::Short { did_suffix } => did_suffix, + SidetreeDID::Long { did_suffix, .. } => did_suffix, + } + } +} + +/// DID Resolver using ION/Sidetree REST API +#[derive(Debug, Clone, Default)] +pub struct HTTPSidetreeDIDResolver { + pub http_did_resolver: HTTPDIDResolver, + pub _marker: PhantomData, +} + +impl HTTPSidetreeDIDResolver { + pub fn new(sidetree_api_url: &str) -> Self { + let identifiers_url = format!("{}identifiers/", sidetree_api_url); + Self { + http_did_resolver: HTTPDIDResolver::new(&identifiers_url), + _marker: PhantomData, + } + } +} + +/// Sidetree DID Method client implementation +pub struct SidetreeClient { + pub resolver: Option>, + pub endpoint: Option, +} + +impl SidetreeClient { + pub fn new(api_url_opt: Option) -> Self { + let resolver_opt = api_url_opt + .as_ref() + .map(|url| HTTPSidetreeDIDResolver::new(&url)); + Self { + endpoint: api_url_opt, + resolver: resolver_opt, + } + } +} + +/// Check that a JWK is Secp256k1 +pub fn is_secp256k1(jwk: &JWK) -> bool { + matches!(jwk, JWK {params: ssi::jwk::Params::EC(ssi::jwk::ECParams { curve: Some(curve), ..}), ..} if curve == "secp256k1") +} + +struct NoOpResolver; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl DIDResolver for NoOpResolver { + async fn resolve( + &self, + _did: &str, + _input_metadata: &ResolutionInputMetadata, + ) -> ( + ResolutionMetadata, + Option, + Option, + ) { + ( + ResolutionMetadata::from_error("Missing Sidetree API endpoint"), + None, + None, + ) + } +} + +fn new_did_state( + update_key: Option, + recovery_key: Option, + verification_key: Option, +) -> Result<(PublicKeyJwk, PublicKeyJwk, Vec)> { + let update_key = update_key.ok_or(anyhow!("Missing required update key"))?; + S::validate_key(&update_key).context("Validate update key")?; + let update_pk = PublicKeyJwk::try_from(update_key.to_public()).context("Convert update key")?; + let recovery_key = recovery_key.ok_or(anyhow!("Missing required recovery key"))?; + S::validate_key(&recovery_key).context("Validate recovery key")?; + let recovery_pk = + PublicKeyJwk::try_from(recovery_key.to_public()).context("Convert recovery key")?; + let mut patches = vec![]; + if let Some(verification_key) = verification_key { + let public_key_entry = PublicKeyEntry::try_from(verification_key) + .context("Convert JWK to public key entry")?; + let document = DocumentState { + public_keys: Some(vec![public_key_entry]), + services: None, + }; + let patch = DIDStatePatch::Replace { document }; + patches.push(patch); + }; + Ok((update_pk, recovery_pk, patches)) +} + +fn b64len(s: &str) -> usize { + base64::encode_config(s, base64::URL_SAFE_NO_PAD).len() +} + +impl DIDStatePatch { + /// Convert a [DID Document Operation][ddo] and DID to a Sidetree [DID State Patch][dsp]. + /// + /// [ddp]: https://identity.foundation/did-registration/#diddocumentoperation + /// [dsp]: https://identity.foundation/sidetree/spec/v1.0.0/#did-state-patches + fn try_from_with_did( + did_doc_op: DIDDocumentOperation, + did: &SidetreeDID, + ) -> Result { + Ok(match did_doc_op { + DIDDocumentOperation::SetDidDocument(_doc) => { + bail!("setDidDocument not implemented") + } + DIDDocumentOperation::AddToDidDocument(_props) => { + bail!("addToDidDocument not implemented") + } + DIDDocumentOperation::RemoveFromDidDocument(_props) => { + bail!("removeFromDidDocument not implemented") + } + DIDDocumentOperation::SetVerificationMethod { vmm, purposes } => { + let sub_id = + did_url_to_id(&vmm.id, did).context("Convert verification method id")?; + let mut value = + serde_json::to_value(vmm).context("Convert verification method map")?; + value["id"] = Value::String(sub_id); + value["purposes"] = serde_json::to_value(purposes) + .context("Convert verification method purposes")?; + let entry: PublicKeyEntry = serde_json::from_value(value) + .context("Convert verification method to Sidetree public key entry")?; + // TODO: allow omitted controller property + DIDStatePatch::AddPublicKeys { + public_keys: vec![entry], + } + } + DIDDocumentOperation::SetService(service) => { + let Service { + id, + type_, + service_endpoint, + property_set, + } = service; + ensure!( + !matches!(property_set, Some(map) if !map.is_empty()), + "Unexpected service properties" + ); + let service_endpoint = match service_endpoint { + None => bail!("Missing endpoint for service"), + Some(OneOrMany::Many(_)) => bail!("Sidetree service must contain one endpoint"), + Some(OneOrMany::One(se)) => se, + }; + let sub_id = did_url_to_id(&id, did).context("Convert service id")?; + let service_type = match type_ { + OneOrMany::One(type_) => type_, + OneOrMany::Many(_) => bail!("Service must contain single type"), + }; + ensure!(b64len(&service_type) <= 30, "Sidetree service type must contain no more than 30 Base64Url-encoded characters"); + ensure!( + b64len(&sub_id) <= 50, + "Sidetree service id must contain no more than 50 Base64Url-encoded characters" + ); + let entry = ServiceEndpointEntry { + id: sub_id, + r#type: service_type, + service_endpoint, + }; + DIDStatePatch::AddServices { + services: vec![entry], + } + } + DIDDocumentOperation::RemoveVerificationMethod(did_url) => { + let id = did_url.to_string(); + DIDStatePatch::RemovePublicKeys { ids: vec![id] } + } + DIDDocumentOperation::RemoveService(did_url) => { + let id = did_url.to_string(); + DIDStatePatch::RemoveServices { ids: vec![id] } + } + }) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct SidetreeError { + // List of error codes: https://github.com/decentralized-identity/sidetree/blob/v1.0.0/lib/core/versions/1.0/ErrorCode.ts + pub code: String, + pub message: Option, +} + +impl fmt::Display for SidetreeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Sidetree error {}", self.code)?; + if let Some(ref message) = self.message { + write!(f, ": {}", message)?; + } + Ok(()) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl DIDMethod for SidetreeClient { + fn name(&self) -> &'static str { + S::METHOD + } + + fn to_resolver(&self) -> &dyn DIDResolver { + match self.resolver { + Some(ref res) => res, + None => &NoOpResolver, + } + } + + fn create(&self, create: DIDCreate) -> Result { + let DIDCreate { + recovery_key, + update_key, + verification_key, + options, + } = create; + ensure!(is_empty(&options), "Create options not supported"); + let (update_pk, recovery_pk, patches) = + new_did_state::(update_key, recovery_key, verification_key) + .context("Prepare keys for DID creation")?; + let operation = S::create_existing(&update_pk, &recovery_pk, patches) + .context("Construct Create operation")?; + Self::op_to_transaction(operation).context("Construct create transaction") + } + + /// + async fn submit_transaction(&self, tx: DIDMethodTransaction) -> Result { + let op = Self::op_from_transaction(tx) + .context("Convert DID method transaction to Sidetree operation")?; + let endpoint = self + .endpoint + .as_ref() + .ok_or(anyhow!("Missing Sidetree REST API endpoint"))?; + let url = format!("{}operations/", endpoint); + let client = Client::builder().build().context("Build HTTP client")?; + let resp = client + .post(url) + .json(&op) + .header("Accept", "application/json") + .header("User-Agent", ssi::USER_AGENT) + .send() + .await + .context("Send HTTP request")?; + if let Err(e) = resp.error_for_status_ref() { + let err: SidetreeError = resp + .json() + .await + .context("Transaction submit failed. Unable to read HTTP response JSON")?; + bail!("Transaction submit failed: {}: {}", e, err); + } + if resp.content_length() == Some(0) { + // Update operation may return empty body with 200 OK. + return Ok(Value::Null); + } + let bytes = resp.bytes().await.context("Unable to read HTTP response")?; + let resp_json: Value = serde_json::from_slice(&bytes).context(format!( + "Unable to parse result as JSON: {}", + String::from_utf8(bytes.to_vec()).context("Unable to parse result as UTF-8")? + ))?; + Ok(resp_json) + } + + fn did_from_transaction(&self, tx: DIDMethodTransaction) -> Result { + let op = Self::op_from_transaction(tx) + .context("Convert DID method transaction to Sidetree operation")?; + let did = match op { + Operation::Create(create_op) => SidetreeDID::::from_create_operation(&create_op) + .context("Derive DID from Create operation")?, + Operation::Update(update_op) => SidetreeDID::Short { + did_suffix: update_op.did_suffix, + }, + Operation::Recover(recover_op) => SidetreeDID::Short { + did_suffix: recover_op.did_suffix, + }, + Operation::Deactivate(deactivate_op) => SidetreeDID::Short { + did_suffix: deactivate_op.did_suffix, + }, + }; + Ok(did.to_string()) + } + + fn update(&self, update: DIDUpdate) -> Result { + let DIDUpdate { + did, + update_key, + new_update_key, + operation, + options, + } = update; + let did = SidetreeDID::::from_str(&did).context("Parse Sidetree DID")?; + ensure!(is_empty(&options), "Update options not supported"); + let update_key = update_key.ok_or(anyhow!("Missing required new update key"))?; + let new_update_key = new_update_key.ok_or(anyhow!("Missing required new update key"))?; + S::validate_key(&new_update_key).context("Validate update key")?; + let new_update_pk = + PublicKeyJwk::try_from(new_update_key.to_public()).context("Convert new update key")?; + let patches = vec![DIDStatePatch::try_from_with_did(operation, &did) + .context("Convert DID document operation to Sidetree patch actions")?]; + let did_suffix = DIDSuffix::from(did); + let update_operation = S::update(did_suffix, &update_key, &new_update_pk, patches) + .context("Construct Update operation")?; + Self::op_to_transaction(Operation::Update(update_operation)) + .context("Construct update transaction") + } + + fn recover(&self, recover: DIDRecover) -> Result { + let DIDRecover { + did, + recovery_key, + new_recovery_key, + new_update_key, + new_verification_key, + options, + } = recover; + let did = SidetreeDID::::from_str(&did).context("Parse Sidetree DID")?; + let did_suffix = DIDSuffix::from(did); + ensure!(is_empty(&options), "Recover options not supported"); + let recovery_key = recovery_key.ok_or(anyhow!("Missing required recovery key"))?; + let (new_update_pk, new_recovery_pk, patches) = + new_did_state::(new_update_key, new_recovery_key, new_verification_key) + .context("Prepare keys for DID recovery")?; + let operation = S::recover_existing( + did_suffix, + &recovery_key, + &new_update_pk, + &new_recovery_pk, + patches, + ) + .context("Construct Recover operation")?; + Self::op_to_transaction(operation).context("Construct recover transaction") + } + + fn deactivate(&self, deactivate: DIDDeactivate) -> Result { + let DIDDeactivate { did, key, options } = deactivate; + let did = SidetreeDID::::from_str(&did).context("Parse Sidetree DID")?; + let recovery_key = key.ok_or(anyhow!( + "Missing required recovery key for DID deactivation" + ))?; + ensure!(is_empty(&options), "Deactivate options not supported"); + let did_suffix = DIDSuffix::from(did); + let deactivate_operation = ::deactivate(did_suffix, recovery_key) + .context("Construct DID Deactivate operation")?; + Self::op_to_transaction(Operation::Deactivate(deactivate_operation)) + .context("Construct DID deactivate transaction") + } +} + +impl SidetreeClient { + fn op_to_transaction(op: Operation) -> Result { + let value = serde_json::to_value(op).context("Convert operation to value")?; + Ok(DIDMethodTransaction { + did_method: S::METHOD.to_string(), + value: serde_json::json!({ "sidetreeOperation": value }), + }) + } + + fn op_from_transaction(tx: DIDMethodTransaction) -> Result { + let mut value = tx.value; + let op_value = value + .get_mut("sidetreeOperation") + .ok_or(anyhow!("Missing sidetreeOperation property"))? + .take(); + let op: Operation = + serde_json::from_value(op_value).context("Convert value to operation")?; + Ok(op) + } +} + +fn is_empty(options: &Value) -> bool { + options.is_null() + || match options.as_object() { + Some(obj) => obj.is_empty(), + None => false, + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl DIDResolver for HTTPSidetreeDIDResolver { + async fn resolve( + &self, + did: &str, + input_metadata: &ResolutionInputMetadata, + ) -> ( + ResolutionMetadata, + Option, + Option, + ) { + let _sidetree_did = match SidetreeDID::::from_str(did) { + Err(_e) => { + return ( + ResolutionMetadata::from_error(ERROR_INVALID_DID), + None, + None, + ); + } + Ok(did) => did, + }; + self.http_did_resolver.resolve(did, input_metadata).await + } +} + +/// Decode and verify JWS with public key inside payload +/// +/// Similar to [ssi::jwt::decode_verify] or [ssi::jws::decode_verify], but for when the payload (claims) must be parsed to +/// determine the public key. +/// +/// This function decodes and verifies a JWS/JWT, where the public key is expected to be found +/// within the payload (claims). Before verification, the deserialized claims object is passed to +/// the provided `get_key` function. The public key returned from the `get_key` function is then +/// used to verify the signature. The verified claims and header object are returned on successful +/// verification, along with the public key that they were verified against (as returned by the +/// `get_key` function). +/// +/// The `get_key` function uses [PublicKeyJwk], for the convenience of this crate, but this +/// function converts it to [ssi::jwk::JWK] internally. +pub fn jws_decode_verify_inner( + jwt: &str, + get_key: impl FnOnce(&Claims) -> &PublicKeyJwk, +) -> Result<(Header, Claims), Error> { + use ssi::jws::{decode_jws_parts, split_jws, verify_bytes, DecodedJWS}; + let (header_b64, payload_enc, signature_b64) = split_jws(jwt).context("Split JWS")?; + let DecodedJWS { + header, + signing_input, + payload, + signature, + } = decode_jws_parts(header_b64, payload_enc.as_bytes(), signature_b64) + .context("Decode JWS parts")?; + let claims: Claims = serde_json::from_slice(&payload).context("Deserialize JWS payload")?; + let pk = get_key(&claims); + let pk = JWK::try_from(pk.clone()).context("Convert PublicKeyJwk to JWK")?; + verify_bytes(header.algorithm, &signing_input, &pk, &signature) + .context("Verify Signed Deactivate Data")?; + Ok((header, claims)) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + struct Example; + + impl Sidetree for Example { + fn generate_key() -> Result { + JWK::generate_secp256k1().context("Generate secp256k1 key") + } + fn validate_key(key: &JWK) -> Result<(), Error> { + ensure!(is_secp256k1(&key), "Key must be Secp256k1"); + Ok(()) + } + const SIGNATURE_ALGORITHM: Algorithm = Algorithm::ES256K; + const METHOD: &'static str = "sidetree"; + } + + /// + static LONGFORM_DID: &str = "did:sidetree:EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljS2V5cyI6W3siaWQiOiJwdWJsaWNLZXlNb2RlbDFJZCIsInB1YmxpY0tleUp3ayI6eyJjcnYiOiJzZWNwMjU2azEiLCJrdHkiOiJFQyIsIngiOiJ0WFNLQl9ydWJYUzdzQ2pYcXVwVkpFelRjVzNNc2ptRXZxMVlwWG45NlpnIiwieSI6ImRPaWNYcWJqRnhvR0otSzAtR0oxa0hZSnFpY19EX09NdVV3a1E3T2w2bmsifSwicHVycG9zZXMiOlsiYXV0aGVudGljYXRpb24iLCJrZXlBZ3JlZW1lbnQiXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZXMiOlt7ImlkIjoic2VydmljZTFJZCIsInNlcnZpY2VFbmRwb2ludCI6Imh0dHA6Ly93d3cuc2VydmljZTEuY29tIiwidHlwZSI6InNlcnZpY2UxVHlwZSJ9XX19XSwidXBkYXRlQ29tbWl0bWVudCI6IkVpREtJa3dxTzY5SVBHM3BPbEhrZGI4Nm5ZdDBhTnhTSFp1MnItYmhFem5qZEEifSwic3VmZml4RGF0YSI6eyJkZWx0YUhhc2giOiJFaUNmRFdSbllsY0Q5RUdBM2RfNVoxQUh1LWlZcU1iSjluZmlxZHo1UzhWRGJnIiwicmVjb3ZlcnlDb21taXRtZW50IjoiRWlCZk9aZE10VTZPQnc4UGs4NzlRdFotMkotOUZiYmpTWnlvYUFfYnFENHpoQSJ9fQ"; + static SHORTFORM_DID: &str = "did:sidetree:EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg"; + + lazy_static::lazy_static! { + + /// + static ref CREATE_OPERATION: Operation = serde_json::from_value(json!({ + "type": "create", + "suffixData": { + "deltaHash": "EiCfDWRnYlcD9EGA3d_5Z1AHu-iYqMbJ9nfiqdz5S8VDbg", + "recoveryCommitment": "EiBfOZdMtU6OBw8Pk879QtZ-2J-9FbbjSZyoaA_bqD4zhA" + }, + "delta": { + "updateCommitment": "EiDKIkwqO69IPG3pOlHkdb86nYt0aNxSHZu2r-bhEznjdA", + "patches": [ + { + "action": "replace", + "document": { + "publicKeys": [ + { + "id": "publicKeyModel1Id", + "type": "EcdsaSecp256k1VerificationKey2019", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "tXSKB_rubXS7sCjXqupVJEzTcW3MsjmEvq1YpXn96Zg", + "y": "dOicXqbjFxoGJ-K0-GJ1kHYJqic_D_OMuUwkQ7Ol6nk" + }, + "purposes": [ + "authentication", + "keyAgreement" + ] + } + ], + "services": [ + { + "id": "service1Id", + "type": "service1Type", + "serviceEndpoint": "http://www.service1.com" + } + ] + } + } + ] + } + })).unwrap(); + + /// + static ref UPDATE_OPERATION: Operation = serde_json::from_value(json!({ + "type": "update", + "didSuffix": "EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg", + "revealValue": "EiBkRSeixqX-PhOij6PIpuGfPld5Nif5MxcrgtGCw-t6LA", + "delta": { + "patches": [ + { + "action": "add-public-keys", + "publicKeys": [ + { + "id": "additional-key", + "type": "EcdsaSecp256k1VerificationKey2019", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "aN75CTjy3VCgGAJDNJHbcb55hO8CobEKzgCNrUeOwAY", + "y": "K9FhCEpa_jG09pB6qriXrgSvKzXm6xtxBvZzIoXXWm4" + }, + "purposes": [ + "authentication", + "assertionMethod", + "capabilityInvocation", + "capabilityDelegation", + "keyAgreement" + ] + } + ] + } + ], + "updateCommitment": "EiDOrcmPtfMHuwIWN6YoihdeIPxOKDHy3D6sdMXu_7CN0w" + }, + "signedData": "eyJhbGciOiJFUzI1NksifQ.eyJ1cGRhdGVLZXkiOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4Ijoid2Z3UUNKM09ScVZkbkhYa1Q4UC1MZ19HdHhCRWhYM3R5OU5VbnduSHJtdyIsInkiOiJ1aWU4cUxfVnVBblJEZHVwaFp1eExPNnFUOWtQcDNLUkdFSVJsVHBXcmZVIn0sImRlbHRhSGFzaCI6IkVpQ3BqTjQ3ZjBNcTZ4RE5VS240aFNlZ01FcW9EU19ycFEyOVd5MVY3M1ZEYncifQ.RwZK1DG5zcr4EsrRImzStb0VX5j2ZqApXZnuoAkA3IoRdErUscNG8RuxNZ0FjlJtjMJ0a-kn-_MdtR0wwvWVgg" + })).unwrap(); + + /// + static ref RECOVER_OPERATION: Operation = serde_json::from_value(json!({ + "type": "recover", + "didSuffix": "EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg", + "revealValue": "EiAJ-97Is59is6FKAProwDo870nmwCeP8n5nRRFwPpUZVQ", + "signedData": "eyJhbGciOiJFUzI1NksifQ.eyJkZWx0YUhhc2giOiJFaUNTem1ZSk0yWGpaWE00a1Q0bGpKcEVGTjVmVkM1QVNWZ3hSekVtMEF2OWp3IiwicmVjb3ZlcnlLZXkiOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoibklxbFJDeDBleUJTWGNRbnFEcFJlU3Y0enVXaHdDUldzc29jOUxfbmo2QSIsInkiOiJpRzI5Vks2bDJVNXNLQlpVU0plUHZ5RnVzWGdTbEsyZERGbFdhQ004RjdrIn0sInJlY292ZXJ5Q29tbWl0bWVudCI6IkVpQ3NBN1NHTE5lZGE1SW5sb3Fub2tVY0pGejZ2S1Q0SFM1ZGNLcm1ubEpocEEifQ.lxWnrg5jaeCAhYuz1fPhidKw6Z2cScNlEc6SWcs15DtJbrHZFxl5IezGJ3cWdOSS2DlzDl4M1ZF8dDE9kRwFeQ", + "delta": { + "patches": [ + { + "action": "replace", + "document": { + "publicKeys": [ + { + "id": "newKey", + "type": "EcdsaSecp256k1VerificationKey2019", + "publicKeyJwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "JUWp0pAMGevNLhqq_Qmd48izuLYfO5XWpjSmy5btkjc", + "y": "QYaSu1NHYnxR4qfk-RkXb4NQnQf1X3XQCpDYuibvlNc" + }, + "purposes": [ + "authentication", + "assertionMethod", + "capabilityInvocation", + "capabilityDelegation", + "keyAgreement" + ] + } + ], + "services": [ + { + "id": "serviceId123", + "type": "someType", + "serviceEndpoint": "https://www.url.com" + } + ] + } + } + ], + "updateCommitment": "EiD6_csybTfxELBoMgkE9O2BTCmhScG_RW_qaZQkIkJ_aQ" + } + })).unwrap(); + + /// + static ref DEACTIVATE_OPERATION: Operation = serde_json::from_value(json!({ + "type": "deactivate", + "didSuffix": "EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg", + "revealValue": "EiB-dib5oumdaDGH47TB17Qg1nHza036bTIGibQOKFUY2A", + "signedData": "eyJhbGciOiJFUzI1NksifQ.eyJkaWRTdWZmaXgiOiJFaUR5T1FiYlpBYTNhaVJ6ZUNrVjdMT3gzU0VSampIOTNFWG9JTTNVb040b1dnIiwicmVjb3ZlcnlLZXkiOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoiSk1ucF9KOW5BSGFkTGpJNmJfNVU3M1VwSEZqSEZTVHdtc1ZUUG9FTTVsMCIsInkiOiJ3c1QxLXN0UWJvSldPeEJyUnVINHQwVV9zX1lSQy14WXQyRkFEVUNHR2M4In19.ARTZrvupKdShOFNAJ4EWnsuaONKBgXUiwY5Ct10a9IXIp1uFsg0UyDnZGZtJT2v2bgtmYsQBmT6L9kKaaDcvUQ" + })).unwrap(); + } + + #[test] + fn test_did_parse_format() { + let longform_did = SidetreeDID::::from_str(LONGFORM_DID).unwrap(); + let shortform_did = SidetreeDID::::from_str(SHORTFORM_DID).unwrap(); + assert_eq!(longform_did.to_string(), LONGFORM_DID); + assert_eq!(shortform_did.to_string(), SHORTFORM_DID); + assert!(LONGFORM_DID.starts_with(SHORTFORM_DID)); + } + + #[test] + fn test_longform_did_construction() { + let create_operation = match &*CREATE_OPERATION { + Operation::Create(op) => op, + _ => panic!("Expected Create Operation"), + }; + let did = SidetreeDID::::from_create_operation(&create_operation).unwrap(); + assert_eq!(did.to_string(), LONGFORM_DID); + } + + #[test] + fn test_update_verify_reveal() { + let create_pvo = CREATE_OPERATION + .clone() + .partial_verify::() + .unwrap(); + let update_pvo = UPDATE_OPERATION + .clone() + .partial_verify::() + .unwrap(); + update_pvo.follows::(&create_pvo).unwrap(); + } + + #[test] + fn test_recover_verify_reveal() { + let create_pvo = CREATE_OPERATION + .clone() + .partial_verify::() + .unwrap(); + let recover_pvo = RECOVER_OPERATION + .clone() + .partial_verify::() + .unwrap(); + recover_pvo.follows::(&create_pvo).unwrap(); + } + + #[test] + fn test_deactivate_verify_reveal() { + let recover_pvo = RECOVER_OPERATION + .clone() + .partial_verify::() + .unwrap(); + let deactivate_pvo = DEACTIVATE_OPERATION + .clone() + .partial_verify::() + .unwrap(); + deactivate_pvo.follows::(&recover_pvo).unwrap(); + } +} From a2068150831314d79784575a37370aaf50ed64f4 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Fri, 25 Feb 2022 10:47:26 -0500 Subject: [PATCH 6/9] Add DIDMethodError - Don't use anyhow in did module - Use Map for DID method options, for simpler checking --- did-ion/src/sidetree.rs | 74 ++++++++++++++++++++++++++--------------- src/did.rs | 51 ++++++++++++++++++---------- 2 files changed, 81 insertions(+), 44 deletions(-) diff --git a/did-ion/src/sidetree.rs b/did-ion/src/sidetree.rs index 402e836b7..22cd9eac8 100644 --- a/did-ion/src/sidetree.rs +++ b/did-ion/src/sidetree.rs @@ -6,8 +6,9 @@ use reqwest::{header, Client, StatusCode}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; use ssi::did::{ - DIDCreate, DIDDeactivate, DIDDocumentOperation, DIDMethod, DIDMethodTransaction, DIDRecover, - DIDUpdate, Document, Service, ServiceEndpoint, VerificationRelationship, + DIDCreate, DIDDeactivate, DIDDocumentOperation, DIDMethod, DIDMethodError, + DIDMethodTransaction, DIDRecover, DIDUpdate, Document, Service, ServiceEndpoint, + VerificationRelationship, }; use ssi::did_resolve::{ DIDResolver, DocumentMetadata, HTTPDIDResolver, ResolutionInputMetadata, ResolutionMetadata, @@ -1483,24 +1484,30 @@ impl DIDMethod for SidetreeClient { } } - fn create(&self, create: DIDCreate) -> Result { + fn create(&self, create: DIDCreate) -> Result { let DIDCreate { recovery_key, update_key, verification_key, options, } = create; - ensure!(is_empty(&options), "Create options not supported"); + for opt in options.keys() { + return Err(DIDMethodError::OptionNotSupported { + operation: "create", + option: opt.clone(), + }); + } let (update_pk, recovery_pk, patches) = new_did_state::(update_key, recovery_key, verification_key) .context("Prepare keys for DID creation")?; let operation = S::create_existing(&update_pk, &recovery_pk, patches) .context("Construct Create operation")?; - Self::op_to_transaction(operation).context("Construct create transaction") + let tx = Self::op_to_transaction(operation).context("Construct create transaction")?; + Ok(tx) } /// - async fn submit_transaction(&self, tx: DIDMethodTransaction) -> Result { + async fn submit_transaction(&self, tx: DIDMethodTransaction) -> Result { let op = Self::op_from_transaction(tx) .context("Convert DID method transaction to Sidetree operation")?; let endpoint = self @@ -1522,7 +1529,7 @@ impl DIDMethod for SidetreeClient { .json() .await .context("Transaction submit failed. Unable to read HTTP response JSON")?; - bail!("Transaction submit failed: {}: {}", e, err); + return Err(anyhow!("Transaction submit failed: {}: {}", e, err).into()); } if resp.content_length() == Some(0) { // Update operation may return empty body with 200 OK. @@ -1536,7 +1543,7 @@ impl DIDMethod for SidetreeClient { Ok(resp_json) } - fn did_from_transaction(&self, tx: DIDMethodTransaction) -> Result { + fn did_from_transaction(&self, tx: DIDMethodTransaction) -> Result { let op = Self::op_from_transaction(tx) .context("Convert DID method transaction to Sidetree operation")?; let did = match op { @@ -1555,7 +1562,7 @@ impl DIDMethod for SidetreeClient { Ok(did.to_string()) } - fn update(&self, update: DIDUpdate) -> Result { + fn update(&self, update: DIDUpdate) -> Result { let DIDUpdate { did, update_key, @@ -1564,7 +1571,12 @@ impl DIDMethod for SidetreeClient { options, } = update; let did = SidetreeDID::::from_str(&did).context("Parse Sidetree DID")?; - ensure!(is_empty(&options), "Update options not supported"); + for opt in options.keys() { + return Err(DIDMethodError::OptionNotSupported { + operation: "update", + option: opt.clone(), + }); + } let update_key = update_key.ok_or(anyhow!("Missing required new update key"))?; let new_update_key = new_update_key.ok_or(anyhow!("Missing required new update key"))?; S::validate_key(&new_update_key).context("Validate update key")?; @@ -1575,11 +1587,12 @@ impl DIDMethod for SidetreeClient { let did_suffix = DIDSuffix::from(did); let update_operation = S::update(did_suffix, &update_key, &new_update_pk, patches) .context("Construct Update operation")?; - Self::op_to_transaction(Operation::Update(update_operation)) - .context("Construct update transaction") + let tx = Self::op_to_transaction(Operation::Update(update_operation)) + .context("Construct update transaction")?; + Ok(tx) } - fn recover(&self, recover: DIDRecover) -> Result { + fn recover(&self, recover: DIDRecover) -> Result { let DIDRecover { did, recovery_key, @@ -1590,7 +1603,12 @@ impl DIDMethod for SidetreeClient { } = recover; let did = SidetreeDID::::from_str(&did).context("Parse Sidetree DID")?; let did_suffix = DIDSuffix::from(did); - ensure!(is_empty(&options), "Recover options not supported"); + for opt in options.keys() { + return Err(DIDMethodError::OptionNotSupported { + operation: "recover", + option: opt.clone(), + }); + } let recovery_key = recovery_key.ok_or(anyhow!("Missing required recovery key"))?; let (new_update_pk, new_recovery_pk, patches) = new_did_state::(new_update_key, new_recovery_key, new_verification_key) @@ -1603,21 +1621,31 @@ impl DIDMethod for SidetreeClient { patches, ) .context("Construct Recover operation")?; - Self::op_to_transaction(operation).context("Construct recover transaction") + let tx = Self::op_to_transaction(operation).context("Construct recover transaction")?; + Ok(tx) } - fn deactivate(&self, deactivate: DIDDeactivate) -> Result { + fn deactivate( + &self, + deactivate: DIDDeactivate, + ) -> Result { let DIDDeactivate { did, key, options } = deactivate; let did = SidetreeDID::::from_str(&did).context("Parse Sidetree DID")?; let recovery_key = key.ok_or(anyhow!( "Missing required recovery key for DID deactivation" ))?; - ensure!(is_empty(&options), "Deactivate options not supported"); + for opt in options.keys() { + return Err(DIDMethodError::OptionNotSupported { + operation: "deactivate", + option: opt.clone(), + }); + } let did_suffix = DIDSuffix::from(did); let deactivate_operation = ::deactivate(did_suffix, recovery_key) .context("Construct DID Deactivate operation")?; - Self::op_to_transaction(Operation::Deactivate(deactivate_operation)) - .context("Construct DID deactivate transaction") + let tx = Self::op_to_transaction(Operation::Deactivate(deactivate_operation)) + .context("Construct DID deactivate transaction")?; + Ok(tx) } } @@ -1642,14 +1670,6 @@ impl SidetreeClient { } } -fn is_empty(options: &Value) -> bool { - options.is_null() - || match options.as_object() { - Some(obj) => obj.is_empty(), - None => false, - } -} - #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl DIDResolver for HTTPSidetreeDIDResolver { diff --git a/src/did.rs b/src/did.rs index f36b50fd6..d09fca861 100644 --- a/src/did.rs +++ b/src/did.rs @@ -5,12 +5,12 @@ //! [did-core]: https://www.w3.org/TR/did-core/ use crate::caip10::BlockchainAccountId; -use anyhow::{bail, Result as AResult}; use std::collections::BTreeMap as Map; use std::collections::HashMap; use std::convert::TryFrom; use std::fmt; use std::str::FromStr; +use thiserror::Error; use crate::did_resolve::{ Content, ContentMetadata, DIDResolver, DereferencingInputMetadata, DereferencingMetadata, @@ -394,7 +394,7 @@ pub struct DIDCreate { pub update_key: Option, pub recovery_key: Option, pub verification_key: Option, - pub options: Value, + pub options: Map, } /// DID Update Operation @@ -405,7 +405,7 @@ pub struct DIDUpdate { pub update_key: Option, pub new_update_key: Option, pub operation: DIDDocumentOperation, - pub options: Value, + pub options: Map, } /// DID Recover Operation @@ -417,7 +417,7 @@ pub struct DIDRecover { pub new_update_key: Option, pub new_recovery_key: Option, pub new_verification_key: Option, - pub options: Value, + pub options: Map, } /// DID Deactivate Operation @@ -426,7 +426,7 @@ pub struct DIDRecover { pub struct DIDDeactivate { pub did: String, pub key: Option, - pub options: Value, + pub options: Map, } /// DID Document Operation @@ -483,6 +483,20 @@ pub struct DIDMethodTransaction { pub value: Value, } +/// An error having to do with a [DIDMethod]. +#[derive(Error, Debug)] +pub enum DIDMethodError { + #[error("Not implemented for DID method: {0}")] + NotImplemented(&'static str), + #[error("Option '{option}' not supported for DID operation '{operation}'")] + OptionNotSupported { + operation: &'static str, + option: String, + }, + #[error(transparent)] + Other(#[from] anyhow::Error), +} + /// An implementation of a [DID method](https://www.w3.org/TR/did-core/#dfn-did-methods). /// /// Depends on the [DIDResolver][] trait. @@ -505,33 +519,36 @@ pub trait DIDMethod: Sync { } /// Retrieve a DID from a DID method transaction - fn did_from_transaction(&self, _tx: DIDMethodTransaction) -> AResult { - bail!("DID from transaction not implemented for DID Method"); + fn did_from_transaction(&self, _tx: DIDMethodTransaction) -> Result { + Err(DIDMethodError::NotImplemented("DID from transaction")) } /// Submit a DID transaction - async fn submit_transaction(&self, _tx: DIDMethodTransaction) -> AResult { - bail!("Transaction submission not implemented for DID Method"); + async fn submit_transaction(&self, _tx: DIDMethodTransaction) -> Result { + Err(DIDMethodError::NotImplemented("Transaction submission")) } /// Create a DID - fn create(&self, _create: DIDCreate) -> AResult { - bail!("Create operation not implemented for DID Method"); + fn create(&self, _create: DIDCreate) -> Result { + Err(DIDMethodError::NotImplemented("Create operation")) } /// Update a DID - fn update(&self, _update: DIDUpdate) -> AResult { - bail!("Update operation not implemented for DID Method"); + fn update(&self, _update: DIDUpdate) -> Result { + Err(DIDMethodError::NotImplemented("Update operation")) } /// Recover a DID - fn recover(&self, _recover: DIDRecover) -> AResult { - bail!("Recover operation not implemented for DID Method"); + fn recover(&self, _recover: DIDRecover) -> Result { + Err(DIDMethodError::NotImplemented("Recover operation")) } /// Deactivate a DID - fn deactivate(&self, _deactivate: DIDDeactivate) -> AResult { - bail!("Deactivate operation not implemented for DID Method"); + fn deactivate( + &self, + _deactivate: DIDDeactivate, + ) -> Result { + Err(DIDMethodError::NotImplemented("Deactivate operation")) } /// Upcast the DID method as a DID resolver. From 369e0673f513b6ae2e9a0c6f4dcb025f88359030 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Fri, 25 Feb 2022 15:58:03 -0500 Subject: [PATCH 7/9] Use error types in sidetree - Add SidetreeError and JWSDecodeVerifyError - Remove anyhow from sidetree API --- did-ion/Cargo.toml | 1 + did-ion/src/lib.rs | 15 ++-- did-ion/src/sidetree.rs | 175 +++++++++++++++++++++++++++------------- 3 files changed, 131 insertions(+), 60 deletions(-) diff --git a/did-ion/Cargo.toml b/did-ion/Cargo.toml index d2875509a..5faac1cd5 100644 --- a/did-ion/Cargo.toml +++ b/did-ion/Cargo.toml @@ -20,6 +20,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_jcs = "0.1" anyhow = "1.0" +thiserror = "1.0" base64 = "0.12" sha2 = "0.10" json-patch = "0.2.6" diff --git a/did-ion/src/lib.rs b/did-ion/src/lib.rs index 662caf2a9..7e69546ec 100644 --- a/did-ion/src/lib.rs +++ b/did-ion/src/lib.rs @@ -1,9 +1,9 @@ -use anyhow::{ensure, Context, Error, Result}; +use anyhow::{anyhow, Context, Result}; use ssi::jwk::{Algorithm, JWK}; pub mod sidetree; -use sidetree::{is_secp256k1, Sidetree, SidetreeClient}; +use sidetree::{is_secp256k1, Sidetree, SidetreeClient, SidetreeError}; pub struct ION; @@ -11,12 +11,15 @@ pub struct ION; pub type DIDION = SidetreeClient; impl Sidetree for ION { - fn generate_key() -> Result { - JWK::generate_secp256k1().context("Generate secp256k1 key") + fn generate_key() -> Result { + let key = JWK::generate_secp256k1().context("Generate secp256k1 key")?; + Ok(key) } - fn validate_key(key: &JWK) -> Result<(), Error> { - ensure!(is_secp256k1(&key), "Key must be Secp256k1 for ION"); + fn validate_key(key: &JWK) -> Result<(), SidetreeError> { + if !is_secp256k1(&key) { + return Err(anyhow!("Key must be Secp256k1").into()); + } Ok(()) } diff --git a/did-ion/src/sidetree.rs b/did-ion/src/sidetree.rs index 22cd9eac8..73fc7799f 100644 --- a/did-ion/src/sidetree.rs +++ b/did-ion/src/sidetree.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, bail, ensure, Context, Error, Result}; +use anyhow::{anyhow, bail, ensure, Context, Error as AError, Result as AResult}; use async_trait::async_trait; use core::fmt::Debug; use json_patch::Patch; @@ -21,6 +21,7 @@ use std::convert::TryFrom; use std::fmt; use std::marker::PhantomData; use std::str::FromStr; +use thiserror::Error as ThisError; const MULTIHASH_SHA2_256_PREFIX: &[u8] = &[0x12]; const MULTIHASH_SHA2_256_SIZE: &[u8] = &[0x20]; @@ -35,6 +36,35 @@ const MULTIHASH_SHA2_256_SIZE: &[u8] = &[0x20]; /// [vmm]: https://www.w3.org/TR/did-core/#verification-methods pub const VERIFICATION_METHOD_TYPE: &str = "JsonWebSignature2020"; +/// An error having to do with [Sidetree]. +#[derive(ThisError, Debug)] +pub enum SidetreeError { + /// Some functionality was not implemented. + #[error("Not implemented: {0}")] + NotImplemented(&'static str), + /// Error from [serde_jcs::to_string] + #[error("Unable to execute JSON Canonicalization Scheme (JCS)")] + JCS(#[from] serde_json::Error), + /// A create operation following another operation is not valid. + #[error("Create operation cannot follow another operation")] + CreateCannotFollow, + /// Update commitment is missing + #[error("Missing update commitment")] + MissingUpdateCommitment, + /// Recovery commitment is missing + #[error("Missing recovery commitment")] + MissingRecoveryCommitment, + /// DID Suffix did not match expected value. + #[error("DID Suffix mismatch. Expected: '{expected}', but found '{actual}'")] + DIDSuffixMismatch { + expected: DIDSuffix, + actual: DIDSuffix, + }, + /// Some error occurred. + #[error(transparent)] + Other(#[from] anyhow::Error), +} + /// Parameters for a Sidetree client implementation /// /// This trait consistest of the subset of parameters defined in [Sidetree §5. Default Parameters][default-params] that are needed to implemented a Sidetree client, that is a client to the [Sidetree REST API][sidetree-rest]. @@ -105,21 +135,23 @@ pub trait Sidetree { } /// [`JSON_CANONICALIZATION_SCHEME`](https://identity.foundation/sidetree/spec/v1.0.0/#json-canonicalization-scheme) - fn json_canonicalization_scheme(value: &T) -> Result { - serde_jcs::to_string(value).context("JSON Canonicalization Scheme (JCS)") + fn json_canonicalization_scheme( + value: &T, + ) -> Result { + serde_jcs::to_string(value).map_err(SidetreeError::JCS) } /// Generate a new keypair ([KEY_ALGORITHM][ka]) /// /// [ka]: https://identity.foundation/sidetree/spec/v1.0.0/#key-algorithm - fn generate_key() -> Result; + fn generate_key() -> Result; /// Ensure that a keypair is valid for this Sidetree DID Method /// /// Check that the key uses this Sidetree DID method's [KEY_ALGORITHM][ka]. /// /// [ka]: https://identity.foundation/sidetree/spec/v1.0.0/#key-algorithm - fn validate_key(key: &JWK) -> Result<(), Error>; + fn validate_key(key: &JWK) -> Result<(), SidetreeError>; /// [`SIGNATURE_ALGORITHM`](https://identity.foundation/sidetree/spec/v1.0.0/#sig-algorithm) (JWS alg) const SIGNATURE_ALGORITHM: Algorithm; @@ -179,7 +211,7 @@ pub trait Sidetree { /// [Public Key Commitment Scheme (Sidetree §6.2.1)][pkcs] /// /// [pkcs]: https://identity.foundation/sidetree/spec/v1.0.0/#public-key-commitment-scheme - fn commitment_scheme(pkjwk: &PublicKeyJwk) -> Result { + fn commitment_scheme(pkjwk: &PublicKeyJwk) -> AResult { let canonicalized_public_key = Self::json_canonicalization_scheme(&pkjwk).context("Canonicalize JWK")?; // Note: hash_algorithm called here instead of reveal_value, since the underlying hash is @@ -209,7 +241,7 @@ pub trait Sidetree { update_pk: &PublicKeyJwk, recovery_pk: &PublicKeyJwk, patches: Vec, - ) -> Result { + ) -> AResult { ensure!( update_pk != recovery_pk, "Update and recovery public key JWK payload must be different." @@ -249,8 +281,8 @@ pub trait Sidetree { /// Create][create]. Returns the private keys and the create operation. /// /// [create]: https://identity.foundation/sidetree/spec/v1.0.0/#create - fn create(patches: Vec) -> Result<(Operation, JWK, JWK)> { - let update_keypair = Self::generate_key().context("Generate Update Key Pair")?; + fn create(patches: Vec) -> AResult<(Operation, JWK, JWK)> { + let update_keypair = Self::generate_key().context("generate update key pair")?; let recovery_keypair = Self::generate_key().context("Generate Recovery Key Pair")?; let update_pk = PublicKeyJwk::try_from(update_keypair.to_public()).context("Update public key")?; @@ -278,7 +310,7 @@ pub trait Sidetree { update_key: &JWK, new_update_pk: &PublicKeyJwk, patches: Vec, - ) -> Result { + ) -> AResult { let update_pk = PublicKeyJwk::try_from(update_key.to_public()) .context("Convert update key to PublicKeyJwk for Update operation")?; let canonicalized_update_pk = Self::json_canonicalization_scheme(&update_pk) @@ -331,7 +363,7 @@ pub trait Sidetree { new_update_pk: &PublicKeyJwk, new_recovery_pk: &PublicKeyJwk, patches: Vec, - ) -> Result { + ) -> AResult { let recovery_pk = PublicKeyJwk::try_from(recovery_key.to_public()) .context("Convert recovery key to PublicKeyJwk for Recover operation")?; ensure!( @@ -383,7 +415,7 @@ pub trait Sidetree { did_suffix: DIDSuffix, recovery_key: &JWK, patches: Vec, - ) -> Result<(Operation, JWK, JWK)> { + ) -> AResult<(Operation, JWK, JWK)> { let new_update_keypair = Self::generate_key().context("Generate New Update Key Pair")?; let new_update_pk = PublicKeyJwk::try_from(new_update_keypair.to_public()) .context("Convert new update public key")?; @@ -410,7 +442,7 @@ pub trait Sidetree { /// Deactivate][deactivate]. Returns the deactivate operation. /// /// [deactivate]: https://identity.foundation/sidetree/spec/v1.0.0/#deactivate - fn deactivate(did_suffix: DIDSuffix, recovery_key: JWK) -> Result { + fn deactivate(did_suffix: DIDSuffix, recovery_key: JWK) -> AResult { let recovery_pk = PublicKeyJwk::try_from(recovery_key.to_public()) .context("Convert recovery key to PublicKeyJwk for Deactivate operation")?; let canonicalized_recovery_pk = Self::json_canonicalization_scheme(&recovery_pk).context( @@ -436,7 +468,7 @@ pub trait Sidetree { /// DID][SidetreeDID::Short] ([`DIDSuffix`]). /// /// Reference: - fn serialize_suffix_data(suffix_data: &SuffixData) -> Result { + fn serialize_suffix_data(suffix_data: &SuffixData) -> AResult { let string = Self::json_canonicalization_scheme(suffix_data).context("Canonicalize Suffix Data")?; let hash = Self::hash(string.as_bytes()); @@ -444,7 +476,7 @@ pub trait Sidetree { } /// Check that a DID Suffix looks valid - fn validate_did_suffix(suffix: &DIDSuffix) -> Result<()> { + fn validate_did_suffix(suffix: &DIDSuffix) -> AResult<()> { let bytes = base64::decode_config(&suffix.0, base64::URL_SAFE_NO_PAD).context("Decode Base64")?; ensure!( @@ -512,13 +544,13 @@ pub enum PartiallyVerifiedOperation { trait SidetreeOperation { type PartiallyVerifiedForm; - fn partial_verify(self) -> Result; + fn partial_verify(self) -> AResult; } impl SidetreeOperation for Operation { type PartiallyVerifiedForm = PartiallyVerifiedOperation; - fn partial_verify(self) -> Result { + fn partial_verify(self) -> AResult { Ok(match self { Operation::Create(op) => PartiallyVerifiedOperation::Create( op.partial_verify::() @@ -544,7 +576,7 @@ fn ensure_reveal_commitment( recovery_commitment: &str, reveal_value: &str, pk: &PublicKeyJwk, -) -> Result<()> { +) -> AResult<()> { let canonicalized_public_key = S::json_canonicalization_scheme(&pk).context("Canonicalize JWK")?; let commitment_value = canonicalized_public_key.as_bytes(); @@ -583,15 +615,18 @@ impl PartiallyVerifiedOperation { } } - pub fn follows(&self, previous: &PartiallyVerifiedOperation) -> Result<()> { + pub fn follows( + &self, + previous: &PartiallyVerifiedOperation, + ) -> Result<(), SidetreeError> { match self { PartiallyVerifiedOperation::Create(_) => { - bail!("Create operation cannot follow another operation"); + return Err(SidetreeError::CreateCannotFollow); } PartiallyVerifiedOperation::Update(update) => { let update_commitment = previous .update_commitment() - .ok_or(anyhow!("No update commitment"))?; + .ok_or(SidetreeError::MissingUpdateCommitment)?; ensure_reveal_commitment::( &update_commitment, &update.reveal_value, @@ -601,7 +636,7 @@ impl PartiallyVerifiedOperation { PartiallyVerifiedOperation::Recover(recover) => { let recovery_commitment = previous .recovery_commitment() - .ok_or(anyhow!("No recovery commitment"))?; + .ok_or(SidetreeError::MissingRecoveryCommitment)?; ensure_reveal_commitment::( &recovery_commitment, &recover.reveal_value, @@ -610,10 +645,10 @@ impl PartiallyVerifiedOperation { } PartiallyVerifiedOperation::Deactivate(deactivate) => { if let PartiallyVerifiedOperation::Create(create) = previous { - ensure!( - deactivate.signed_did_suffix == create.did_suffix, - "DID Suffix mismatch" - ); + return Err(SidetreeError::DIDSuffixMismatch { + expected: create.did_suffix.clone(), + actual: deactivate.signed_did_suffix.clone(), + }); } else { // Note: Recover operations do not sign over the DID suffix. If the deactivate // operation follows a recover operation rather than a create operation, the @@ -621,7 +656,7 @@ impl PartiallyVerifiedOperation { } let recovery_commitment = previous .recovery_commitment() - .ok_or(anyhow!("No recovery commitment"))?; + .ok_or(SidetreeError::MissingRecoveryCommitment)?; ensure_reveal_commitment::( &recovery_commitment, &deactivate.reveal_value, @@ -636,7 +671,7 @@ impl PartiallyVerifiedOperation { impl SidetreeOperation for CreateOperation { type PartiallyVerifiedForm = PartiallyVerifiedCreateOperation; - fn partial_verify(self) -> Result { + fn partial_verify(self) -> AResult { let did = SidetreeDID::::from_create_operation(&self) .context("Unable to derive DID from create operation")?; let did_suffix = DIDSuffix::from(did); @@ -674,7 +709,7 @@ impl SidetreeOperation for UpdateOperation { /// by this function. The correspondence of the reveal value's hash to the previous update /// commitment is not checked either, since that is not known from this function. - fn partial_verify(self) -> Result { + fn partial_verify(self) -> AResult { // Verify JWS against public key in payload. // Then check public key against its hash (reveal value). let (header, claims) = @@ -712,7 +747,7 @@ impl SidetreeOperation for RecoverOperation { type PartiallyVerifiedForm = PartiallyVerifiedRecoverOperation; /// Partially verify a [RecoverOperation] - fn partial_verify(self) -> Result { + fn partial_verify(self) -> AResult { // Verify JWS against public key in payload. // Then check public key against its hash (reveal value). let (header, claims) = @@ -752,7 +787,7 @@ impl SidetreeOperation for DeactivateOperation { type PartiallyVerifiedForm = PartiallyVerifiedDeactivateOperation; /// Partially verify a [DeactivateOperation] - fn partial_verify(self) -> Result { + fn partial_verify(self) -> AResult { // Verify JWS against public key in payload. // Then check public key against its hash (reveal value). @@ -789,6 +824,13 @@ impl SidetreeOperation for DeactivateOperation { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct DIDSuffix(pub String); +impl fmt::Display for DIDSuffix { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0)?; + Ok(()) + } +} + /// A Sidetree-based DID /// /// Reference: [Sidetree §9. DID URI Composition][duc] @@ -898,7 +940,7 @@ pub struct PublicKeyEntry { } impl TryFrom for PublicKeyEntry { - type Error = Error; + type Error = AError; fn try_from(jwk: JWK) -> Result { let id = jwk.thumbprint().context("Compute JWK thumbprint")?; let pkjwk = PublicKeyJwk::try_from(jwk.to_public()).context("Convert key")?; @@ -1166,7 +1208,7 @@ pub struct PublicKeyJwk { } impl TryFrom for PublicKeyJwk { - type Error = Error; + type Error = AError; fn try_from(jwk: JWK) -> Result { let jwk_value = serde_json::to_value(jwk).context("Convert JWK to Value")?; ensure!( @@ -1184,7 +1226,7 @@ impl TryFrom for PublicKeyJwk { /// /// Note: `nonce` property is dropped. impl TryFrom for JWK { - type Error = Error; + type Error = AError; fn try_from(pkjwk: PublicKeyJwk) -> Result { let jwk = serde_json::from_value(pkjwk.jwk).context("Convert Value to JWK")?; Ok(jwk) @@ -1192,7 +1234,7 @@ impl TryFrom for JWK { } impl FromStr for SidetreeDID { - type Err = Error; + type Err = AError; fn from_str(did: &str) -> Result { let mut parts = did.split(':'); ensure!(parts.next() == Some("did"), "Expected DID URI scheme"); @@ -1240,7 +1282,7 @@ impl SidetreeDID { /// Construct a [Long-Form Sidetree DID][lfdu] from a [Create Operation][CreateOperation] /// /// [lfdu]: https://identity.foundation/sidetree/spec/v1.0.0/#long-form-did-uris - pub fn from_create_operation(create_operation: &CreateOperation) -> Result { + pub fn from_create_operation(create_operation: &CreateOperation) -> AResult { let op_json = S::json_canonicalization_scheme(&create_operation) .context("Canonicalize Create Operation")?; let op_string = S::data_encoding_scheme(op_json.as_bytes()); @@ -1258,7 +1300,7 @@ impl SidetreeDID { /// Convert a DID URL to an object id given a DID /// /// Object id is an id of a [ServiceEndpointEntry] or [PublicKeyEntry]. -fn did_url_to_id(did_url: &str, did: &SidetreeDID) -> Result { +fn did_url_to_id(did_url: &str, did: &SidetreeDID) -> AResult { let did_string = did.to_string(); let unprefixed = match did_url.strip_prefix(&did_string) { Some(s) => s, @@ -1346,7 +1388,7 @@ fn new_did_state( update_key: Option, recovery_key: Option, verification_key: Option, -) -> Result<(PublicKeyJwk, PublicKeyJwk, Vec)> { +) -> AResult<(PublicKeyJwk, PublicKeyJwk, Vec)> { let update_key = update_key.ok_or(anyhow!("Missing required update key"))?; S::validate_key(&update_key).context("Validate update key")?; let update_pk = PublicKeyJwk::try_from(update_key.to_public()).context("Convert update key")?; @@ -1380,7 +1422,7 @@ impl DIDStatePatch { fn try_from_with_did( did_doc_op: DIDDocumentOperation, did: &SidetreeDID, - ) -> Result { + ) -> AResult { Ok(match did_doc_op { DIDDocumentOperation::SetDidDocument(_doc) => { bail!("setDidDocument not implemented") @@ -1454,13 +1496,13 @@ impl DIDStatePatch { } #[derive(Debug, Serialize, Deserialize, Clone)] -struct SidetreeError { +struct SidetreeAPIError { // List of error codes: https://github.com/decentralized-identity/sidetree/blob/v1.0.0/lib/core/versions/1.0/ErrorCode.ts pub code: String, pub message: Option, } -impl fmt::Display for SidetreeError { +impl fmt::Display for SidetreeAPIError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Sidetree error {}", self.code)?; if let Some(ref message) = self.message { @@ -1525,7 +1567,7 @@ impl DIDMethod for SidetreeClient { .await .context("Send HTTP request")?; if let Err(e) = resp.error_for_status_ref() { - let err: SidetreeError = resp + let err: SidetreeAPIError = resp .json() .await .context("Transaction submit failed. Unable to read HTTP response JSON")?; @@ -1650,7 +1692,7 @@ impl DIDMethod for SidetreeClient { } impl SidetreeClient { - fn op_to_transaction(op: Operation) -> Result { + fn op_to_transaction(op: Operation) -> AResult { let value = serde_json::to_value(op).context("Convert operation to value")?; Ok(DIDMethodTransaction { did_method: S::METHOD.to_string(), @@ -1658,7 +1700,7 @@ impl SidetreeClient { }) } - fn op_from_transaction(tx: DIDMethodTransaction) -> Result { + fn op_from_transaction(tx: DIDMethodTransaction) -> AResult { let mut value = tx.value; let op_value = value .get_mut("sidetreeOperation") @@ -1696,6 +1738,26 @@ impl DIDResolver for HTTPSidetreeDIDResolver { } } +/// An error resulting from [jws_decode_verify_inner] +#[derive(ThisError, Debug)] +pub enum JWSDecodeVerifyError { + /// Unable to split JWS + #[error("Unable to split JWS")] + SplitJWS(#[source] ssi::error::Error), + /// Unable to decode JWS parts + #[error("Unable to decode JWS parts")] + DecodeJWSParts(#[source] ssi::error::Error), + /// Deserialize JWS payload + #[error("Deserialize JWS payload")] + DeserializeJWSPayload(#[source] serde_json::Error), + /// Unable to convert PublicKeyJwk to JWK + #[error("Unable to convert PublicKeyJwk to JWK")] + ConvertPublicKeyJwkToJWK(#[source] anyhow::Error), + /// Unable to verify JWS + #[error("Unable to verify JWS")] + VerifyJWS(#[source] ssi::error::Error), +} + /// Decode and verify JWS with public key inside payload /// /// Similar to [ssi::jwt::decode_verify] or [ssi::jws::decode_verify], but for when the payload (claims) must be parsed to @@ -1713,21 +1775,23 @@ impl DIDResolver for HTTPSidetreeDIDResolver { pub fn jws_decode_verify_inner( jwt: &str, get_key: impl FnOnce(&Claims) -> &PublicKeyJwk, -) -> Result<(Header, Claims), Error> { +) -> Result<(Header, Claims), JWSDecodeVerifyError> { use ssi::jws::{decode_jws_parts, split_jws, verify_bytes, DecodedJWS}; - let (header_b64, payload_enc, signature_b64) = split_jws(jwt).context("Split JWS")?; + let (header_b64, payload_enc, signature_b64) = + split_jws(jwt).map_err(JWSDecodeVerifyError::SplitJWS)?; let DecodedJWS { header, signing_input, payload, signature, } = decode_jws_parts(header_b64, payload_enc.as_bytes(), signature_b64) - .context("Decode JWS parts")?; - let claims: Claims = serde_json::from_slice(&payload).context("Deserialize JWS payload")?; + .map_err(JWSDecodeVerifyError::DecodeJWSParts)?; + let claims: Claims = + serde_json::from_slice(&payload).map_err(JWSDecodeVerifyError::DeserializeJWSPayload)?; let pk = get_key(&claims); - let pk = JWK::try_from(pk.clone()).context("Convert PublicKeyJwk to JWK")?; + let pk = JWK::try_from(pk.clone()).map_err(JWSDecodeVerifyError::ConvertPublicKeyJwkToJWK)?; verify_bytes(header.algorithm, &signing_input, &pk, &signature) - .context("Verify Signed Deactivate Data")?; + .map_err(JWSDecodeVerifyError::VerifyJWS)?; Ok((header, claims)) } @@ -1739,11 +1803,14 @@ mod tests { struct Example; impl Sidetree for Example { - fn generate_key() -> Result { - JWK::generate_secp256k1().context("Generate secp256k1 key") + fn generate_key() -> Result { + let key = JWK::generate_secp256k1().context("Generate secp256k1 key")?; + Ok(key) } - fn validate_key(key: &JWK) -> Result<(), Error> { - ensure!(is_secp256k1(&key), "Key must be Secp256k1"); + fn validate_key(key: &JWK) -> Result<(), SidetreeError> { + if !is_secp256k1(&key) { + return Err(anyhow!("Key must be Secp256k1").into()); + } Ok(()) } const SIGNATURE_ALGORITHM: Algorithm = Algorithm::ES256K; From 992dd341661a34fd318e72d8e2b2bf23a55a3609 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Fri, 25 Feb 2022 16:56:52 -0500 Subject: [PATCH 8/9] More Sidetree rustdocs --- did-ion/src/sidetree.rs | 50 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/did-ion/src/sidetree.rs b/did-ion/src/sidetree.rs index 73fc7799f..3a642e9c7 100644 --- a/did-ion/src/sidetree.rs +++ b/did-ion/src/sidetree.rs @@ -492,6 +492,12 @@ pub trait Sidetree { } } +/// Sidetree DID operation +/// +/// ### References +/// - +/// - +/// - #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "type")] #[serde(rename_all = "camelCase")] @@ -502,6 +508,9 @@ pub enum Operation { Deactivate(DeactivateOperation), } +/// Partially verified DID Create operation +/// +/// Converted from [CreateOperation]. #[derive(Debug, Clone)] pub struct PartiallyVerifiedCreateOperation { did_suffix: DIDSuffix, @@ -511,6 +520,9 @@ pub struct PartiallyVerifiedCreateOperation { hashed_delta: Delta, } +/// Partially verified DID Create operation +/// +/// Converted from [UpdateOperation]. #[derive(Debug, Clone)] pub struct PartiallyVerifiedUpdateOperation { reveal_value: String, @@ -518,6 +530,9 @@ pub struct PartiallyVerifiedUpdateOperation { signed_update_key: PublicKeyJwk, } +/// Partially verified DID Recovery operation +/// +/// Converted from [RecoverOperation]. #[derive(Debug, Clone)] pub struct PartiallyVerifiedRecoverOperation { reveal_value: String, @@ -527,6 +542,9 @@ pub struct PartiallyVerifiedRecoverOperation { signed_anchor_origin: Option, } +/// Partially verified DID Deactivate operation +/// +/// Converted from [DeactivateOperation]. #[derive(Debug, Clone)] pub struct PartiallyVerifiedDeactivateOperation { signed_did_suffix: DIDSuffix, @@ -534,6 +552,13 @@ pub struct PartiallyVerifiedDeactivateOperation { signed_recovery_key: PublicKeyJwk, } +/// Partially verified Sidetree DID operation +/// +/// Converted from [Operation]. +/// +/// Operation verification is described in [Sidetree §10.2.1 Operation Verification][ov]. +/// +/// [ov]: https://identity.foundation/sidetree/spec/v1.0.0/#operation-verification #[derive(Debug, Clone)] pub enum PartiallyVerifiedOperation { Create(PartiallyVerifiedCreateOperation), @@ -542,8 +567,25 @@ pub enum PartiallyVerifiedOperation { Deactivate(PartiallyVerifiedDeactivateOperation), } -trait SidetreeOperation { +/// A Sidetree operation +/// +/// See also the enum [Operation] which implements this trait. +pub trait SidetreeOperation { + /// The result of [partially verifying][Self::partial_verify] the operation. type PartiallyVerifiedForm; + + /// Partially verify the operation. + /// + /// Operation verification is described in [Sidetree §10.2.1 Operation Verification][ov]. + /// + /// This function verifies the internal consistency (including signatures and hashes) of the operation, + /// and returns the integrity-verified data. + /// Public key commitment values are not checked; that is, the signature is verified, but + /// whether the public key is the correct reveal value is not checked, since that depends on + /// what the previous operation was. The DID suffix is also not checked, except for a Create + /// operation, since it is otherwise in reference to an earlier (Create) opeation. + /// + /// [ov]: https://identity.foundation/sidetree/spec/v1.0.0/#operation-verification fn partial_verify(self) -> AResult; } @@ -705,7 +747,7 @@ impl SidetreeOperation for UpdateOperation { /// - the operation's [delta object](UpdateOperation::delta) is verified against the /// [delta hash](UpdateClaims::update_key) in the signed data payload. /// - /// The [DID Suffix](UpdateOperation::did_suffix), and the delta values, are **not** verified + /// The [DID Suffix](UpdateOperation::did_suffix) is **not** verified /// by this function. The correspondence of the reveal value's hash to the previous update /// commitment is not checked either, since that is not known from this function. @@ -1066,11 +1108,11 @@ pub enum DIDStatePatch { /// ### References /// - [Sidetree §11.1 Create - Create Operation Delta Object][codo] /// - [Sidetree §11.2 Update - Update Operation Delta Object][uodo] -/// - [Sidetree §11.3 Recover - Recover Operation Delta Object][uodo] +/// - [Sidetree §11.3 Recover - Recover Operation Delta Object][rodo] /// /// [codo]: https://identity.foundation/sidetree/spec/v1.0.0/#create-delta-object /// [uodo]: https://identity.foundation/sidetree/spec/v1.0.0/#update-delta-object -/// [rodo] https://identity.foundation/sidetree/spec/v1.0.0/#recover-delta-object +/// [rodo]: https://identity.foundation/sidetree/spec/v1.0.0/#recover-delta-object #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Delta { From cd6a760046c6d95b4f7eda1f79f8fd6d7e7e0f86 Mon Sep 17 00:00:00 2001 From: "Charles E. Lehner" Date: Fri, 25 Feb 2022 17:13:49 -0500 Subject: [PATCH 9/9] Use error type for PublicKeyJwk JWK conversion - Add PublicKeyJwkFromJWKError - Add JWKFromPublicKeyJwkError - Remove anyhow::Error from JWSDecodeVerifyError --- did-ion/src/sidetree.rs | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/did-ion/src/sidetree.rs b/did-ion/src/sidetree.rs index 3a642e9c7..024e37517 100644 --- a/did-ion/src/sidetree.rs +++ b/did-ion/src/sidetree.rs @@ -1249,14 +1249,32 @@ pub struct PublicKeyJwk { jwk: Value, } +/// Error resulting from [converting JWK to PublicKeyJwk][PublicKeyJwk::try_from] +#[derive(ThisError, Debug)] +pub enum PublicKeyJwkFromJWKError { + /// Unable to convert JWK to [Value] + #[error("Unable to convert JWK to Value")] + ToValue(#[from] serde_json::Error), + /// Public Key JWK must not contain private key parameters (e.g. "d") + #[error("Public Key JWK must not contain private key parameters")] + PrivateKeyParameters, +} + +/// Error resulting from attempting to convert [PublicKeyJwk] to JWK +#[derive(ThisError, Debug)] +pub enum JWKFromPublicKeyJwkError { + /// Unable to convert [Value] to JWK + #[error("Unable to convert Value to JWK")] + FromValue(#[from] serde_json::Error), +} + impl TryFrom for PublicKeyJwk { - type Error = AError; + type Error = PublicKeyJwkFromJWKError; fn try_from(jwk: JWK) -> Result { - let jwk_value = serde_json::to_value(jwk).context("Convert JWK to Value")?; - ensure!( - jwk_value.get("d").is_none(), - "Public Key JWK must not contain private key parameters" - ); + let jwk_value = serde_json::to_value(jwk).map_err(PublicKeyJwkFromJWKError::ToValue)?; + if jwk_value.get("d").is_some() { + return Err(PublicKeyJwkFromJWKError::PrivateKeyParameters); + }; Ok(Self { jwk: jwk_value, nonce: None, @@ -1268,9 +1286,9 @@ impl TryFrom for PublicKeyJwk { /// /// Note: `nonce` property is dropped. impl TryFrom for JWK { - type Error = AError; + type Error = JWKFromPublicKeyJwkError; fn try_from(pkjwk: PublicKeyJwk) -> Result { - let jwk = serde_json::from_value(pkjwk.jwk).context("Convert Value to JWK")?; + let jwk = serde_json::from_value(pkjwk.jwk).map_err(JWKFromPublicKeyJwkError::FromValue)?; Ok(jwk) } } @@ -1794,7 +1812,7 @@ pub enum JWSDecodeVerifyError { DeserializeJWSPayload(#[source] serde_json::Error), /// Unable to convert PublicKeyJwk to JWK #[error("Unable to convert PublicKeyJwk to JWK")] - ConvertPublicKeyJwkToJWK(#[source] anyhow::Error), + JWKFromPublicKeyJwk(#[source] JWKFromPublicKeyJwkError), /// Unable to verify JWS #[error("Unable to verify JWS")] VerifyJWS(#[source] ssi::error::Error), @@ -1831,7 +1849,7 @@ pub fn jws_decode_verify_inner( let claims: Claims = serde_json::from_slice(&payload).map_err(JWSDecodeVerifyError::DeserializeJWSPayload)?; let pk = get_key(&claims); - let pk = JWK::try_from(pk.clone()).map_err(JWSDecodeVerifyError::ConvertPublicKeyJwkToJWK)?; + let pk = JWK::try_from(pk.clone()).map_err(JWSDecodeVerifyError::JWKFromPublicKeyJwk)?; verify_bytes(header.algorithm, &signing_input, &pk, &signature) .map_err(JWSDecodeVerifyError::VerifyJWS)?; Ok((header, claims))