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

Allow custom kid to be set in JWS #1239

Merged
merged 12 commits into from
Sep 22, 2023
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
Loading