Skip to content

Commit

Permalink
Add support for did:jwk resolution (#1404)
Browse files Browse the repository at this point in the history
* did:jwk implementation & resolution

* did:jwk WASM bindings

* wasm did jwk test

* cargo fmt

* add missing license header

* Update identity_did/src/did_jwk.rs

Co-authored-by: wulfraem <[email protected]>

* Update identity_did/src/did_jwk.rs

Co-authored-by: wulfraem <[email protected]>

---------

Co-authored-by: wulfraem <[email protected]>
  • Loading branch information
UMR1352 and wulfraem authored Sep 5, 2024
1 parent 84f1b7e commit 02a0857
Show file tree
Hide file tree
Showing 12 changed files with 602 additions and 105 deletions.
347 changes: 243 additions & 104 deletions bindings/wasm/docs/api-reference.md

Large diffs are not rendered by default.

27 changes: 26 additions & 1 deletion bindings/wasm/examples/src/0_basic/2_resolve_did.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { IotaDocument, IotaIdentityClient, JwkMemStore, KeyIdMemStore, Storage } from "@iota/identity-wasm/node";
import {
CoreDocument,
DIDJwk,
IotaDocument,
IotaIdentityClient,
IToCoreDocument,
JwkMemStore,
KeyIdMemStore,
Resolver,
Storage,
} from "@iota/identity-wasm/node";
import { AliasOutput, Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node";
import { API_ENDPOINT, createDid } from "../util";

const DID_JWK: string =
"did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9";

/** Demonstrates how to resolve an existing DID in an Alias Output. */
export async function resolveIdentity() {
const client = new Client({
Expand Down Expand Up @@ -34,4 +47,16 @@ export async function resolveIdentity() {
// We can also resolve the Alias Output directly.
const aliasOutput: AliasOutput = await didClient.resolveDidOutput(did);
console.log("The Alias Output holds " + aliasOutput.getAmount() + " tokens");

// did:jwk can be resolved as well.
const handlers = new Map<string, (did: string) => Promise<CoreDocument | IToCoreDocument>>();
handlers.set("jwk", didJwkHandler);
const resolver = new Resolver({ handlers });
const did_jwk_resolved_doc = await resolver.resolve(DID_JWK);
console.log(`DID ${DID_JWK} resolves to:\n ${JSON.stringify(did_jwk_resolved_doc, null, 2)}`);
}

const didJwkHandler = async (did: string) => {
let did_jwk = DIDJwk.parse(did);
return CoreDocument.expandDIDJwk(did_jwk);
};
105 changes: 105 additions & 0 deletions bindings/wasm/src/did/did_jwk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2020-2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use identity_iota::did::DIDJwk;
use identity_iota::did::DID as _;
use wasm_bindgen::prelude::*;

use super::wasm_core_did::get_core_did_clone;
use super::IToCoreDID;
use super::WasmCoreDID;
use crate::error::Result;
use crate::error::WasmResult;
use crate::jose::WasmJwk;

/// `did:jwk` DID.
#[wasm_bindgen(js_name = DIDJwk)]
pub struct WasmDIDJwk(pub(crate) DIDJwk);

#[wasm_bindgen(js_class = DIDJwk)]
impl WasmDIDJwk {
#[wasm_bindgen(constructor)]
/// Creates a new {@link DIDJwk} from a {@link CoreDID}.
///
/// ### Errors
/// Throws an error if the given did is not a valid `did:jwk` DID.
pub fn new(did: IToCoreDID) -> Result<WasmDIDJwk> {
let did = get_core_did_clone(&did).0;
DIDJwk::try_from(did).wasm_result().map(Self)
}
/// Parses a {@link DIDJwk} from the given `input`.
///
/// ### Errors
///
/// Throws an error if the input is not a valid {@link DIDJwk}.
#[wasm_bindgen]
pub fn parse(input: &str) -> Result<WasmDIDJwk> {
DIDJwk::parse(input).wasm_result().map(Self)
}

/// Returns the JSON WEB KEY (JWK) encoded inside this `did:jwk`.
#[wasm_bindgen]
pub fn jwk(&self) -> WasmJwk {
self.0.jwk().into()
}

// ===========================================================================
// DID trait
// ===========================================================================

/// Returns the {@link CoreDID} scheme.
///
/// E.g.
/// - `"did:example:12345678" -> "did"`
/// - `"did:iota:smr:12345678" -> "did"`
#[wasm_bindgen]
pub fn scheme(&self) -> String {
self.0.scheme().to_owned()
}

/// Returns the {@link CoreDID} authority: the method name and method-id.
///
/// E.g.
/// - `"did:example:12345678" -> "example:12345678"`
/// - `"did:iota:smr:12345678" -> "iota:smr:12345678"`
#[wasm_bindgen]
pub fn authority(&self) -> String {
self.0.authority().to_owned()
}

/// Returns the {@link CoreDID} method name.
///
/// E.g.
/// - `"did:example:12345678" -> "example"`
/// - `"did:iota:smr:12345678" -> "iota"`
#[wasm_bindgen]
pub fn method(&self) -> String {
self.0.method().to_owned()
}

/// Returns the {@link CoreDID} method-specific ID.
///
/// E.g.
/// - `"did:example:12345678" -> "12345678"`
/// - `"did:iota:smr:12345678" -> "smr:12345678"`
#[wasm_bindgen(js_name = methodId)]
pub fn method_id(&self) -> String {
self.0.method_id().to_owned()
}

/// Returns the {@link CoreDID} as a string.
#[allow(clippy::inherent_to_string)]
#[wasm_bindgen(js_name = toString)]
pub fn to_string(&self) -> String {
self.0.to_string()
}

// Only intended to be called internally.
#[wasm_bindgen(js_name = toCoreDid, skip_typescript)]
pub fn to_core_did(&self) -> WasmCoreDID {
WasmCoreDID(self.0.clone().into())
}
}

impl_wasm_json!(WasmDIDJwk, DIDJwk);
impl_wasm_clone!(WasmDIDJwk, DIDJwk);
2 changes: 2 additions & 0 deletions bindings/wasm/src/did/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

mod did_jwk;
mod jws_verification_options;
mod service;
mod wasm_core_did;
Expand All @@ -19,5 +20,6 @@ pub use self::wasm_core_document::PromiseJws;
pub use self::wasm_core_document::PromiseJwt;
pub use self::wasm_core_document::WasmCoreDocument;
pub use self::wasm_did_url::WasmDIDUrl;
pub use did_jwk::*;

pub use self::jws_verification_options::*;
7 changes: 7 additions & 0 deletions bindings/wasm/src/did/wasm_core_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::credential::WasmJwt;
use crate::credential::WasmPresentation;
use crate::did::service::WasmService;
use crate::did::wasm_did_url::WasmDIDUrl;
use crate::did::WasmDIDJwk;
use crate::error::Result;
use crate::error::WasmResult;
use crate::jose::WasmDecodedJws;
Expand Down Expand Up @@ -765,6 +766,12 @@ impl WasmCoreDocument {
});
Ok(promise.unchecked_into())
}

/// Creates a {@link CoreDocument} from the given {@link DIDJwk}.
#[wasm_bindgen(js_name = expandDIDJwk)]
pub fn expand_did_jwk(did: WasmDIDJwk) -> Result<WasmCoreDocument> {
CoreDocument::expand_did_jwk(did.0).wasm_result().map(Self::from)
}
}

#[wasm_bindgen]
Expand Down
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
123 changes: 123 additions & 0 deletions identity_did/src/did_jwk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2020-2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

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 {
/// [`DIDJwk`]'s method.
pub const METHOD: &'static str = "jwk";

/// Tries to parse a [`DIDJwk`] 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);
}
}
Loading

0 comments on commit 02a0857

Please sign in to comment.