Skip to content

Commit

Permalink
Create and resolve did:jwk (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
amika-sq authored Dec 4, 2023
1 parent a9d41fb commit 7e12405
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 20 deletions.
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ repository = "https://github.com/TBD54566975/web5-rs.git"
license-file = "LICENSE"

[workspace.dependencies]
thiserror = "1.0.50"
thiserror = "1.0.50"
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
serde_with = "3.4.0"
6 changes: 3 additions & 3 deletions crates/credentials/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
15 changes: 13 additions & 2 deletions crates/dids/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
21 changes: 21 additions & 0 deletions crates/dids/src/did.rs
Original file line number Diff line number Diff line change
@@ -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<dyn KeyManager>;

/// 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
}
}
17 changes: 3 additions & 14 deletions crates/dids/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
120 changes: 120 additions & 0 deletions crates/dids/src/method/jwk.rs
Original file line number Diff line number Diff line change
@@ -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<dyn KeyManager>,
}

impl Did for DidJwk {
fn uri(&self) -> &str {
&self.uri
}

fn key_manager(&self) -> &Arc<dyn KeyManager> {
&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<DidJwk, DidJwkCreateOptions> for DidJwk {
const NAME: &'static str = "jwk";

fn create(
key_manager: Arc<dyn KeyManager>,
options: DidJwkCreateOptions,
) -> Result<DidJwk, DidMethodError> {
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())
);
}
}
41 changes: 41 additions & 0 deletions crates/dids/src/method/mod.rs
Original file line number Diff line number Diff line change
@@ -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<T: Did, CreateOptions> {
/// 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<dyn KeyManager>,
options: CreateOptions,
) -> Result<T, DidMethodError>;

/// 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;
}
109 changes: 109 additions & 0 deletions crates/dids/src/resolver.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub did_resolution_metadata: DidResolutionMetadata,
pub did_document: Option<DidDocument>,
pub did_document_metadata: Option<DidDocumentMetadata>,
}

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())
);
}
}

0 comments on commit 7e12405

Please sign in to comment.