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.