From 63e2d25762a58c8c5007a2d90ad15ff470aa201f Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 20 Sep 2023 10:00:28 +0200 Subject: [PATCH 01/10] Use thumbprint as `kid` on `JWK` --- identity_storage/src/key_storage/memstore.rs | 5 ++--- identity_storage/src/key_storage/stronghold.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index 2b2fa36d4a..1f7edb217f 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -72,9 +72,8 @@ impl JwkStorage for JwkMemStore { let mut jwk: Jwk = encode_jwk(&private_key, &public_key); jwk.set_alg(alg.name()); - // Unwrapping is OK because the None variant only occurs for kty = oct. - let mut public_jwk: Jwk = jwk.to_public().unwrap(); - public_jwk.set_kid(kid.clone()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + let mut public_jwk: Jwk = jwk.to_public().expect("should only panic if kty == oct"); let mut jwk_store: RwLockWriteGuard<'_, JwkKeyStore> = self.jwk_store.write().await; jwk_store.insert(kid.clone(), jwk); diff --git a/identity_storage/src/key_storage/stronghold.rs b/identity_storage/src/key_storage/stronghold.rs index 02ee182e49..7e56d3a13b 100644 --- a/identity_storage/src/key_storage/stronghold.rs +++ b/identity_storage/src/key_storage/stronghold.rs @@ -91,7 +91,7 @@ impl JwkStorage for StrongholdStorage { params.crv = EdCurve::Ed25519.name().to_owned(); let mut jwk: Jwk = Jwk::from_params(params); jwk.set_alg(alg.name()); - jwk.set_kid(key_id.clone()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); Ok(JwkGenOutput { key_id, jwk }) } From 1529fc0db53f1f4d8e1abb3d9be71e86caccc046 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 20 Sep 2023 10:39:48 +0200 Subject: [PATCH 02/10] Allow setting kid and method id for verification --- .../credential_jwt_validator.rs | 37 ++++++++++--------- .../verifiable/jws_verification_options.rs | 10 +++++ identity_storage/src/key_storage/memstore.rs | 2 +- .../src/storage/jwk_document_ext.rs | 11 ++++-- .../src/storage/signature_options.rs | 14 +++++++ .../src/storage/tests/credential_jws.rs | 31 ++++++++++++++++ 6 files changed, 84 insertions(+), 21 deletions(-) diff --git a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs index d3c3cef053..2368f1e6e9 100644 --- a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs +++ b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs @@ -223,24 +223,27 @@ where )); } - // Parse the `kid` to a DID Url which should be the identifier of a verification method in a trusted issuer's DID - // document. - let method_id: DIDUrl = { - let kid: &str = decoded.protected_header().and_then(|header| header.kid()).ok_or( - JwtValidationError::MethodDataLookupError { - source: None, - message: "could not extract kid from protected header", + // If no method_url is set, parse the `kid` to a DID Url which should be the identifier + // of a verification method in a trusted issuer's DID document. + let method_id: DIDUrl = match &options.method_id { + Some(method_id) => method_id.clone(), + None => { + let kid: &str = decoded.protected_header().and_then(|header| header.kid()).ok_or( + JwtValidationError::MethodDataLookupError { + source: None, + message: "could not extract kid from protected header", + signer_ctx: SignerContext::Issuer, + }, + )?; + + // Convert kid to DIDUrl + DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError { + source: Some(err.into()), + message: "could not parse kid as a DID Url", signer_ctx: SignerContext::Issuer, - }, - )?; - - // Convert kid to DIDUrl - DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError { - source: Some(err.into()), - message: "could not parse kid as a DID Url", - signer_ctx: SignerContext::Issuer, - }) - }?; + })? + } + }; // locate the corresponding issuer let issuer: &CoreDocument = trusted_issuers diff --git a/identity_document/src/verifiable/jws_verification_options.rs b/identity_document/src/verifiable/jws_verification_options.rs index 99690bcc99..a51492578a 100644 --- a/identity_document/src/verifiable/jws_verification_options.rs +++ b/identity_document/src/verifiable/jws_verification_options.rs @@ -1,6 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use identity_did::DIDUrl; use identity_verification::MethodScope; /// Holds additional options for verifying a JWS with @@ -15,6 +16,9 @@ pub struct JwsVerificationOptions { pub nonce: Option, /// Verify the signing verification method relation matches this. pub method_scope: Option, + /// The DID URl of the method, whose JWK should be used to verify the JWS. + /// If unset, the `kid` of the JWS is used as the DID Url. + pub method_id: Option, } impl JwsVerificationOptions { @@ -34,4 +38,10 @@ impl JwsVerificationOptions { self.method_scope = Some(value); self } + + /// The DID URl of the method, whose JWK should be used to verify the JWS. + pub fn method_id(mut self, value: DIDUrl) -> Self { + self.method_id = Some(value); + self + } } diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index 1f7edb217f..7dd92fc3c2 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -73,7 +73,7 @@ impl JwkStorage for JwkMemStore { let mut jwk: Jwk = encode_jwk(&private_key, &public_key); jwk.set_alg(alg.name()); jwk.set_kid(jwk.thumbprint_sha256_b64()); - let mut public_jwk: Jwk = jwk.to_public().expect("should only panic if kty == oct"); + let public_jwk: Jwk = jwk.to_public().expect("should only panic if kty == oct"); let mut jwk_store: RwLockWriteGuard<'_, JwkKeyStore> = self.jwk_store.write().await; jwk_store.insert(kid.clone(), jwk); diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index 62ade8a3d1..00e53a0069 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -97,8 +97,9 @@ pub trait JwkDocumentExt: private::Sealed { /// Produces a JWT where the payload is produced from the given `credential` /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). /// - /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be - /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. async fn create_credential_jwt( &self, credential: &Credential, @@ -357,7 +358,11 @@ impl JwkDocumentExt for CoreDocument { header.set_alg(alg); - header.set_kid(method.id().to_string()); + if let Some(ref kid) = options.kid { + header.set_kid(kid.clone()); + } else { + header.set_kid(method.id().to_string()); + } if options.attach_jwk { header.set_jwk(jwk.clone()) diff --git a/identity_storage/src/storage/signature_options.rs b/identity_storage/src/storage/signature_options.rs index 63a5da3ab8..b7af074015 100644 --- a/identity_storage/src/storage/signature_options.rs +++ b/identity_storage/src/storage/signature_options.rs @@ -43,6 +43,14 @@ pub struct JwsSignatureOptions { #[serde(skip_serializing_if = "Option::is_none")] pub nonce: Option, + /// The kid to set in the protected header. + /// + /// If unset, the kid of the JWK with which the JWS is produced is used. + /// + /// [More Info](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.4) + #[serde(skip_serializing_if = "Option::is_none")] + pub kid: Option, + /// Whether the payload should be detached from the JWS. /// /// [More Info](https://www.rfc-editor.org/rfc/rfc7515#appendix-F). @@ -91,6 +99,12 @@ impl JwsSignatureOptions { self } + /// Replace the value of the `kid` field. + pub fn kid(mut self, value: impl Into) -> Self { + self.kid = Some(value.into()); + self + } + /// Replace the value of the `detached_payload` field. pub fn detached_payload(mut self, value: bool) -> Self { self.detached_payload = value; diff --git a/identity_storage/src/storage/tests/credential_jws.rs b/identity_storage/src/storage/tests/credential_jws.rs index 862b2b8ec9..49ffa164cc 100644 --- a/identity_storage/src/storage/tests/credential_jws.rs +++ b/identity_storage/src/storage/tests/credential_jws.rs @@ -6,6 +6,7 @@ use identity_core::convert::FromJson; use identity_credential::credential::Credential; use identity_credential::validator::JwtCredentialValidationOptions; +use identity_did::DID; use identity_document::document::CoreDocument; use identity_document::verifiable::JwsVerificationOptions; use identity_verification::jose::jws::JwsAlgorithm; @@ -186,3 +187,33 @@ async fn signing_credential_with_b64() { .await .is_err()); } + +#[tokio::test] +async fn signing_credential_with_custom_kid() { + let (document, storage, fragment, credential) = setup().await; + + let my_kid = "my-kid"; + let jws = document + .create_credential_jwt( + &credential, + &storage, + fragment.as_ref(), + &JwsSignatureOptions::default().kid(my_kid), + ) + .await + .unwrap(); + + let validator = identity_credential::validator::JwtCredentialValidator::new(); + let method_id = document.id().clone().join(format!("#{fragment}")).unwrap(); + let decoded = validator + .validate::<_, Object>( + &jws, + &document, + &JwtCredentialValidationOptions::default() + .verification_options(JwsVerificationOptions::new().method_id(method_id)), + identity_credential::validator::FailFast::FirstError, + ) + .unwrap(); + + assert_eq!(decoded.header.kid().unwrap(), my_kid); +} From 409d9d136a347776c811b72f73001727ef0eb825 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 20 Sep 2023 11:05:03 +0200 Subject: [PATCH 03/10] Add options in Wasm --- bindings/wasm/src/common/timestamp.rs | 7 ++++ .../wasm/src/did/jws_verification_options.rs | 24 ++++++++----- .../wasm/src/storage/signature_options.rs | 13 +++++++ bindings/wasm/tests/credentials.ts | 3 -- bindings/wasm/tests/storage.ts | 36 +++++++++++++++++++ 5 files changed, 71 insertions(+), 12 deletions(-) diff --git a/bindings/wasm/src/common/timestamp.rs b/bindings/wasm/src/common/timestamp.rs index 9421d4f065..a6337d91b7 100644 --- a/bindings/wasm/src/common/timestamp.rs +++ b/bindings/wasm/src/common/timestamp.rs @@ -18,7 +18,14 @@ extern "C" { pub struct WasmTimestamp(pub(crate) Timestamp); #[wasm_bindgen(js_class = Timestamp)] +#[allow(clippy::new_without_default)] impl WasmTimestamp { + /// Creates a new {@link Timestamp} with the current date and time. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self::now_utc() + } + /// Parses a {@link Timestamp} from the provided input string. #[wasm_bindgen] pub fn parse(input: &str) -> Result { diff --git a/bindings/wasm/src/did/jws_verification_options.rs b/bindings/wasm/src/did/jws_verification_options.rs index 1ed292ddb4..32d83b4087 100644 --- a/bindings/wasm/src/did/jws_verification_options.rs +++ b/bindings/wasm/src/did/jws_verification_options.rs @@ -7,6 +7,8 @@ use crate::verification::WasmMethodScope; use identity_iota::document::verifiable::JwsVerificationOptions; use wasm_bindgen::prelude::*; +use super::WasmDIDUrl; + #[wasm_bindgen(js_name = JwsVerificationOptions, inspectable)] pub struct WasmJwsVerificationOptions(pub(crate) JwsVerificationOptions); @@ -30,10 +32,16 @@ impl WasmJwsVerificationOptions { } /// Set the scope of the verification methods that may be used to verify the given JWS. - #[wasm_bindgen(js_name = setScope)] - pub fn set_scope(&mut self, value: &WasmMethodScope) { + #[wasm_bindgen(js_name = setMethodScope)] + pub fn set_method_scope(&mut self, value: &WasmMethodScope) { self.0.method_scope = Some(value.0); } + + /// Set the DID URl of the method, whose JWK should be used to verify the JWS. + #[wasm_bindgen(js_name = setMethodId)] + pub fn set_method_id(&mut self, value: &WasmDIDUrl) { + self.0.method_id = Some(value.0.clone()); + } } impl_wasm_json!(WasmJwsVerificationOptions, JwsVerificationOptions); @@ -50,13 +58,6 @@ extern "C" { const I_JWS_SIGNATURE_OPTIONS: &'static str = r#" /** Holds options to create {@link JwsVerificationOptions}. */ interface IJwsVerificationOptions { - /** - * A list of permitted extension parameters. - * - * [More info](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.11) - */ - readonly crits?: [string]; - /** Verify that the `nonce` set in the protected header matches this. * * [More Info](https://tools.ietf.org/html/rfc8555#section-6.5.2) @@ -65,4 +66,9 @@ interface IJwsVerificationOptions { /** Verify the signing verification method relationship matches this.*/ readonly methodScope?: MethodScope; + + /** The DID URl of the method, whose JWK should be used to verify the JWS. + * If unset, the `kid` of the JWS is used as the DID Url. + */ + readonly methodId?: string; }"#; diff --git a/bindings/wasm/src/storage/signature_options.rs b/bindings/wasm/src/storage/signature_options.rs index 239e797593..f1cfc9e3a6 100644 --- a/bindings/wasm/src/storage/signature_options.rs +++ b/bindings/wasm/src/storage/signature_options.rs @@ -59,6 +59,12 @@ impl WasmJwsSignatureOptions { self.0.nonce = Some(value); } + /// Replace the value of the `kid` field. + #[wasm_bindgen(js_name = setKid)] + pub fn set_kid(&mut self, value: String) { + self.0.kid = Some(value); + } + /// Replace the value of the `detached_payload` field. #[wasm_bindgen(js_name = setDetachedPayload)] pub fn set_detached_payload(&mut self, value: bool) { @@ -117,6 +123,13 @@ interface IJwsSignatureOptions { */ readonly nonce?: string; + /** The kid to set in the protected header. + * If unset, the kid of the JWK with which the JWS is produced is used. + * + * [More Info](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.4) + */ + readonly kid?: string; + /** /// Whether the payload should be detached from the JWS. * * [More Info](https://www.rfc-editor.org/rfc/rfc7515#appendix-F). diff --git a/bindings/wasm/tests/credentials.ts b/bindings/wasm/tests/credentials.ts index 92f240dd0a..56cd6c790c 100644 --- a/bindings/wasm/tests/credentials.ts +++ b/bindings/wasm/tests/credentials.ts @@ -15,9 +15,6 @@ import { Timestamp, UnknownCredential, } from "../node"; -export {}; - -// const assert = require("assert"); const credentialFields = { context: "https://www.w3.org/2018/credentials/examples/v1", diff --git a/bindings/wasm/tests/storage.ts b/bindings/wasm/tests/storage.ts index 7a3f39a6b6..6a6fffa5e6 100644 --- a/bindings/wasm/tests/storage.ts +++ b/bindings/wasm/tests/storage.ts @@ -22,7 +22,9 @@ import { MethodDigest, MethodScope, Presentation, + StatusCheck, Storage, + SubjectHolderRelationship, Timestamp, VerificationMethod, verifyEdDSA, @@ -422,3 +424,37 @@ describe("#JwkStorageDocument", function() { } } }); + +describe("#OptionParsing", function() { + it("JwsSignatureOptions can be parsed", () => { + new JwsSignatureOptions({ + nonce: "nonce", + attachJwk: true, + b64: true, + cty: "type", + detachedPayload: false, + kid: "kid", + typ: "typ", + url: "https://www.example.com", + }); + }), + it("JwsVerificationOptions can be parsed", () => { + new JwsVerificationOptions({ + nonce: "nonce", + methodId: "did:iota:0x123", + methodScope: MethodScope.AssertionMethod(), + }); + }), + it("JwtCredentialValidationOptions can be parsed", () => { + new JwtCredentialValidationOptions({ + // These are equivalent ways of creating a timestamp. + earliestExpiryDate: new Timestamp(), + latestIssuanceDate: Timestamp.nowUTC(), + status: StatusCheck.SkipAll, + subjectHolderRelationship: ["did:iota:0x123", SubjectHolderRelationship.SubjectOnNonTransferable], + verifierOptions: new JwsVerificationOptions({ + nonce: "nonce", + }), + }); + }); +}); From a9d20b70850150e5020e80dafadb0a7e00702985 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 20 Sep 2023 11:13:39 +0200 Subject: [PATCH 04/10] Update documentation --- bindings/wasm/src/did/wasm_core_document.rs | 13 ++++++++----- identity_document/src/document/core_document.rs | 3 ++- identity_storage/src/storage/jwk_document_ext.rs | 5 +++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index e0ebd778b9..217241f702 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -478,7 +478,8 @@ impl WasmCoreDocument { /// Regardless of which options are passed the following conditions must be met in order for a verification attempt to /// take place. /// - The JWS must be encoded according to the JWS compact serialization. - /// - The `kid` value in the protected header must be an identifier of a verification method in this DID document. + /// - The `kid` value in the protected header must be an identifier of a verification method in this DID document, + /// or set explicitly in the `options`. #[wasm_bindgen(js_name = verifyJws)] #[allow(non_snake_case)] pub fn verify_jws( @@ -668,8 +669,9 @@ impl WasmCoreDocument { /// Produces a JWT where the payload is produced from the given `credential` /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). /// - /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be - /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. #[wasm_bindgen(js_name = createCredentialJwt)] pub fn create_credential_jwt( &self, @@ -698,8 +700,9 @@ impl WasmCoreDocument { /// Produces a JWT where the payload is produced from the given presentation. /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). /// - /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be - /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. #[wasm_bindgen(js_name = createPresentationJwt)] pub fn create_presentation_jwt( &self, diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 128140763e..ba595221d7 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -938,7 +938,8 @@ impl CoreDocument { /// Regardless of which options are passed the following conditions must be met in order for a verification attempt to /// take place. /// - The JWS must be encoded according to the JWS compact serialization. - /// - The `kid` value in the protected header must be an identifier of a verification method in this DID document. + /// - The `kid` value in the protected header must be an identifier of a verification method in this DID document, + /// or set explicitly in the `options`. // // NOTE: This is tested in `identity_storage` and `identity_credential`. pub fn verify_jws<'jws, T: JwsVerifier>( diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index 00e53a0069..3ec0506c19 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -115,8 +115,9 @@ pub trait JwkDocumentExt: private::Sealed { /// Produces a JWT where the payload is produced from the given `presentation` /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). /// - /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be - /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. async fn create_presentation_jwt( &self, presentation: &Presentation, From 25bd99af134be86d1e6866c7dc56be6f6ed6edb1 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 20 Sep 2023 13:43:23 +0200 Subject: [PATCH 05/10] Format --- identity_storage/src/storage/jwk_document_ext.rs | 2 +- identity_storage/src/storage/tests/credential_jws.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index d44ec8643a..2ce1d94cad 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -101,7 +101,7 @@ pub trait JwkDocumentExt: private::Sealed { /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding /// private key backed by the `storage` in accordance with the passed `options`. - /// + /// /// The `custom_claims` can be used to set additional claims on the resulting JWT. async fn create_credential_jwt( &self, diff --git a/identity_storage/src/storage/tests/credential_jws.rs b/identity_storage/src/storage/tests/credential_jws.rs index f7589a1a05..d82f5bb43a 100644 --- a/identity_storage/src/storage/tests/credential_jws.rs +++ b/identity_storage/src/storage/tests/credential_jws.rs @@ -223,7 +223,6 @@ async fn signing_credential_with_custom_kid() { assert_eq!(decoded.header.kid().unwrap(), my_kid); } - #[tokio::test] async fn custom_claims() { let (document, storage, kid, credential) = setup().await; @@ -254,4 +253,4 @@ async fn custom_claims() { ) .unwrap(); assert_eq!(decoded.custom_claims.unwrap(), custom_claims); -} \ No newline at end of file +} From dbd03e3ce9f6d603708373454acc3e6ed1dc7f91 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 20 Sep 2023 13:46:23 +0200 Subject: [PATCH 06/10] Add missing documentation --- bindings/wasm/src/did/wasm_core_document.rs | 2 ++ bindings/wasm/src/iota/iota_document.rs | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index 8f3234fdd9..0d75b7abc8 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -673,6 +673,8 @@ impl WasmCoreDocument { /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding /// private key backed by the `storage` in accordance with the passed `options`. + /// + /// The `custom_claims` can be used to set additional claims on the resulting JWT. #[wasm_bindgen(js_name = createCredentialJwt)] pub fn create_credential_jwt( &self, diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index b482091b48..e165769d7a 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -716,8 +716,11 @@ impl WasmIotaDocument { /// Produces a JWS where the payload is produced from the given `credential` /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). /// - /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be - /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. + /// + /// The `custom_claims` can be used to set additional claims on the resulting JWT. #[wasm_bindgen(js_name = createCredentialJwt)] pub fn create_credential_jwt( &self, @@ -750,8 +753,9 @@ impl WasmIotaDocument { /// Produces a JWT where the payload is produced from the given presentation. /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). /// - /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be - /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. #[wasm_bindgen(js_name = createPresentationJwt)] pub fn create_presentation_jwt( &self, From d3f3af3c5737a85583c7165b0e40bce80b5ccf98 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 21 Sep 2023 13:54:22 +0200 Subject: [PATCH 07/10] Fix methodId type, respect methodId in verify_jws --- bindings/wasm/src/did/jws_verification_options.rs | 2 +- bindings/wasm/tests/credentials.ts | 14 ++++++++++++-- identity_document/src/document/core_document.rs | 14 ++++++++++---- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/bindings/wasm/src/did/jws_verification_options.rs b/bindings/wasm/src/did/jws_verification_options.rs index 32d83b4087..596fc05a2a 100644 --- a/bindings/wasm/src/did/jws_verification_options.rs +++ b/bindings/wasm/src/did/jws_verification_options.rs @@ -70,5 +70,5 @@ interface IJwsVerificationOptions { /** The DID URl of the method, whose JWK should be used to verify the JWS. * If unset, the `kid` of the JWS is used as the DID Url. */ - readonly methodId?: string; + readonly methodId?: DIDUrl; }"#; diff --git a/bindings/wasm/tests/credentials.ts b/bindings/wasm/tests/credentials.ts index 56cd6c790c..4fbbc8eaed 100644 --- a/bindings/wasm/tests/credentials.ts +++ b/bindings/wasm/tests/credentials.ts @@ -5,6 +5,7 @@ import { JwkMemStore, JwsAlgorithm, JwsSignatureOptions, + JwsVerificationOptions, JwtPresentationOptions, JwtPresentationValidationOptions, JwtPresentationValidator, @@ -237,22 +238,31 @@ describe("Presentation", function() { verifiableCredential: [credentialJwt, unsignedVc, otherCredential], }); + const myKid = "my-kid"; const presentationJwt = await doc.createPresentationJwt( storage, fragment, unsignedVp, - new JwsSignatureOptions(), + new JwsSignatureOptions({ + kid: myKid, + }), new JwtPresentationOptions(), ); let issuer = JwtPresentationValidator.extractHolder(presentationJwt); assert.deepStrictEqual(issuer.toString(), doc.id().toString()); + const methodId = doc.id().join(fragment); const decodedPresentation = new JwtPresentationValidator().validate( presentationJwt, doc, - new JwtPresentationValidationOptions(), + new JwtPresentationValidationOptions({ + presentationVerifierOptions: new JwsVerificationOptions({ + methodId: methodId, + }), + }), ); + assert.deepStrictEqual(decodedPresentation.protectedHeader().kid(), myKid); const credentials: UnknownCredential[] = decodedPresentation .presentation() diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index ba595221d7..b079111ed4 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -961,12 +961,18 @@ impl CoreDocument { )); } - let kid = validation_item.kid().ok_or(Error::JwsVerificationError( - identity_verification::jose::error::Error::InvalidParam("missing kid value"), - ))?; + let method_url_query: DIDUrlQuery<'_> = match &options.method_id { + Some(method_id) => method_id.into(), + None => validation_item + .kid() + .ok_or(Error::JwsVerificationError( + identity_verification::jose::error::Error::InvalidParam("missing kid value"), + ))? + .into(), + }; let public_key: &Jwk = self - .resolve_method(kid, options.method_scope) + .resolve_method(method_url_query, options.method_scope) .ok_or(Error::MethodNotFound)? .data() .try_public_key_jwk() From a986ad3eb42a53df9110ee52c423bccd1736fffc Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 21 Sep 2023 14:03:56 +0200 Subject: [PATCH 08/10] Add verify_jws test --- identity_storage/src/storage/tests/api.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/identity_storage/src/storage/tests/api.rs b/identity_storage/src/storage/tests/api.rs index 2f477ffc98..2af2da4ef8 100644 --- a/identity_storage/src/storage/tests/api.rs +++ b/identity_storage/src/storage/tests/api.rs @@ -8,6 +8,7 @@ use identity_credential::credential::Credential; use identity_credential::credential::Jws; use identity_credential::validator::JwtCredentialValidationOptions; use identity_did::DIDUrl; +use identity_did::DID; use identity_document::document::CoreDocument; use identity_document::verifiable::JwsVerificationOptions; use identity_verification::jose::jws::EdDSAJwsVerifier; @@ -274,6 +275,28 @@ async fn create_jws_detached() { .is_ok()); } +#[tokio::test] +async fn create_jws_with_custom_kid() { + let (document, storage, fragment) = setup_with_method().await; + + let payload: &[u8] = b"test"; + let key_id: &str = "my-key-id"; + let signature_options: JwsSignatureOptions = JwsSignatureOptions::new().kid(key_id); + let verification_options: JwsVerificationOptions = + JwsVerificationOptions::new().method_id(document.id().clone().join(format!("#{fragment}")).unwrap()); + + let jws: Jws = document + .create_jws(&storage, &fragment, payload, &signature_options) + .await + .unwrap(); + + let decoded = document + .verify_jws(jws.as_str(), None, &EdDSAJwsVerifier::default(), &verification_options) + .unwrap(); + + assert_eq!(decoded.protected.kid().unwrap(), key_id); +} + #[tokio::test] async fn signing_credential() { let (mut document, storage) = setup(); From 3d9ff2d60e4203af1226ab6183579b8dda60cb8d Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 21 Sep 2023 16:34:22 +0200 Subject: [PATCH 09/10] Update bindings/wasm/src/did/jws_verification_options.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Eike Haß --- bindings/wasm/src/did/jws_verification_options.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/wasm/src/did/jws_verification_options.rs b/bindings/wasm/src/did/jws_verification_options.rs index 596fc05a2a..5ac1aed252 100644 --- a/bindings/wasm/src/did/jws_verification_options.rs +++ b/bindings/wasm/src/did/jws_verification_options.rs @@ -67,7 +67,7 @@ interface IJwsVerificationOptions { /** Verify the signing verification method relationship matches this.*/ readonly methodScope?: MethodScope; - /** The DID URl of the method, whose JWK should be used to verify the JWS. + /** The DID URL of the method, whose JWK should be used to verify the JWS. * If unset, the `kid` of the JWS is used as the DID Url. */ readonly methodId?: DIDUrl; From 4e430c9821f365d54d6a88e01fa4f574186ef132 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 22 Sep 2023 11:45:37 +0200 Subject: [PATCH 10/10] Fix post-merge issue --- identity_storage/src/storage/tests/credential_jws.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/identity_storage/src/storage/tests/credential_jws.rs b/identity_storage/src/storage/tests/credential_jws.rs index a58cee62d3..6348ba70a7 100644 --- a/identity_storage/src/storage/tests/credential_jws.rs +++ b/identity_storage/src/storage/tests/credential_jws.rs @@ -211,7 +211,8 @@ async fn signing_credential_with_custom_kid() { .await .unwrap(); - let validator = identity_credential::validator::JwtCredentialValidator::new(); + let validator = + identity_credential::validator::JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); let method_id = document.id().clone().join(format!("#{fragment}")).unwrap(); let decoded = validator .validate::<_, Object>(