Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for did:jwk resolution #1404

Merged
merged 7 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
wulfraem marked this conversation as resolved.
Show resolved Hide resolved
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
Loading