Skip to content

Commit 02a0857

Browse files
UMR1352wulfraem
andauthored
Add support for did:jwk resolution (#1404)
* 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 <wulfraem@users.noreply.github.com> * Update identity_did/src/did_jwk.rs Co-authored-by: wulfraem <wulfraem@users.noreply.github.com> --------- Co-authored-by: wulfraem <wulfraem@users.noreply.github.com>
1 parent 84f1b7e commit 02a0857

File tree

12 files changed

+602
-105
lines changed

12 files changed

+602
-105
lines changed

bindings/wasm/docs/api-reference.md

+243-104
Large diffs are not rendered by default.

bindings/wasm/examples/src/0_basic/2_resolve_did.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
// Copyright 2020-2023 IOTA Stiftung
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { IotaDocument, IotaIdentityClient, JwkMemStore, KeyIdMemStore, Storage } from "@iota/identity-wasm/node";
4+
import {
5+
CoreDocument,
6+
DIDJwk,
7+
IotaDocument,
8+
IotaIdentityClient,
9+
IToCoreDocument,
10+
JwkMemStore,
11+
KeyIdMemStore,
12+
Resolver,
13+
Storage,
14+
} from "@iota/identity-wasm/node";
515
import { AliasOutput, Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node";
616
import { API_ENDPOINT, createDid } from "../util";
717

18+
const DID_JWK: string =
19+
"did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9";
20+
821
/** Demonstrates how to resolve an existing DID in an Alias Output. */
922
export async function resolveIdentity() {
1023
const client = new Client({
@@ -34,4 +47,16 @@ export async function resolveIdentity() {
3447
// We can also resolve the Alias Output directly.
3548
const aliasOutput: AliasOutput = await didClient.resolveDidOutput(did);
3649
console.log("The Alias Output holds " + aliasOutput.getAmount() + " tokens");
50+
51+
// did:jwk can be resolved as well.
52+
const handlers = new Map<string, (did: string) => Promise<CoreDocument | IToCoreDocument>>();
53+
handlers.set("jwk", didJwkHandler);
54+
const resolver = new Resolver({ handlers });
55+
const did_jwk_resolved_doc = await resolver.resolve(DID_JWK);
56+
console.log(`DID ${DID_JWK} resolves to:\n ${JSON.stringify(did_jwk_resolved_doc, null, 2)}`);
3757
}
58+
59+
const didJwkHandler = async (did: string) => {
60+
let did_jwk = DIDJwk.parse(did);
61+
return CoreDocument.expandDIDJwk(did_jwk);
62+
};

bindings/wasm/src/did/did_jwk.rs

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2020-2024 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use identity_iota::did::DIDJwk;
5+
use identity_iota::did::DID as _;
6+
use wasm_bindgen::prelude::*;
7+
8+
use super::wasm_core_did::get_core_did_clone;
9+
use super::IToCoreDID;
10+
use super::WasmCoreDID;
11+
use crate::error::Result;
12+
use crate::error::WasmResult;
13+
use crate::jose::WasmJwk;
14+
15+
/// `did:jwk` DID.
16+
#[wasm_bindgen(js_name = DIDJwk)]
17+
pub struct WasmDIDJwk(pub(crate) DIDJwk);
18+
19+
#[wasm_bindgen(js_class = DIDJwk)]
20+
impl WasmDIDJwk {
21+
#[wasm_bindgen(constructor)]
22+
/// Creates a new {@link DIDJwk} from a {@link CoreDID}.
23+
///
24+
/// ### Errors
25+
/// Throws an error if the given did is not a valid `did:jwk` DID.
26+
pub fn new(did: IToCoreDID) -> Result<WasmDIDJwk> {
27+
let did = get_core_did_clone(&did).0;
28+
DIDJwk::try_from(did).wasm_result().map(Self)
29+
}
30+
/// Parses a {@link DIDJwk} from the given `input`.
31+
///
32+
/// ### Errors
33+
///
34+
/// Throws an error if the input is not a valid {@link DIDJwk}.
35+
#[wasm_bindgen]
36+
pub fn parse(input: &str) -> Result<WasmDIDJwk> {
37+
DIDJwk::parse(input).wasm_result().map(Self)
38+
}
39+
40+
/// Returns the JSON WEB KEY (JWK) encoded inside this `did:jwk`.
41+
#[wasm_bindgen]
42+
pub fn jwk(&self) -> WasmJwk {
43+
self.0.jwk().into()
44+
}
45+
46+
// ===========================================================================
47+
// DID trait
48+
// ===========================================================================
49+
50+
/// Returns the {@link CoreDID} scheme.
51+
///
52+
/// E.g.
53+
/// - `"did:example:12345678" -> "did"`
54+
/// - `"did:iota:smr:12345678" -> "did"`
55+
#[wasm_bindgen]
56+
pub fn scheme(&self) -> String {
57+
self.0.scheme().to_owned()
58+
}
59+
60+
/// Returns the {@link CoreDID} authority: the method name and method-id.
61+
///
62+
/// E.g.
63+
/// - `"did:example:12345678" -> "example:12345678"`
64+
/// - `"did:iota:smr:12345678" -> "iota:smr:12345678"`
65+
#[wasm_bindgen]
66+
pub fn authority(&self) -> String {
67+
self.0.authority().to_owned()
68+
}
69+
70+
/// Returns the {@link CoreDID} method name.
71+
///
72+
/// E.g.
73+
/// - `"did:example:12345678" -> "example"`
74+
/// - `"did:iota:smr:12345678" -> "iota"`
75+
#[wasm_bindgen]
76+
pub fn method(&self) -> String {
77+
self.0.method().to_owned()
78+
}
79+
80+
/// Returns the {@link CoreDID} method-specific ID.
81+
///
82+
/// E.g.
83+
/// - `"did:example:12345678" -> "12345678"`
84+
/// - `"did:iota:smr:12345678" -> "smr:12345678"`
85+
#[wasm_bindgen(js_name = methodId)]
86+
pub fn method_id(&self) -> String {
87+
self.0.method_id().to_owned()
88+
}
89+
90+
/// Returns the {@link CoreDID} as a string.
91+
#[allow(clippy::inherent_to_string)]
92+
#[wasm_bindgen(js_name = toString)]
93+
pub fn to_string(&self) -> String {
94+
self.0.to_string()
95+
}
96+
97+
// Only intended to be called internally.
98+
#[wasm_bindgen(js_name = toCoreDid, skip_typescript)]
99+
pub fn to_core_did(&self) -> WasmCoreDID {
100+
WasmCoreDID(self.0.clone().into())
101+
}
102+
}
103+
104+
impl_wasm_json!(WasmDIDJwk, DIDJwk);
105+
impl_wasm_clone!(WasmDIDJwk, DIDJwk);

bindings/wasm/src/did/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2020-2023 IOTA Stiftung
22
// SPDX-License-Identifier: Apache-2.0
33

4+
mod did_jwk;
45
mod jws_verification_options;
56
mod service;
67
mod wasm_core_did;
@@ -19,5 +20,6 @@ pub use self::wasm_core_document::PromiseJws;
1920
pub use self::wasm_core_document::PromiseJwt;
2021
pub use self::wasm_core_document::WasmCoreDocument;
2122
pub use self::wasm_did_url::WasmDIDUrl;
23+
pub use did_jwk::*;
2224

2325
pub use self::jws_verification_options::*;

bindings/wasm/src/did/wasm_core_document.rs

+7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use crate::credential::WasmJwt;
2424
use crate::credential::WasmPresentation;
2525
use crate::did::service::WasmService;
2626
use crate::did::wasm_did_url::WasmDIDUrl;
27+
use crate::did::WasmDIDJwk;
2728
use crate::error::Result;
2829
use crate::error::WasmResult;
2930
use crate::jose::WasmDecodedJws;
@@ -765,6 +766,12 @@ impl WasmCoreDocument {
765766
});
766767
Ok(promise.unchecked_into())
767768
}
769+
770+
/// Creates a {@link CoreDocument} from the given {@link DIDJwk}.
771+
#[wasm_bindgen(js_name = expandDIDJwk)]
772+
pub fn expand_did_jwk(did: WasmDIDJwk) -> Result<WasmCoreDocument> {
773+
CoreDocument::expand_did_jwk(did.0).wasm_result().map(Self::from)
774+
}
768775
}
769776

770777
#[wasm_bindgen]

identity_did/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ description = "Agnostic implementation of the Decentralized Identifiers (DID) st
1414
did_url_parser = { version = "0.2.0", features = ["std", "serde"] }
1515
form_urlencoded = { version = "1.2.0", default-features = false, features = ["alloc"] }
1616
identity_core = { version = "=1.3.1", path = "../identity_core" }
17+
identity_jose = { version = "=1.3.1", path = "../identity_jose" }
1718
serde.workspace = true
1819
strum.workspace = true
1920
thiserror.workspace = true

identity_did/src/did_jwk.rs

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2020-2024 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use std::fmt::Debug;
5+
use std::fmt::Display;
6+
use std::str::FromStr;
7+
8+
use identity_jose::jwk::Jwk;
9+
use identity_jose::jwu::decode_b64_json;
10+
11+
use crate::CoreDID;
12+
use crate::Error;
13+
use crate::DID;
14+
15+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)]
16+
#[repr(transparent)]
17+
#[serde(into = "CoreDID", try_from = "CoreDID")]
18+
/// A type representing a `did:jwk` DID.
19+
pub struct DIDJwk(CoreDID);
20+
21+
impl DIDJwk {
22+
/// [`DIDJwk`]'s method.
23+
pub const METHOD: &'static str = "jwk";
24+
25+
/// Tries to parse a [`DIDJwk`] from a string.
26+
pub fn parse(s: &str) -> Result<Self, Error> {
27+
s.parse()
28+
}
29+
30+
/// Returns the JWK encoded inside this did:jwk.
31+
pub fn jwk(&self) -> Jwk {
32+
decode_b64_json(self.method_id()).expect("did:jwk encodes a valid JWK")
33+
}
34+
}
35+
36+
impl AsRef<CoreDID> for DIDJwk {
37+
fn as_ref(&self) -> &CoreDID {
38+
&self.0
39+
}
40+
}
41+
42+
impl From<DIDJwk> for CoreDID {
43+
fn from(value: DIDJwk) -> Self {
44+
value.0
45+
}
46+
}
47+
48+
impl<'a> TryFrom<&'a str> for DIDJwk {
49+
type Error = Error;
50+
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
51+
value.parse()
52+
}
53+
}
54+
55+
impl Display for DIDJwk {
56+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57+
write!(f, "{}", self.0)
58+
}
59+
}
60+
61+
impl FromStr for DIDJwk {
62+
type Err = Error;
63+
fn from_str(s: &str) -> Result<Self, Self::Err> {
64+
s.parse::<CoreDID>().and_then(TryFrom::try_from)
65+
}
66+
}
67+
68+
impl From<DIDJwk> for String {
69+
fn from(value: DIDJwk) -> Self {
70+
value.to_string()
71+
}
72+
}
73+
74+
impl TryFrom<CoreDID> for DIDJwk {
75+
type Error = Error;
76+
fn try_from(value: CoreDID) -> Result<Self, Self::Error> {
77+
let Self::METHOD = value.method() else {
78+
return Err(Error::InvalidMethodName);
79+
};
80+
decode_b64_json::<Jwk>(value.method_id())
81+
.map(|_| Self(value))
82+
.map_err(|_| Error::InvalidMethodId)
83+
}
84+
}
85+
86+
#[cfg(test)]
87+
mod tests {
88+
use identity_core::convert::FromJson;
89+
90+
use super::*;
91+
92+
#[test]
93+
fn test_valid_deserialization() -> Result<(), Error> {
94+
"did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9".parse::<DIDJwk>()?;
95+
"did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9".parse::<DIDJwk>()?;
96+
97+
Ok(())
98+
}
99+
100+
#[test]
101+
fn test_jwk() {
102+
let did = DIDJwk::parse("did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9").unwrap();
103+
let target_jwk = Jwk::from_json_value(serde_json::json!({
104+
"kty":"OKP","crv":"X25519","use":"enc","x":"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08"
105+
}))
106+
.unwrap();
107+
108+
assert_eq!(did.jwk(), target_jwk);
109+
}
110+
111+
#[test]
112+
fn test_invalid_deserialization() {
113+
assert!(
114+
"did:iota:0xf4d6f08f5a1b80dd578da7dc1b49c886d580acd4cf7d48119dfeb82b538ad88a"
115+
.parse::<DIDJwk>()
116+
.is_err()
117+
);
118+
assert!("did:jwk:".parse::<DIDJwk>().is_err());
119+
assert!("did:jwk:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp"
120+
.parse::<DIDJwk>()
121+
.is_err());
122+
}
123+
}

identity_did/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
#[allow(clippy::module_inception)]
2020
mod did;
21+
mod did_jwk;
2122
mod did_url;
2223
mod error;
2324

@@ -26,4 +27,5 @@ pub use crate::did_url::RelativeDIDUrl;
2627
pub use ::did_url_parser::DID as BaseDIDUrl;
2728
pub use did::CoreDID;
2829
pub use did::DID;
30+
pub use did_jwk::*;
2931
pub use error::Error;

identity_document/src/document/core_document.rs

+47
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use core::fmt::Formatter;
77
use std::collections::HashMap;
88
use std::convert::Infallible;
99

10+
use identity_did::DIDJwk;
1011
use identity_verification::jose::jwk::Jwk;
1112
use identity_verification::jose::jws::DecodedJws;
1213
use identity_verification::jose::jws::Decoder;
@@ -984,6 +985,23 @@ impl CoreDocument {
984985
}
985986
}
986987

988+
impl CoreDocument {
989+
/// Creates a [`CoreDocument`] from a did:jwk DID.
990+
pub fn expand_did_jwk(did_jwk: DIDJwk) -> Result<Self, Error> {
991+
let verification_method = VerificationMethod::try_from(did_jwk.clone()).map_err(Error::InvalidKeyMaterial)?;
992+
let verification_method_id = verification_method.id().clone();
993+
994+
DocumentBuilder::default()
995+
.id(did_jwk.into())
996+
.verification_method(verification_method)
997+
.assertion_method(verification_method_id.clone())
998+
.authentication(verification_method_id.clone())
999+
.capability_invocation(verification_method_id.clone())
1000+
.capability_delegation(verification_method_id.clone())
1001+
.build()
1002+
}
1003+
}
1004+
9871005
#[cfg(test)]
9881006
mod tests {
9891007
use identity_core::convert::FromJson;
@@ -1682,4 +1700,33 @@ mod tests {
16821700
verifier(json);
16831701
}
16841702
}
1703+
1704+
#[test]
1705+
fn test_did_jwk_expansion() {
1706+
let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9"
1707+
.parse::<DIDJwk>()
1708+
.unwrap();
1709+
let target_doc = serde_json::from_value(serde_json::json!({
1710+
"id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9",
1711+
"verificationMethod": [
1712+
{
1713+
"id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0",
1714+
"type": "JsonWebKey2020",
1715+
"controller": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9",
1716+
"publicKeyJwk": {
1717+
"kty":"OKP",
1718+
"crv":"X25519",
1719+
"use":"enc",
1720+
"x":"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08"
1721+
}
1722+
}
1723+
],
1724+
"assertionMethod": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"],
1725+
"authentication": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"],
1726+
"capabilityInvocation": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"],
1727+
"capabilityDelegation": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"]
1728+
})).unwrap();
1729+
1730+
assert_eq!(CoreDocument::expand_did_jwk(did_jwk).unwrap(), target_doc);
1731+
}
16851732
}

0 commit comments

Comments
 (0)