Skip to content

Commit

Permalink
Allow custom kid to be set in JWS (#1239)
Browse files Browse the repository at this point in the history
* Use thumbprint as `kid` on `JWK`

* Allow setting kid and method id for verification

* Add options in Wasm

* Update documentation

* Format

* Add missing documentation

* Fix methodId type, respect methodId in verify_jws

* Add verify_jws test

* Update bindings/wasm/src/did/jws_verification_options.rs

Co-authored-by: Eike Haß <[email protected]>

* Fix post-merge issue

---------

Co-authored-by: Eike Haß <[email protected]>
  • Loading branch information
PhilippGackstatter and eike-hass authored Sep 22, 2023
1 parent 5b9911b commit e434b02
Show file tree
Hide file tree
Showing 16 changed files with 228 additions and 54 deletions.
7 changes: 7 additions & 0 deletions bindings/wasm/src/common/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<WasmTimestamp> {
Expand Down
24 changes: 15 additions & 9 deletions bindings/wasm/src/did/jws_verification_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
Expand All @@ -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)
Expand All @@ -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;
}"#;
15 changes: 10 additions & 5 deletions bindings/wasm/src/did/wasm_core_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 8 additions & 4 deletions bindings/wasm/src/iota/iota_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions bindings/wasm/src/storage/signature_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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).
Expand Down
17 changes: 12 additions & 5 deletions bindings/wasm/tests/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
JwkMemStore,
JwsAlgorithm,
JwsSignatureOptions,
JwsVerificationOptions,
JwtPresentationOptions,
JwtPresentationValidationOptions,
JwtPresentationValidator,
Expand All @@ -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",
Expand Down Expand Up @@ -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()
Expand Down
36 changes: 36 additions & 0 deletions bindings/wasm/tests/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import {
MethodDigest,
MethodScope,
Presentation,
StatusCheck,
Storage,
SubjectHolderRelationship,
Timestamp,
VerificationMethod,
} from "../node";
Expand Down Expand Up @@ -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",
}),
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -211,24 +211,27 @@ impl<V: JwsVerifier> JwtCredentialValidator<V> {
));
}

// 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
Expand Down
17 changes: 12 additions & 5 deletions identity_document/src/document/core_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
Expand All @@ -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()
Expand Down
10 changes: 10 additions & 0 deletions identity_document/src/verifiable/jws_verification_options.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

use identity_did::DIDUrl;
use identity_verification::MethodScope;

/// Holds additional options for verifying a JWS with
Expand All @@ -15,6 +16,9 @@ pub struct JwsVerificationOptions {
pub nonce: Option<String>,
/// Verify the signing verification method relation matches this.
pub method_scope: Option<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.
pub method_id: Option<DIDUrl>,
}

impl JwsVerificationOptions {
Expand All @@ -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
}
}
5 changes: 2 additions & 3 deletions identity_storage/src/key_storage/memstore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion identity_storage/src/key_storage/stronghold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
Expand Down
Loading

0 comments on commit e434b02

Please sign in to comment.