Skip to content

Commit

Permalink
did:jwk implementation & resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
UMR1352 committed Aug 30, 2024
1 parent 13f9987 commit 98d9520
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 0 deletions.
1 change: 1 addition & 0 deletions identity_did/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ description = "Agnostic implementation of the Decentralized Identifiers (DID) st
did_url_parser = { version = "0.2.0", features = ["std", "serde"] }
form_urlencoded = { version = "1.2.0", default-features = false, features = ["alloc"] }
identity_core = { version = "=1.3.1", path = "../identity_core" }
identity_jose = { version = "=1.3.1", path = "../identity_jose" }
serde.workspace = true
strum.workspace = true
thiserror.workspace = true
Expand Down
120 changes: 120 additions & 0 deletions identity_did/src/did_jwk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use std::fmt::Debug;
use std::fmt::Display;
use std::str::FromStr;

use identity_jose::jwk::Jwk;
use identity_jose::jwu::decode_b64_json;

use crate::CoreDID;
use crate::Error;
use crate::DID;

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)]
#[repr(transparent)]
#[serde(into = "CoreDID", try_from = "CoreDID")]
/// A type representing a `did:jwk` DID.
pub struct DIDJwk(CoreDID);

impl DIDJwk {
/// [`DIDKey`]'s method.
pub const METHOD: &'static str = "jwk";

/// Tries to parse a [`DIDKey`] from a string.
pub fn parse(s: &str) -> Result<Self, Error> {
s.parse()
}

/// Returns the JWK encoded inside this did:jwk.
pub fn jwk(&self) -> Jwk {
decode_b64_json(self.method_id()).expect("did:jwk encodes a valid JWK")
}
}

impl AsRef<CoreDID> for DIDJwk {
fn as_ref(&self) -> &CoreDID {
&self.0
}
}

impl From<DIDJwk> for CoreDID {
fn from(value: DIDJwk) -> Self {
value.0
}
}

impl<'a> TryFrom<&'a str> for DIDJwk {
type Error = Error;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
value.parse()
}
}

impl Display for DIDJwk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

impl FromStr for DIDJwk {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<CoreDID>().and_then(TryFrom::try_from)
}
}

impl From<DIDJwk> for String {
fn from(value: DIDJwk) -> Self {
value.to_string()
}
}

impl TryFrom<CoreDID> for DIDJwk {
type Error = Error;
fn try_from(value: CoreDID) -> Result<Self, Self::Error> {
let Self::METHOD = value.method() else {
return Err(Error::InvalidMethodName);
};
decode_b64_json::<Jwk>(value.method_id())
.map(|_| Self(value))
.map_err(|_| Error::InvalidMethodId)
}
}

#[cfg(test)]
mod tests {
use identity_core::convert::FromJson;

use super::*;

#[test]
fn test_valid_deserialization() -> Result<(), Error> {
"did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9".parse::<DIDJwk>()?;
"did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9".parse::<DIDJwk>()?;

Ok(())
}

#[test]
fn test_jwk() {
let did = DIDJwk::parse("did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9").unwrap();
let target_jwk = Jwk::from_json_value(serde_json::json!({
"kty":"OKP","crv":"X25519","use":"enc","x":"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08"
}))
.unwrap();

assert_eq!(did.jwk(), target_jwk);
}

#[test]
fn test_invalid_deserialization() {
assert!(
"did:iota:0xf4d6f08f5a1b80dd578da7dc1b49c886d580acd4cf7d48119dfeb82b538ad88a"
.parse::<DIDJwk>()
.is_err()
);
assert!("did:jwk:".parse::<DIDJwk>().is_err());
assert!("did:jwk:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp"
.parse::<DIDJwk>()
.is_err());
}
}
2 changes: 2 additions & 0 deletions identity_did/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#[allow(clippy::module_inception)]
mod did;
mod did_jwk;
mod did_url;
mod error;

Expand All @@ -26,4 +27,5 @@ pub use crate::did_url::RelativeDIDUrl;
pub use ::did_url_parser::DID as BaseDIDUrl;
pub use did::CoreDID;
pub use did::DID;
pub use did_jwk::*;
pub use error::Error;
47 changes: 47 additions & 0 deletions identity_document/src/document/core_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use core::fmt::Formatter;
use std::collections::HashMap;
use std::convert::Infallible;

use identity_did::DIDJwk;
use identity_verification::jose::jwk::Jwk;
use identity_verification::jose::jws::DecodedJws;
use identity_verification::jose::jws::Decoder;
Expand Down Expand Up @@ -984,6 +985,23 @@ impl CoreDocument {
}
}

impl CoreDocument {
/// Creates a [`CoreDocument`] from a did:jwk DID.
pub fn expand_did_jwk(did_jwk: DIDJwk) -> Result<Self, Error> {
let verification_method = VerificationMethod::try_from(did_jwk.clone()).map_err(Error::InvalidKeyMaterial)?;
let verification_method_id = verification_method.id().clone();

DocumentBuilder::default()
.id(did_jwk.into())
.verification_method(verification_method)
.assertion_method(verification_method_id.clone())
.authentication(verification_method_id.clone())
.capability_invocation(verification_method_id.clone())
.capability_delegation(verification_method_id.clone())
.build()
}
}

#[cfg(test)]
mod tests {
use identity_core::convert::FromJson;
Expand Down Expand Up @@ -1682,4 +1700,33 @@ mod tests {
verifier(json);
}
}

#[test]
fn test_did_jwk_expansion() {
let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9"
.parse::<DIDJwk>()
.unwrap();
let target_doc = serde_json::from_value(serde_json::json!({
"id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9",
"verificationMethod": [
{
"id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0",
"type": "JsonWebKey2020",
"controller": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9",
"publicKeyJwk": {
"kty":"OKP",
"crv":"X25519",
"use":"enc",
"x":"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08"
}
}
],
"assertionMethod": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"],
"authentication": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"],
"capabilityInvocation": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"],
"capabilityDelegation": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"]
})).unwrap();

assert_eq!(CoreDocument::expand_did_jwk(did_jwk).unwrap(), target_doc);
}
}
9 changes: 9 additions & 0 deletions identity_iota_core/src/document/iota_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,15 @@ impl From<IotaDocument> for CoreDocument {
}
}

impl From<CoreDocument> for IotaDocument {
fn from(value: CoreDocument) -> Self {
IotaDocument {
document: value,
metadata: IotaDocumentMetadata::default(),
}
}
}

impl TryFrom<(CoreDocument, IotaDocumentMetadata)> for IotaDocument {
type Error = Error;
/// Converts the tuple into an [`IotaDocument`] if the given [`CoreDocument`] has an identifier satisfying the
Expand Down
28 changes: 28 additions & 0 deletions identity_resolver/src/resolution/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use core::future::Future;
use futures::stream::FuturesUnordered;
use futures::TryStreamExt;
use identity_did::DIDJwk;
use identity_did::DID;
use std::collections::HashSet;

Expand Down Expand Up @@ -247,6 +248,22 @@ impl<DOC: 'static> Resolver<DOC, SingleThreadedCommand<DOC>> {
}
}

impl<DOC: From<CoreDocument> + 'static> Resolver<DOC, SingleThreadedCommand<DOC>> {
/// Attaches a handler capable of resolving `did:jwk` DIDs.
pub fn attach_did_jwk_handler(&mut self) {
let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) };
self.attach_handler(DIDJwk::METHOD.to_string(), handler)
}
}

impl<DOC: From<CoreDocument> + 'static> Resolver<DOC, SendSyncCommand<DOC>> {
/// Attaches a handler capable of resolving `did:jwk` DIDs.
pub fn attach_did_jwk_handler(&mut self) {
let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) };
self.attach_handler(DIDJwk::METHOD.to_string(), handler)
}
}

#[cfg(feature = "iota")]
mod iota_handler {
use crate::ErrorCause;
Expand Down Expand Up @@ -414,4 +431,15 @@ mod tests {
let doc = resolver.resolve(&did2).await.unwrap();
assert_eq!(doc.id(), &did2);
}

#[tokio::test]
async fn test_did_jwk_resolution() {
let mut resolver = Resolver::<CoreDocument>::new();
resolver.attach_did_jwk_handler();

let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9".parse::<DIDJwk>().unwrap();

let doc = resolver.resolve(&did_jwk).await.unwrap();
assert_eq!(doc.id(), did_jwk.as_ref());
}
}
9 changes: 9 additions & 0 deletions identity_verification/src/verification_method/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use core::fmt::Display;
use core::fmt::Formatter;
use std::borrow::Cow;

use identity_did::DIDJwk;
use identity_jose::jwk::Jwk;
use serde::de;
use serde::Deserialize;
Expand Down Expand Up @@ -247,6 +248,14 @@ impl KeyComparable for VerificationMethod {
}
}

impl TryFrom<DIDJwk> for VerificationMethod {
type Error = Error;
fn try_from(did: DIDJwk) -> Result<Self, Self::Error> {
let jwk = did.jwk();
Self::new_from_jwk(did, jwk, Some("0"))
}
}

// Horrible workaround for a tracked serde issue https://github.com/serde-rs/serde/issues/2200. Serde doesn't "consume"
// the input when deserializing flattened enums (MethodData in this case) causing duplication of data (in this case
// it ends up in the properties object). This workaround simply removes the duplication.
Expand Down

0 comments on commit 98d9520

Please sign in to comment.