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..5ac1aed252 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?: DIDUrl; }"#; diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index 29f56094cc..bd349b7723 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -479,7 +479,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( @@ -669,8 +670,11 @@ 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`. + /// + /// 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, @@ -703,8 +707,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/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index 2f826232f5..883986981e 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, 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 dfe4e5ed79..10eb4c85e8 100644 --- a/bindings/wasm/tests/credentials.ts +++ b/bindings/wasm/tests/credentials.ts @@ -6,6 +6,7 @@ import { JwkMemStore, JwsAlgorithm, JwsSignatureOptions, + JwsVerificationOptions, JwtPresentationOptions, JwtPresentationValidationOptions, JwtPresentationValidator, @@ -16,9 +17,6 @@ import { Timestamp, UnknownCredential, } from "../node"; -export {}; - -// const assert = require("assert"); const credentialFields = { context: "https://www.w3.org/2018/credentials/examples/v1", @@ -241,22 +239,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(new EdDSAJwsVerifier()).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/bindings/wasm/tests/storage.ts b/bindings/wasm/tests/storage.ts index 347eb42a58..ac78e490ec 100644 --- a/bindings/wasm/tests/storage.ts +++ b/bindings/wasm/tests/storage.ts @@ -23,7 +23,9 @@ import { MethodDigest, MethodScope, Presentation, + StatusCheck, Storage, + SubjectHolderRelationship, Timestamp, VerificationMethod, } from "../node"; @@ -424,3 +426,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", + }), + }); + }); +}); diff --git a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator.rs b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator.rs index 40ca2138e4..e10009ba85 100644 --- a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator.rs +++ b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator.rs @@ -211,24 +211,27 @@ impl JwtCredentialValidator { )); } - // 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/document/core_document.rs b/identity_document/src/document/core_document.rs index 128140763e..b079111ed4 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>( @@ -960,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() 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 2b2fa36d4a..7dd92fc3c2 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 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 }) } diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index 31d3c0e6a4..2ce1d94cad 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -98,8 +98,10 @@ 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`. + /// /// The `custom_claims` can be used to set additional claims on the resulting JWT. async fn create_credential_jwt( &self, @@ -117,8 +119,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, @@ -360,7 +363,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/api.rs b/identity_storage/src/storage/tests/api.rs index e442755ad7..681aa0baf5 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_eddsa_verifier::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(); diff --git a/identity_storage/src/storage/tests/credential_jws.rs b/identity_storage/src/storage/tests/credential_jws.rs index 750733c776..6348ba70a7 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_eddsa_verifier::EdDSAJwsVerifier; @@ -194,6 +195,38 @@ async fn signing_credential_with_b64() { .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), + None, + ) + .await + .unwrap(); + + 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>( + &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); +} + #[tokio::test] async fn custom_claims() { let (document, storage, kid, credential) = setup().await;