From 02a08574a698bb2b571abe10d910608397d3943e Mon Sep 17 00:00:00 2001
From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com>
Date: Thu, 5 Sep 2024 11:46:38 +0200
Subject: [PATCH] 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
* Update identity_did/src/did_jwk.rs
Co-authored-by: wulfraem
---------
Co-authored-by: wulfraem
---
bindings/wasm/docs/api-reference.md | 347 ++++++++++++------
.../examples/src/0_basic/2_resolve_did.ts | 27 +-
bindings/wasm/src/did/did_jwk.rs | 105 ++++++
bindings/wasm/src/did/mod.rs | 2 +
bindings/wasm/src/did/wasm_core_document.rs | 7 +
identity_did/Cargo.toml | 1 +
identity_did/src/did_jwk.rs | 123 +++++++
identity_did/src/lib.rs | 2 +
.../src/document/core_document.rs | 47 +++
.../src/document/iota_document.rs | 9 +
identity_resolver/src/resolution/resolver.rs | 28 ++
.../src/verification_method/method.rs | 9 +
12 files changed, 602 insertions(+), 105 deletions(-)
create mode 100644 bindings/wasm/src/did/did_jwk.rs
create mode 100644 identity_did/src/did_jwk.rs
diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md
index db03dc07ec..6dd0837a69 100644
--- a/bindings/wasm/docs/api-reference.md
+++ b/bindings/wasm/docs/api-reference.md
@@ -14,6 +14,9 @@ if the object is being concurrently modified.
CustomMethodData
A custom verification method data format.
+DIDJwk
+did:jwk
DID.
+
DIDUrl
A method agnostic DID Url.
@@ -254,29 +257,6 @@ working with storage backed DID documents.
- PresentationProofAlgorithm
-- ProofAlgorithm
-
-- StatusCheck
-Controls validation behaviour when checking whether or not a credential has been revoked by its
-credentialStatus
.
-
-- Strict
-Validate the status if supported, reject any unsupported
-credentialStatus
types.
-Only RevocationBitmap2022
is currently supported.
-This is the default.
-
-- SkipUnsupported
-Validate the status if supported, skip any unsupported
-credentialStatus
types.
-
-- SkipAll
-Skip all status checks.
-
-- SerializationType
-
-- MethodRelationship
-
- SubjectHolderRelationship
Declares how credential subjects must relate to the presentation holder.
See also the Subject-Holder Relationship section of the specification.
@@ -291,11 +271,8 @@ This variant is the default.
- Any
The holder is not required to have any kind of relationship to any credential subject.
-- CredentialStatus
+- ProofAlgorithm
-- StatusPurpose
-Purpose of a StatusList2021.
-
- StateMetadataEncoding
- FailFast
@@ -307,12 +284,6 @@ This variant is the default.
- FirstError
Return after the first error occurs.
-- PayloadType
-
-- MethodRelationship
-
-- CredentialStatus
-
- StatusCheck
Controls validation behaviour when checking whether or not a credential has been revoked by its
credentialStatus
.
@@ -330,11 +301,28 @@ This variant is the default.
- SkipAll
Skip all status checks.
+- SerializationType
+
+- PayloadType
+
+- StatusPurpose
+Purpose of a StatusList2021.
+
+- MethodRelationship
+
+- CredentialStatus
+
## Functions
+- encodeB64(data) ⇒
string
+Encode the given bytes in url-safe base64.
+
+- decodeB64(data) ⇒
Uint8Array
+Decode the given url-safe base64-encoded slice into its raw bytes.
+
- verifyEd25519(alg, signingInput, decodedSignature, publicKey)
Verify a JWS signature secured with the EdDSA
algorithm and curve Ed25519
.
This function is useful when one is composing a IJwsVerifier
that delegates
@@ -346,15 +334,6 @@ prior to calling the function.
- start()
Initializes the console error panic hook for better error messages
-- encodeB64(data) ⇒
string
-Encode the given bytes in url-safe base64.
-
-- decodeB64(data) ⇒
Uint8Array
-Decode the given url-safe base64-encoded slice into its raw bytes.
-
-- start()
-Initializes the console error panic hook for better error messages
-
@@ -592,6 +571,7 @@ if the object is being concurrently modified.
* [.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options)](#CoreDocument+createPresentationJwt) ⇒ [Promise.<Jwt>
](#Jwt)
* _static_
* [.fromJSON(json)](#CoreDocument.fromJSON) ⇒ [CoreDocument
](#CoreDocument)
+ * [.expandDIDJwk(did)](#CoreDocument.expandDIDJwk) ⇒ [CoreDocument
](#CoreDocument)
@@ -1030,6 +1010,17 @@ Deserializes an instance from a plain JS representation.
| --- | --- |
| json | any
|
+
+
+### CoreDocument.expandDIDJwk(did) ⇒ [CoreDocument
](#CoreDocument)
+Creates a [CoreDocument](#CoreDocument) from the given [DIDJwk](#DIDJwk).
+
+**Kind**: static method of [CoreDocument
](#CoreDocument)
+
+| Param | Type |
+| --- | --- |
+| did | [DIDJwk
](#DIDJwk) |
+
## Credential
@@ -1282,6 +1273,136 @@ Deserializes an instance from a JSON object.
| --- | --- |
| json | any
|
+
+
+## DIDJwk
+`did:jwk` DID.
+
+**Kind**: global class
+
+* [DIDJwk](#DIDJwk)
+ * [new DIDJwk(did)](#new_DIDJwk_new)
+ * _instance_
+ * [.jwk()](#DIDJwk+jwk) ⇒ [Jwk
](#Jwk)
+ * [.scheme()](#DIDJwk+scheme) ⇒ string
+ * [.authority()](#DIDJwk+authority) ⇒ string
+ * [.method()](#DIDJwk+method) ⇒ string
+ * [.methodId()](#DIDJwk+methodId) ⇒ string
+ * [.toString()](#DIDJwk+toString) ⇒ string
+ * [.toCoreDid()](#DIDJwk+toCoreDid) ⇒ [CoreDID
](#CoreDID)
+ * [.toJSON()](#DIDJwk+toJSON) ⇒ any
+ * [.clone()](#DIDJwk+clone) ⇒ [DIDJwk
](#DIDJwk)
+ * _static_
+ * [.parse(input)](#DIDJwk.parse) ⇒ [DIDJwk
](#DIDJwk)
+ * [.fromJSON(json)](#DIDJwk.fromJSON) ⇒ [DIDJwk
](#DIDJwk)
+
+
+
+### new DIDJwk(did)
+Creates a new [DIDJwk](#DIDJwk) from a [CoreDID](#CoreDID).
+
+### Errors
+Throws an error if the given did is not a valid `did:jwk` DID.
+
+
+| Param | Type |
+| --- | --- |
+| did | [CoreDID
](#CoreDID) \| IToCoreDID
|
+
+
+
+### didJwk.jwk() ⇒ [Jwk
](#Jwk)
+Returns the JSON WEB KEY (JWK) encoded inside this `did:jwk`.
+
+**Kind**: instance method of [DIDJwk
](#DIDJwk)
+
+
+### didJwk.scheme() ⇒ string
+Returns the [CoreDID](#CoreDID) scheme.
+
+E.g.
+- `"did:example:12345678" -> "did"`
+- `"did:iota:smr:12345678" -> "did"`
+
+**Kind**: instance method of [DIDJwk
](#DIDJwk)
+
+
+### didJwk.authority() ⇒ string
+Returns the [CoreDID](#CoreDID) authority: the method name and method-id.
+
+E.g.
+- `"did:example:12345678" -> "example:12345678"`
+- `"did:iota:smr:12345678" -> "iota:smr:12345678"`
+
+**Kind**: instance method of [DIDJwk
](#DIDJwk)
+
+
+### didJwk.method() ⇒ string
+Returns the [CoreDID](#CoreDID) method name.
+
+E.g.
+- `"did:example:12345678" -> "example"`
+- `"did:iota:smr:12345678" -> "iota"`
+
+**Kind**: instance method of [DIDJwk
](#DIDJwk)
+
+
+### didJwk.methodId() ⇒ string
+Returns the [CoreDID](#CoreDID) method-specific ID.
+
+E.g.
+- `"did:example:12345678" -> "12345678"`
+- `"did:iota:smr:12345678" -> "smr:12345678"`
+
+**Kind**: instance method of [DIDJwk
](#DIDJwk)
+
+
+### didJwk.toString() ⇒ string
+Returns the [CoreDID](#CoreDID) as a string.
+
+**Kind**: instance method of [DIDJwk
](#DIDJwk)
+
+
+### didJwk.toCoreDid() ⇒ [CoreDID
](#CoreDID)
+**Kind**: instance method of [DIDJwk
](#DIDJwk)
+
+
+### didJwk.toJSON() ⇒ any
+Serializes this to a JSON object.
+
+**Kind**: instance method of [DIDJwk
](#DIDJwk)
+
+
+### didJwk.clone() ⇒ [DIDJwk
](#DIDJwk)
+Deep clones the object.
+
+**Kind**: instance method of [DIDJwk
](#DIDJwk)
+
+
+### DIDJwk.parse(input) ⇒ [DIDJwk
](#DIDJwk)
+Parses a [DIDJwk](#DIDJwk) from the given `input`.
+
+### Errors
+
+Throws an error if the input is not a valid [DIDJwk](#DIDJwk).
+
+**Kind**: static method of [DIDJwk
](#DIDJwk)
+
+| Param | Type |
+| --- | --- |
+| input | string
|
+
+
+
+### DIDJwk.fromJSON(json) ⇒ [DIDJwk
](#DIDJwk)
+Deserializes an instance from a JSON object.
+
+**Kind**: static method of [DIDJwk
](#DIDJwk)
+
+| Param | Type |
+| --- | --- |
+| json | any
|
+
## DIDUrl
@@ -3224,7 +3345,7 @@ Utility functions for validating JPT credentials.
### JptCredentialValidatorUtils.extractIssuer(credential) ⇒ [CoreDID
](#CoreDID)
-Utility for extracting the issuer field of a [`Credential`](`Credential`) as a DID.
+Utility for extracting the issuer field of a [Credential](#Credential) as a DID.
# Errors
Fails if the issuer field is not a valid DID.
@@ -5450,7 +5571,8 @@ Supported verification method types.
* _static_
* [.Ed25519VerificationKey2018()](#MethodType.Ed25519VerificationKey2018) ⇒ [MethodType
](#MethodType)
* [.X25519KeyAgreementKey2019()](#MethodType.X25519KeyAgreementKey2019) ⇒ [MethodType
](#MethodType)
- * [.JsonWebKey()](#MethodType.JsonWebKey) ⇒ [MethodType
](#MethodType)
+ * ~~[.JsonWebKey()](#MethodType.JsonWebKey)~~
+ * [.JsonWebKey2020()](#MethodType.JsonWebKey2020) ⇒ [MethodType
](#MethodType)
* [.custom(type_)](#MethodType.custom) ⇒ [MethodType
](#MethodType)
* [.fromJSON(json)](#MethodType.fromJSON) ⇒ [MethodType
](#MethodType)
@@ -5482,7 +5604,13 @@ Deep clones the object.
**Kind**: static method of [MethodType
](#MethodType)
-### MethodType.JsonWebKey() ⇒ [MethodType
](#MethodType)
+### ~~MethodType.JsonWebKey()~~
+***Deprecated***
+
+**Kind**: static method of [MethodType
](#MethodType)
+
+
+### MethodType.JsonWebKey2020() ⇒ [MethodType
](#MethodType)
A verification method for use with JWT verification as prescribed by the [Jwk](#Jwk)
in the `publicKeyJwk` entry.
@@ -7529,46 +7657,9 @@ Deserializes an instance from a JSON object.
| --- | --- |
| json | any
|
+
-**Kind**: global variable
-
-
-## StatusCheck
-Controls validation behaviour when checking whether or not a credential has been revoked by its
-[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status).
-
-**Kind**: global variable
-
-
-## Strict
-Validate the status if supported, reject any unsupported
-[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types.
-
-Only `RevocationBitmap2022` is currently supported.
-
-This is the default.
-
-**Kind**: global variable
-
-
-## SkipUnsupported
-Validate the status if supported, skip any unsupported
-[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types.
-
-**Kind**: global variable
-
-
-## SkipAll
-Skip all status checks.
-
-**Kind**: global variable
-
-
-## SerializationType
-**Kind**: global variable
-
-
-## MethodRelationship
+## PresentationProofAlgorithm
**Kind**: global variable
@@ -7596,7 +7687,10 @@ The holder must match the subject only for credentials where the [`nonTransferab
## Any
The holder is not required to have any kind of relationship to any credential subject.
-## StateMetadataEncoding
+**Kind**: global variable
+
+
+## ProofAlgorithm
**Kind**: global variable
@@ -7620,36 +7714,59 @@ Return all errors that occur during validation.
Return after the first error occurs.
**Kind**: global variable
+
+
+## StatusCheck
+Controls validation behaviour when checking whether or not a credential has been revoked by its
+[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status).
**Kind**: global variable
-
+
-## verifyEd25519(alg, signingInput, decodedSignature, publicKey)
-Verify a JWS signature secured with the `EdDSA` algorithm and curve `Ed25519`.
+## Strict
+Validate the status if supported, reject any unsupported
+[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types.
-This function is useful when one is composing a `IJwsVerifier` that delegates
-`EdDSA` verification with curve `Ed25519` to this function.
+Only `RevocationBitmap2022` is currently supported.
-# Warning
+This is the default.
-This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this
-prior to calling the function.
+**Kind**: global variable
+
-**Kind**: global function
+## SkipUnsupported
+Validate the status if supported, skip any unsupported
+[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types.
-| Param | Type |
-| --- | --- |
-| alg | JwsAlgorithm
|
-| signingInput | Uint8Array
|
-| decodedSignature | Uint8Array
|
-| publicKey | [Jwk
](#Jwk) |
+**Kind**: global variable
+
-
+## SkipAll
+Skip all status checks.
-## start()
-Initializes the console error panic hook for better error messages
+**Kind**: global variable
+
-**Kind**: global function
+## SerializationType
+**Kind**: global variable
+
+
+## PayloadType
+**Kind**: global variable
+
+
+## StatusPurpose
+Purpose of a [StatusList2021](#StatusList2021).
+
+**Kind**: global variable
+
+
+## MethodRelationship
+**Kind**: global variable
+
+
+## CredentialStatus
+**Kind**: global variable
## encodeB64(data) ⇒ string
@@ -7672,6 +7789,28 @@ Decode the given url-safe base64-encoded slice into its raw bytes.
| --- | --- |
| data | Uint8Array
|
+
+
+## verifyEd25519(alg, signingInput, decodedSignature, publicKey)
+Verify a JWS signature secured with the `EdDSA` algorithm and curve `Ed25519`.
+
+This function is useful when one is composing a `IJwsVerifier` that delegates
+`EdDSA` verification with curve `Ed25519` to this function.
+
+# Warning
+
+This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this
+prior to calling the function.
+
+**Kind**: global function
+
+| Param | Type |
+| --- | --- |
+| alg | JwsAlgorithm
|
+| signingInput | Uint8Array
|
+| decodedSignature | Uint8Array
|
+| publicKey | [Jwk
](#Jwk) |
+
## start()
diff --git a/bindings/wasm/examples/src/0_basic/2_resolve_did.ts b/bindings/wasm/examples/src/0_basic/2_resolve_did.ts
index 58bc205b6a..ce8ea7c3e1 100644
--- a/bindings/wasm/examples/src/0_basic/2_resolve_did.ts
+++ b/bindings/wasm/examples/src/0_basic/2_resolve_did.ts
@@ -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({
@@ -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 Promise>();
+ 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);
+};
diff --git a/bindings/wasm/src/did/did_jwk.rs b/bindings/wasm/src/did/did_jwk.rs
new file mode 100644
index 0000000000..15ce291eca
--- /dev/null
+++ b/bindings/wasm/src/did/did_jwk.rs
@@ -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 {
+ 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 {
+ 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);
diff --git a/bindings/wasm/src/did/mod.rs b/bindings/wasm/src/did/mod.rs
index b89db3edbf..ae2e89bc0c 100644
--- a/bindings/wasm/src/did/mod.rs
+++ b/bindings/wasm/src/did/mod.rs
@@ -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;
@@ -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::*;
diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs
index 0fe08e6675..2a7d896ac8 100644
--- a/bindings/wasm/src/did/wasm_core_document.rs
+++ b/bindings/wasm/src/did/wasm_core_document.rs
@@ -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;
@@ -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 {
+ CoreDocument::expand_did_jwk(did.0).wasm_result().map(Self::from)
+ }
}
#[wasm_bindgen]
diff --git a/identity_did/Cargo.toml b/identity_did/Cargo.toml
index 5b4e85069c..18b32330ca 100644
--- a/identity_did/Cargo.toml
+++ b/identity_did/Cargo.toml
@@ -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
diff --git a/identity_did/src/did_jwk.rs b/identity_did/src/did_jwk.rs
new file mode 100644
index 0000000000..5ebd61021c
--- /dev/null
+++ b/identity_did/src/did_jwk.rs
@@ -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 {
+ 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 for DIDJwk {
+ fn as_ref(&self) -> &CoreDID {
+ &self.0
+ }
+}
+
+impl From 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 {
+ 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 {
+ s.parse::().and_then(TryFrom::try_from)
+ }
+}
+
+impl From for String {
+ fn from(value: DIDJwk) -> Self {
+ value.to_string()
+ }
+}
+
+impl TryFrom for DIDJwk {
+ type Error = Error;
+ fn try_from(value: CoreDID) -> Result {
+ let Self::METHOD = value.method() else {
+ return Err(Error::InvalidMethodName);
+ };
+ decode_b64_json::(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::()?;
+ "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9".parse::()?;
+
+ 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::()
+ .is_err()
+ );
+ assert!("did:jwk:".parse::().is_err());
+ assert!("did:jwk:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp"
+ .parse::()
+ .is_err());
+ }
+}
diff --git a/identity_did/src/lib.rs b/identity_did/src/lib.rs
index 9289419211..62c846847e 100644
--- a/identity_did/src/lib.rs
+++ b/identity_did/src/lib.rs
@@ -18,6 +18,7 @@
#[allow(clippy::module_inception)]
mod did;
+mod did_jwk;
mod did_url;
mod error;
@@ -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;
diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs
index 2f6bcd593f..2747f7fae6 100644
--- a/identity_document/src/document/core_document.rs
+++ b/identity_document/src/document/core_document.rs
@@ -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;
@@ -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 {
+ 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;
@@ -1682,4 +1700,33 @@ mod tests {
verifier(json);
}
}
+
+ #[test]
+ fn test_did_jwk_expansion() {
+ let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9"
+ .parse::()
+ .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);
+ }
}
diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs
index 7ae60381d7..bd3404045c 100644
--- a/identity_iota_core/src/document/iota_document.rs
+++ b/identity_iota_core/src/document/iota_document.rs
@@ -555,6 +555,15 @@ impl From for CoreDocument {
}
}
+impl From 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
diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs
index eff86351be..228a65582b 100644
--- a/identity_resolver/src/resolution/resolver.rs
+++ b/identity_resolver/src/resolution/resolver.rs
@@ -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;
@@ -247,6 +248,22 @@ impl Resolver> {
}
}
+impl + 'static> Resolver> {
+ /// 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 + 'static> Resolver> {
+ /// 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;
@@ -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::::new();
+ resolver.attach_did_jwk_handler();
+
+ let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9".parse::().unwrap();
+
+ let doc = resolver.resolve(&did_jwk).await.unwrap();
+ assert_eq!(doc.id(), did_jwk.as_ref());
+ }
}
diff --git a/identity_verification/src/verification_method/method.rs b/identity_verification/src/verification_method/method.rs
index 65c838639f..084956c3a9 100644
--- a/identity_verification/src/verification_method/method.rs
+++ b/identity_verification/src/verification_method/method.rs
@@ -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;
@@ -247,6 +248,14 @@ impl KeyComparable for VerificationMethod {
}
}
+impl TryFrom for VerificationMethod {
+ type Error = Error;
+ fn try_from(did: DIDJwk) -> Result {
+ 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.