From 7e1240504dbe128709271cb7d101a23d538690f3 Mon Sep 17 00:00:00 2001 From: Adam Mika <88001738+amika-sq@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:59:55 -0700 Subject: [PATCH] Create and resolve did:jwk (#20) --- Cargo.toml | 5 +- crates/credentials/Cargo.toml | 6 +- crates/dids/Cargo.toml | 15 ++++- crates/dids/src/did.rs | 21 ++++++ crates/dids/src/lib.rs | 17 +---- crates/dids/src/method/jwk.rs | 120 ++++++++++++++++++++++++++++++++++ crates/dids/src/method/mod.rs | 41 ++++++++++++ crates/dids/src/resolver.rs | 109 ++++++++++++++++++++++++++++++ 8 files changed, 314 insertions(+), 20 deletions(-) create mode 100644 crates/dids/src/did.rs create mode 100644 crates/dids/src/method/jwk.rs create mode 100644 crates/dids/src/method/mod.rs create mode 100644 crates/dids/src/resolver.rs diff --git a/Cargo.toml b/Cargo.toml index 32958c24..b2365c12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,7 @@ repository = "https://github.com/TBD54566975/web5-rs.git" license-file = "LICENSE" [workspace.dependencies] -thiserror = "1.0.50" \ No newline at end of file +thiserror = "1.0.50" +serde = { version = "1.0.193", features = ["derive"] } +serde_json = "1.0.108" +serde_with = "3.4.0" \ No newline at end of file diff --git a/crates/credentials/Cargo.toml b/crates/credentials/Cargo.toml index 8ba37d87..3b66ffb0 100644 --- a/crates/credentials/Cargo.toml +++ b/crates/credentials/Cargo.toml @@ -8,9 +8,9 @@ license-file.workspace = true [dependencies] jsonschema = "0.17.1" -serde = { version = "1.0.193", features = ["derive"] } -serde_json = "1.0.108" -serde_with = "3.4.0" +serde = { workspace = true } +serde_json = { workspace = true } +serde_with = { workspace = true } [dev-dependencies] serde_canonical_json = "1.0.0" \ No newline at end of file diff --git a/crates/dids/Cargo.toml b/crates/dids/Cargo.toml index bddb7a9e..e1882373 100644 --- a/crates/dids/Cargo.toml +++ b/crates/dids/Cargo.toml @@ -2,7 +2,18 @@ name = "dids" version = "0.1.0" edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +homepage.workspace = true +repository.workspace = true +license-file.workspace = true [dependencies] +async-trait = "0.1.74" +crypto = { path = "../crypto" } +did-jwk = "0.1.1" +serde = { workspace = true } +serde_json = { workspace = true } +ssi-dids = "0.1.1" +thiserror = { workspace = true } + +[dev-dependencies] +tokio = { version = "1.34.0", features = ["macros", "test-util"] } \ No newline at end of file diff --git a/crates/dids/src/did.rs b/crates/dids/src/did.rs new file mode 100644 index 00000000..116f888b --- /dev/null +++ b/crates/dids/src/did.rs @@ -0,0 +1,21 @@ +use crate::resolver::{DidResolutionResult, DidResolver}; +use async_trait::async_trait; +use crypto::key_manager::KeyManager; +use std::sync::Arc; + +/// Trait that defines all common behavior for a DID. +#[async_trait] +pub trait Did { + /// Returns the DID URI the target [`Did`] represents (e.g: `did:jwk:12345`). + fn uri(&self) -> &str; + + /// Returns a reference to the [`KeyManager`] that contains all the keys necessary to + /// manage and sign using the target [`Did`]. + fn key_manager(&self) -> &Arc; + + /// Returns a [`DidResolutionResult`] for the target [`Did`], as specified in + /// [Resolving a DID](https://w3c-ccg.github.io/did-resolution/#resolving). + async fn resolve(&self) -> DidResolutionResult { + DidResolver::resolve_uri(self.uri()).await + } +} diff --git a/crates/dids/src/lib.rs b/crates/dids/src/lib.rs index 7d12d9af..b1e62004 100644 --- a/crates/dids/src/lib.rs +++ b/crates/dids/src/lib.rs @@ -1,14 +1,3 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod did; +pub mod method; +pub mod resolver; diff --git a/crates/dids/src/method/jwk.rs b/crates/dids/src/method/jwk.rs new file mode 100644 index 00000000..33fe7041 --- /dev/null +++ b/crates/dids/src/method/jwk.rs @@ -0,0 +1,120 @@ +use crate::did::Did; +use crate::method::{DidMethod, DidMethodError}; +use crate::resolver::DidResolutionResult; +use async_trait::async_trait; +use crypto::key::{Key, KeyType}; +use crypto::key_manager::KeyManager; +use did_jwk::DIDJWK as SpruceDidJwkMethod; +use ssi_dids::did_resolve::{DIDResolver, ResolutionInputMetadata}; +use ssi_dids::{DIDMethod, Source}; +use std::sync::Arc; + +/// Concrete implementation for a did:jwk DID +pub struct DidJwk { + uri: String, + key_manager: Arc, +} + +impl Did for DidJwk { + fn uri(&self) -> &str { + &self.uri + } + + fn key_manager(&self) -> &Arc { + &self.key_manager + } +} + +/// Options that can be used to create a did:jwk DID +pub struct DidJwkCreateOptions { + pub key_type: KeyType, +} + +#[async_trait] +impl DidMethod for DidJwk { + const NAME: &'static str = "jwk"; + + fn create( + key_manager: Arc, + options: DidJwkCreateOptions, + ) -> Result { + let key_alias = key_manager.generate_private_key(options.key_type)?; + let public_key = + key_manager + .get_public_key(&key_alias)? + .ok_or(DidMethodError::DidCreationFailure( + "PublicKey not found".to_string(), + ))?; + + let uri = SpruceDidJwkMethod + .generate(&Source::Key(&public_key.jwk())) + .ok_or(DidMethodError::DidCreationFailure( + "Failed to generate did:jwk".to_string(), + ))?; + + Ok(DidJwk { uri, key_manager }) + } + + async fn resolve_uri(did_uri: &str) -> DidResolutionResult { + let input_metadata = ResolutionInputMetadata::default(); + let (did_resolution_metadata, did_document, did_document_metadata) = + SpruceDidJwkMethod.resolve(did_uri, &input_metadata).await; + + DidResolutionResult { + did_resolution_metadata, + did_document, + did_document_metadata, + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ssi_dids::did_resolve::ERROR_INVALID_DID; + + fn create_did_jwk() -> DidJwk { + let key_manager = Arc::new(crypto::key_manager::LocalKeyManager::new_in_memory()); + let options = DidJwkCreateOptions { + key_type: KeyType::Ed25519, + }; + + DidJwk::create(key_manager, options).unwrap() + } + + #[test] + fn create_produces_correct_uri() { + let did = create_did_jwk(); + assert!(did.uri.starts_with("did:jwk:")); + } + + #[tokio::test] + async fn instance_resolve() { + let did = create_did_jwk(); + let result = did.resolve().await; + assert!(result.did_resolution_metadata.error.is_none()); + + let did_document = result.did_document.unwrap(); + assert_eq!(did_document.id, did.uri); + } + + #[tokio::test] + async fn resolve_uri_success() { + let did = create_did_jwk(); + let result = DidJwk::resolve_uri(&did.uri).await; + assert!(result.did_resolution_metadata.error.is_none()); + + let did_document = result.did_document.unwrap(); + assert_eq!(did_document.id, did.uri); + } + + #[tokio::test] + async fn resolve_uri_failure() { + let result = DidJwk::resolve_uri("did:jwk:does-not-exist").await; + assert_eq!( + result.did_resolution_metadata.error, + Some(ERROR_INVALID_DID.to_string()) + ); + } +} diff --git a/crates/dids/src/method/mod.rs b/crates/dids/src/method/mod.rs new file mode 100644 index 00000000..c5a77253 --- /dev/null +++ b/crates/dids/src/method/mod.rs @@ -0,0 +1,41 @@ +pub mod jwk; + +use crate::did::Did; +use crate::resolver::DidResolutionResult; +use async_trait::async_trait; +use crypto::key_manager::{KeyManager, KeyManagerError}; +use std::sync::Arc; + +/// Errors that can occur when working with DID methods. +#[derive(thiserror::Error, Debug)] +pub enum DidMethodError { + #[error(transparent)] + KeyManagerError(#[from] KeyManagerError), + #[error("Failure creating DID: {0}")] + DidCreationFailure(String), +} + +/// A trait with common behavior across all DID methods. +#[async_trait] +pub trait DidMethod { + /// The name of the implemented DID method (e.g. `jwk`). + /// + /// This is used to identify the [`DidMethod`] responsible for creating/resolving an arbitrary + /// DID URI. + /// + /// # Example + /// If a consumer wants to resolve a DID URI of `did:jwk:12345`, the method portion of the URI + /// (`jwk` in this example) is compared against each [`DidMethod`]'s `NAME` constant. If a match + /// is found, the corresponding [`DidMethod`] is used to resolve the DID URI. + const NAME: &'static str; + + /// Create a new DID instance. + fn create( + key_manager: Arc, + options: CreateOptions, + ) -> Result; + + /// Resolve a DID URI to a [`DidResolutionResult`], as specified in + /// [Resolving a DID](https://w3c-ccg.github.io/did-resolution/#resolving). + async fn resolve_uri(did_uri: &str) -> DidResolutionResult; +} diff --git a/crates/dids/src/resolver.rs b/crates/dids/src/resolver.rs new file mode 100644 index 00000000..16d82b85 --- /dev/null +++ b/crates/dids/src/resolver.rs @@ -0,0 +1,109 @@ +use crate::method::jwk::DidJwk; +use crate::method::DidMethod; +use serde::{Deserialize, Serialize}; +use ssi_dids::did_resolve::{ + DocumentMetadata as DidDocumentMetadata, ResolutionMetadata as DidResolutionMetadata, +}; +use ssi_dids::Document as DidDocument; + +pub struct DidResolver; + +impl DidResolver { + /// Resolves a DID URI, using the appropriate DID method, to a DID Document. + pub async fn resolve_uri(did_uri: &str) -> DidResolutionResult { + let method_name = match DidResolver::method_name(did_uri) { + Some(method_name) => method_name, + None => return DidResolutionResult::from_error(ERROR_INVALID_DID), + }; + + match method_name { + DidJwk::NAME => DidJwk::resolve_uri(did_uri).await, + _ => return DidResolutionResult::from_error(ERROR_METHOD_NOT_SUPPORTED), + } + } + + /// Returns the method name of a DID URI, if the provided DID URI is valid, `None` otherwise. + fn method_name(did_uri: &str) -> Option<&str> { + let parts: Vec<&str> = did_uri.split(':').collect(); + if parts.len() < 3 || parts[0] != "did" { + return None; + }; + + Some(parts[1]) + } +} + +/// Result of a DID resolution. +/// +/// See [Resolving a DID](https://w3c-ccg.github.io/did-resolution/#resolving) for more information +/// about the resolution process, and documentation around expected results formats in the case +/// there was an error resolving the DID. +#[derive(Debug, Deserialize, Serialize)] +pub struct DidResolutionResult { + #[serde(rename = "@context")] + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + pub did_resolution_metadata: DidResolutionMetadata, + pub did_document: Option, + pub did_document_metadata: Option, +} + +const DID_RESOLUTION_V1_CONTEXT: &str = "https://w3id.org/did-resolution/v1"; +const ERROR_METHOD_NOT_SUPPORTED: &str = "methodNotSupported"; +const ERROR_INVALID_DID: &str = "invalidDid"; + +impl Default for DidResolutionResult { + fn default() -> Self { + Self { + context: Some(DID_RESOLUTION_V1_CONTEXT.to_string()), + did_resolution_metadata: DidResolutionMetadata::default(), + did_document: None, + did_document_metadata: None, + } + } +} + +impl DidResolutionResult { + /// Convenience method for creating a DidResolutionResult with an error. + pub fn from_error(err: &str) -> Self { + Self { + did_resolution_metadata: DidResolutionMetadata::from_error(err), + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn resolve_did_jwk() { + let did_uri = "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9"; + let result = DidResolver::resolve_uri(did_uri).await; + assert!(result.did_resolution_metadata.error.is_none()); + + let did_document = result.did_document.unwrap(); + assert_eq!(did_document.id, did_uri); + } + + #[tokio::test] + async fn resolve_invalid_did() { + let did_uri = "did:jwk"; + let result = DidResolver::resolve_uri(did_uri).await; + assert_eq!( + result.did_resolution_metadata.error, + Some(ERROR_INVALID_DID.to_string()) + ); + } + + #[tokio::test] + async fn resolve_unsupported_method() { + let did_uri = "did:unsupported:1234"; + let result = DidResolver::resolve_uri(did_uri).await; + assert_eq!( + result.did_resolution_metadata.error, + Some(ERROR_METHOD_NOT_SUPPORTED.to_string()) + ); + } +}