diff --git a/Cargo.toml b/Cargo.toml index c5622399c7..c877899c4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "identity_resolver", "identity_verification", "identity_jose", + "identity_eddsa_verifier", "examples", ] diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index ddc2b80ddc..c4fb497604 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -19,6 +19,7 @@ crate-type = ["cdylib", "rlib"] async-trait = { version = "0.1", default-features = false } console_error_panic_hook = { version = "0.1" } futures = { version = "0.3" } +identity_eddsa_verifier = { version = "0.7.0-alpha.7", path = "../../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } js-sys = { version = "0.3.61" } proc_typescript = { version = "0.1.0", path = "./proc_typescript" } serde = { version = "1.0", features = ["derive"] } @@ -33,7 +34,7 @@ wasm-bindgen-futures = { version = "0.4", default-features = false } version = "0.7.0-alpha.7" path = "../../identity_iota" default-features = false -features = ["client", "revocation-bitmap", "resolver", "eddsa", "domain-linkage"] +features = ["client", "revocation-bitmap", "resolver", "domain-linkage"] [dev-dependencies] rand = "0.8.5" diff --git a/bindings/wasm/examples/src/0_basic/5_create_vc.ts b/bindings/wasm/examples/src/0_basic/5_create_vc.ts index bf389464c0..1b711e51ad 100644 --- a/bindings/wasm/examples/src/0_basic/5_create_vc.ts +++ b/bindings/wasm/examples/src/0_basic/5_create_vc.ts @@ -3,6 +3,7 @@ import { Credential, + EdDSAJwsVerifier, FailFast, JwkMemStore, JwsSignatureOptions, @@ -79,7 +80,7 @@ export async function createVC() { // Validate the credential's signature, the credential's semantic structure, // check that the issuance date is not in the future and that the expiration date is not in the past. // Note that the validation returns an object containing the decoded credential. - const decoded_credential = new JwtCredentialValidator().validate( + const decoded_credential = new JwtCredentialValidator(new EdDSAJwsVerifier()).validate( credentialJwt, issuerDocument, new JwtCredentialValidationOptions(), diff --git a/bindings/wasm/examples/src/0_basic/6_create_vp.ts b/bindings/wasm/examples/src/0_basic/6_create_vp.ts index 7e3de5df63..f78f19dd1b 100644 --- a/bindings/wasm/examples/src/0_basic/6_create_vp.ts +++ b/bindings/wasm/examples/src/0_basic/6_create_vp.ts @@ -5,6 +5,7 @@ import { CoreDID, Credential, Duration, + EdDSAJwsVerifier, FailFast, IotaIdentityClient, JwkMemStore, @@ -97,7 +98,7 @@ export async function createVP() { new JwsSignatureOptions(), ); - const res = new JwtCredentialValidator().validate( + const res = new JwtCredentialValidator(new EdDSAJwsVerifier()).validate( credentialJwt, issuerDocument, new JwtCredentialValidationOptions(), @@ -178,14 +179,14 @@ export async function createVP() { ); // Validate presentation. Note that this doesn't validate the included credentials. - let decodedPresentation = new JwtPresentationValidator().validate( + let decodedPresentation = new JwtPresentationValidator(new EdDSAJwsVerifier()).validate( presentationJwt, resolvedHolder, jwtPresentationValidationOptions, ); // Validate the credentials in the presentation. - let credentialValidator = new JwtCredentialValidator(); + let credentialValidator = new JwtCredentialValidator(new EdDSAJwsVerifier()); let validationOptions = new JwtCredentialValidationOptions({ subjectHolderRelationship: [ presentationHolderDID.toString(), diff --git a/bindings/wasm/examples/src/0_basic/7_revoke_vc.ts b/bindings/wasm/examples/src/0_basic/7_revoke_vc.ts index eb3d59e792..63b00ee4d7 100644 --- a/bindings/wasm/examples/src/0_basic/7_revoke_vc.ts +++ b/bindings/wasm/examples/src/0_basic/7_revoke_vc.ts @@ -3,10 +3,14 @@ import { Credential, + EdCurve, FailFast, + IJwsVerifier, IotaDocument, IotaIdentityClient, + Jwk, JwkMemStore, + JwsAlgorithm, JwsSignatureOptions, JwtCredentialValidationOptions, JwtCredentialValidator, @@ -16,6 +20,7 @@ import { Service, Storage, VerificationMethod, + verifyEd25519, } from "@iota/identity-wasm/node"; import { AliasOutput, Client, IRent, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; import { API_ENDPOINT, createDid } from "../util"; @@ -134,7 +139,7 @@ export async function revokeVC() { console.log(`Credential JWT > ${credentialJwt.toString()}`); // Validate the credential using the issuer's DID Document. - let jwtCredentialValidator = new JwtCredentialValidator(); + let jwtCredentialValidator = new JwtCredentialValidator(new Ed25519JwsVerifier()); jwtCredentialValidator.validate( credentialJwt, issuerDocument, @@ -225,3 +230,16 @@ export async function revokeVC() { console.log(`Credential successfully revoked!`); } } + +// A custom JWS Verifier capabale of verifying EdDSA signatures with curve Ed25519. +class Ed25519JwsVerifier implements IJwsVerifier { + verify(alg: JwsAlgorithm, signingInput: Uint8Array, decodedSignature: Uint8Array, publicKey: Jwk) { + switch (alg) { + case JwsAlgorithm.EdDSA: + // This verifies that the curve is Ed25519 so we don't need to check ourselves. + return verifyEd25519(alg, signingInput, decodedSignature, publicKey); + default: + throw new Error(`unsupported jws algorithm ${alg}`); + } + } +} diff --git a/bindings/wasm/examples/src/1_advanced/5_domain_linkage.ts b/bindings/wasm/examples/src/1_advanced/5_domain_linkage.ts index 9b490108cc..a2af8f83fb 100644 --- a/bindings/wasm/examples/src/1_advanced/5_domain_linkage.ts +++ b/bindings/wasm/examples/src/1_advanced/5_domain_linkage.ts @@ -7,6 +7,7 @@ import { DIDUrl, DomainLinkageConfiguration, Duration, + EdDSAJwsVerifier, IotaDID, IotaDocument, IotaIdentityClient, @@ -123,7 +124,7 @@ export async function domainLinkage() { // Validate the linkage between the Domain Linkage Credential in the configuration and the provided issuer DID. // Validation succeeds when no error is thrown. - new JwtDomainLinkageValidator().validateLinkage( + new JwtDomainLinkageValidator(new EdDSAJwsVerifier()).validateLinkage( issuerDocument, fetchedConfigurationResource, domainFoo, @@ -157,7 +158,7 @@ export async function domainLinkage() { // Validate the linkage between the Domain Linkage Credential in the configuration and the provided issuer DID. // Validation succeeds when no error is thrown. - new JwtDomainLinkageValidator().validateLinkage( + new JwtDomainLinkageValidator(new EdDSAJwsVerifier()).validateLinkage( didDocument, fetchedConfigurationResource, domains[0], diff --git a/bindings/wasm/src/credential/domain_linkage_validator.rs b/bindings/wasm/src/credential/domain_linkage_validator.rs index 9fb3439242..4dc8840b4b 100644 --- a/bindings/wasm/src/credential/domain_linkage_validator.rs +++ b/bindings/wasm/src/credential/domain_linkage_validator.rs @@ -28,7 +28,7 @@ impl WasmJwtDomainLinkageValidator { /// algorithm will be used. #[wasm_bindgen(constructor)] #[allow(non_snake_case)] - pub fn new(signatureVerifier: Option) -> WasmJwtDomainLinkageValidator { + pub fn new(signatureVerifier: IJwsVerifier) -> WasmJwtDomainLinkageValidator { let signature_verifier = WasmJwsVerifier::new(signatureVerifier); WasmJwtDomainLinkageValidator { validator: JwtDomainLinkageValidator::with_signature_verifier(signature_verifier), diff --git a/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs b/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs index 0011fc690d..cf1a48254b 100644 --- a/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs +++ b/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs @@ -4,6 +4,7 @@ use identity_iota::core::Object; use identity_iota::core::Url; use identity_iota::credential::JwtCredentialValidator; +use identity_iota::credential::JwtCredentialValidatorUtils; use identity_iota::credential::StatusCheck; use identity_iota::did::CoreDID; @@ -39,7 +40,7 @@ impl WasmJwtCredentialValidator { /// algorithm will be used. #[wasm_bindgen(constructor)] #[allow(non_snake_case)] - pub fn new(signatureVerifier: Option) -> WasmJwtCredentialValidator { + pub fn new(signatureVerifier: IJwsVerifier) -> WasmJwtCredentialValidator { let signature_verifier = WasmJwsVerifier::new(signatureVerifier); WasmJwtCredentialValidator(JwtCredentialValidator::with_signature_verifier(signature_verifier)) } @@ -123,13 +124,13 @@ impl WasmJwtCredentialValidator { /// Validate that the credential expires on or after the specified timestamp. #[wasm_bindgen(js_name = checkExpiresOnOrAfter)] pub fn check_expires_on_or_after(credential: &WasmCredential, timestamp: &WasmTimestamp) -> Result<()> { - JwtCredentialValidator::check_expires_on_or_after(&credential.0, timestamp.0).wasm_result() + JwtCredentialValidatorUtils::check_expires_on_or_after(&credential.0, timestamp.0).wasm_result() } /// Validate that the credential is issued on or before the specified timestamp. #[wasm_bindgen(js_name = checkIssuedOnOrBefore)] pub fn check_issued_on_or_before(credential: &WasmCredential, timestamp: &WasmTimestamp) -> Result<()> { - JwtCredentialValidator::check_issued_on_or_before(&credential.0, timestamp.0).wasm_result() + JwtCredentialValidatorUtils::check_issued_on_or_before(&credential.0, timestamp.0).wasm_result() } /// Validate that the relationship between the `holder` and the credential subjects is in accordance with @@ -141,7 +142,8 @@ impl WasmJwtCredentialValidator { relationship: WasmSubjectHolderRelationship, ) -> Result<()> { let holder: Url = Url::parse(holder).wasm_result()?; - JwtCredentialValidator::check_subject_holder_relationship(&credential.0, &holder, relationship.into()).wasm_result() + JwtCredentialValidatorUtils::check_subject_holder_relationship(&credential.0, &holder, relationship.into()) + .wasm_result() } /// Checks whether the credential status has been revoked. @@ -158,7 +160,7 @@ impl WasmJwtCredentialValidator { let trusted_issuers: Vec> = issuer_locks.iter().map(ImportedDocumentLock::blocking_read).collect(); let status_check: StatusCheck = statusCheck.into(); - JwtCredentialValidator::check_status(&credential.0, &trusted_issuers, status_check).wasm_result() + JwtCredentialValidatorUtils::check_status(&credential.0, &trusted_issuers, status_check).wasm_result() } /// Utility for extracting the issuer field of a {@link Credential} as a DID. @@ -168,7 +170,7 @@ impl WasmJwtCredentialValidator { /// Fails if the issuer field is not a valid DID. #[wasm_bindgen(js_name = extractIssuer)] pub fn extract_issuer(credential: &WasmCredential) -> Result { - JwtCredentialValidator::extract_issuer::(&credential.0) + JwtCredentialValidatorUtils::extract_issuer::(&credential.0) .map(WasmCoreDID::from) .wasm_result() } @@ -180,7 +182,7 @@ impl WasmJwtCredentialValidator { /// If the JWT decoding fails or the issuer field is not a valid DID. #[wasm_bindgen(js_name = extractIssuerFromJwt)] pub fn extract_issuer_from_jwt(credential: &WasmJwt) -> Result { - JwtCredentialValidator::extract_issuer_from_jwt::(&credential.0) + JwtCredentialValidatorUtils::extract_issuer_from_jwt::(&credential.0) .map(WasmCoreDID::from) .wasm_result() } diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs index a202e3d6e5..a502c20075 100644 --- a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs +++ b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs @@ -13,6 +13,7 @@ use crate::error::WasmResult; use crate::verification::IJwsVerifier; use crate::verification::WasmJwsVerifier; use identity_iota::credential::JwtPresentationValidator; +use identity_iota::credential::JwtPresentationValidatorUtils; use identity_iota::did::CoreDID; use wasm_bindgen::prelude::*; @@ -26,7 +27,7 @@ impl WasmJwtPresentationValidator { /// algorithm will be used. #[wasm_bindgen(constructor)] #[allow(non_snake_case)] - pub fn new(signatureVerifier: Option) -> WasmJwtPresentationValidator { + pub fn new(signatureVerifier: IJwsVerifier) -> WasmJwtPresentationValidator { let signature_verifier = WasmJwsVerifier::new(signatureVerifier); WasmJwtPresentationValidator(JwtPresentationValidator::with_signature_verifier(signature_verifier)) } @@ -76,7 +77,7 @@ impl WasmJwtPresentationValidator { /// Validates the semantic structure of the {@link Presentation}. #[wasm_bindgen(js_name = checkStructure)] pub fn check_structure(presentation: &WasmPresentation) -> Result<()> { - JwtPresentationValidator::check_structure(&presentation.0).wasm_result()?; + JwtPresentationValidatorUtils::check_structure(&presentation.0).wasm_result()?; Ok(()) } @@ -87,7 +88,7 @@ impl WasmJwtPresentationValidator { /// * If the holder can't be parsed as DIDs. #[wasm_bindgen(js_name = extractHolder)] pub fn extract_holder(presentation: &WasmJwt) -> Result { - let holder = JwtPresentationValidator::extract_holder::(&presentation.0).wasm_result()?; + let holder = JwtPresentationValidatorUtils::extract_holder::(&presentation.0).wasm_result()?; Ok(WasmCoreDID(holder)) } } diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index 5cf0da170d..29f56094cc 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -486,7 +486,7 @@ impl WasmCoreDocument { &self, jws: &WasmJws, options: &WasmJwsVerificationOptions, - signatureVerifier: Option, + signatureVerifier: IJwsVerifier, detachedPayload: Option, ) -> Result { let jws_verifier = WasmJwsVerifier::new(signatureVerifier); diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index b482091b48..2f826232f5 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -361,7 +361,7 @@ impl WasmIotaDocument { &self, jws: &WasmJws, options: &WasmJwsVerificationOptions, - signatureVerifier: Option, + signatureVerifier: IJwsVerifier, detachedPayload: Option, ) -> Result { let jws_verifier = WasmJwsVerifier::new(signatureVerifier); diff --git a/bindings/wasm/src/verification/custom_verification.rs b/bindings/wasm/src/verification/custom_verification.rs index a577c07ed5..4c82d8dcfe 100644 --- a/bindings/wasm/src/verification/custom_verification.rs +++ b/bindings/wasm/src/verification/custom_verification.rs @@ -1,25 +1,21 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use identity_iota::verification::jws::EdDSAJwsVerifier; -use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::jws::JwsVerifier; use identity_iota::verification::jws::SignatureVerificationError; use identity_iota::verification::jws::SignatureVerificationErrorKind; use identity_iota::verification::jws::VerificationInput; use wasm_bindgen::prelude::*; -use crate::error::WasmResult; use crate::jose::WasmJwk; -use crate::jose::WasmJwsAlgorithm; /// Wrapper that enables custom TS JWS signature verification plugins to be used where the /// JwsVerifier trait is required. Falls back to the default implementation if a custom /// implementation was not passed. -pub(crate) struct WasmJwsVerifier(Option); +pub(crate) struct WasmJwsVerifier(IJwsVerifier); impl WasmJwsVerifier { - pub(crate) fn new(verifier: Option) -> Self { + pub(crate) fn new(verifier: IJwsVerifier) -> Self { Self(verifier) } } @@ -30,26 +26,22 @@ impl JwsVerifier for WasmJwsVerifier { input: identity_iota::verification::jws::VerificationInput, public_key: &identity_iota::verification::jwk::Jwk, ) -> Result<(), identity_iota::verification::jws::SignatureVerificationError> { - match &self.0 { - None => EdDSAJwsVerifier::default().verify(input, public_key), - Some(custom) => { - let VerificationInput { - alg, - signing_input, - decoded_signature, - } = input; - let verification_result = custom.verify( - alg.name().to_owned(), - signing_input.into(), - decoded_signature.into(), - WasmJwk(public_key.to_owned()), - ); - // Convert error - crate::error::stringify_js_error(verification_result).map_err(|error_string| { - SignatureVerificationError::new(SignatureVerificationErrorKind::Unspecified).with_custom_message(error_string) - }) - } - } + let VerificationInput { + alg, + signing_input, + decoded_signature, + } = input; + let verification_result = IJwsVerifier::verify( + &self.0, + alg.name().to_owned(), + signing_input.into(), + decoded_signature.into(), + WasmJwk(public_key.to_owned()), + ); + // Convert error + crate::error::stringify_js_error(verification_result).map_err(|error_string| { + SignatureVerificationError::new(SignatureVerificationErrorKind::Unspecified).with_custom_message(error_string) + }) } } #[wasm_bindgen(typescript_custom_section)] @@ -86,29 +78,3 @@ extern "C" { publicKey: WasmJwk, ) -> Result<(), JsValue>; } - -/// Verify a JWS signature secured with the `JwsAlgorithm::EdDSA` algorithm. -/// Only the `EdCurve::Ed25519` variant is supported for now. -/// -/// This function is useful when one is building an `IJwsVerifier` that extends the default provided by -/// the IOTA Identity Framework. -/// -/// # Warning -/// This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this -/// prior to calling the function. -#[wasm_bindgen(js_name = verifyEdDSA)] -#[allow(non_snake_case)] -pub fn verify_eddsa( - alg: WasmJwsAlgorithm, - signingInput: &[u8], - decodedSignature: &[u8], - publicKey: &WasmJwk, -) -> Result<(), JsValue> { - let alg: JwsAlgorithm = JwsAlgorithm::try_from(alg)?; - let input: VerificationInput = VerificationInput { - alg, - signing_input: Box::from(signingInput), - decoded_signature: Box::from(decodedSignature), - }; - identity_iota::verification::jose::jws::EdDSAJwsVerifier::verify_eddsa(input, &publicKey.0).wasm_result() -} diff --git a/bindings/wasm/src/verification/jws_verifier.rs b/bindings/wasm/src/verification/jws_verifier.rs new file mode 100644 index 0000000000..2e04d64e68 --- /dev/null +++ b/bindings/wasm/src/verification/jws_verifier.rs @@ -0,0 +1,82 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_eddsa_verifier::Ed25519Verifier; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota::verification::jws::JwsAlgorithm; +use identity_iota::verification::jws::JwsVerifier; +use identity_iota::verification::jws::VerificationInput; +use wasm_bindgen::prelude::*; + +use crate::error::WasmResult; +use crate::jose::WasmJwk; +use crate::jose::WasmJwsAlgorithm; + +/// Verify a JWS signature secured with the `EdDSA` algorithm and curve `Ed25519`. +/// +/// This function is useful when one is composing a {@link IJwsVerifier} that delegates +/// `EdDSA` verification with curve `Ed25519` to this function. +/// +/// # Warning +/// +/// This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this +/// prior to calling the function. +#[wasm_bindgen(js_name = verifyEd25519)] +#[allow(non_snake_case)] +pub fn verify_ed25519( + alg: WasmJwsAlgorithm, + signingInput: &[u8], + decodedSignature: &[u8], + publicKey: &WasmJwk, +) -> Result<(), JsValue> { + let alg: JwsAlgorithm = JwsAlgorithm::try_from(alg)?; + let input: VerificationInput = VerificationInput { + alg, + signing_input: Box::from(signingInput), + decoded_signature: Box::from(decodedSignature), + }; + Ed25519Verifier::verify(input, &publicKey.0).wasm_result() +} + +/// An implementor of {@link IJwsVerifier} that can handle the +/// `EdDSA` algorithm. +#[wasm_bindgen(js_name = EdDSAJwsVerifier)] +pub struct WasmEdDSAJwsVerifier(); + +#[wasm_bindgen(js_class = EdDSAJwsVerifier)] +#[allow(clippy::new_without_default)] +impl WasmEdDSAJwsVerifier { + /// Constructs an EdDSAJwsVerifier. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self() + } + + /// Verify a JWS signature secured with the `EdDSA` algorithm. + /// Only the `Ed25519` curve is supported for now. + /// + /// This function is useful when one is building an `IJwsVerifier` that extends the default provided by + /// the IOTA Identity Framework. + /// + /// # Warning + /// + /// This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this + /// prior to calling the function. + #[wasm_bindgen] + #[allow(non_snake_case)] + pub fn verify( + &self, + alg: WasmJwsAlgorithm, + signingInput: &[u8], + decodedSignature: &[u8], + publicKey: &WasmJwk, + ) -> Result<(), JsValue> { + let alg: JwsAlgorithm = JwsAlgorithm::try_from(alg)?; + let input: VerificationInput = VerificationInput { + alg, + signing_input: signingInput.into(), + decoded_signature: decodedSignature.into(), + }; + EdDSAJwsVerifier::default().verify(input, &publicKey.0).wasm_result() + } +} diff --git a/bindings/wasm/src/verification/mod.rs b/bindings/wasm/src/verification/mod.rs index f1493f65bc..a16a71c411 100644 --- a/bindings/wasm/src/verification/mod.rs +++ b/bindings/wasm/src/verification/mod.rs @@ -3,6 +3,7 @@ mod custom_verification; +mod jws_verifier; mod wasm_method_data; mod wasm_method_relationship; mod wasm_method_scope; diff --git a/bindings/wasm/tests/credentials.ts b/bindings/wasm/tests/credentials.ts index 92f240dd0a..dfe4e5ed79 100644 --- a/bindings/wasm/tests/credentials.ts +++ b/bindings/wasm/tests/credentials.ts @@ -2,6 +2,7 @@ import * as assert from "assert"; import { CoreDocument, Credential, + EdDSAJwsVerifier, JwkMemStore, JwsAlgorithm, JwsSignatureOptions, @@ -251,7 +252,7 @@ describe("Presentation", function() { let issuer = JwtPresentationValidator.extractHolder(presentationJwt); assert.deepStrictEqual(issuer.toString(), doc.id().toString()); - const decodedPresentation = new JwtPresentationValidator().validate( + const decodedPresentation = new JwtPresentationValidator(new EdDSAJwsVerifier()).validate( presentationJwt, doc, new JwtPresentationValidationOptions(), diff --git a/bindings/wasm/tests/storage.ts b/bindings/wasm/tests/storage.ts index aa45f004bc..347eb42a58 100644 --- a/bindings/wasm/tests/storage.ts +++ b/bindings/wasm/tests/storage.ts @@ -4,6 +4,7 @@ import { Credential, DecodedJwtPresentation, Duration, + EdDSAJwsVerifier, FailFast, IJwsVerifier, IotaDocument, @@ -25,7 +26,6 @@ import { Storage, Timestamp, VerificationMethod, - verifyEdDSA, } from "../node"; import { createVerificationMethod } from "./key_id_storage"; @@ -95,7 +95,7 @@ describe("#JwkStorageDocument", function() { ); // Verify the signature and obtain a decoded token. - const token = doc.verifyJws(jws, new JwsVerificationOptions()); + const token = doc.verifyJws(jws, new JwsVerificationOptions(), new EdDSAJwsVerifier()); assert.deepStrictEqual(testString, token.claims()); // Check that we can also verify it using a custom verifier @@ -139,8 +139,7 @@ describe("#JwkStorageDocument", function() { ); // Check that the credentialJwt can be decoded and verified - let credentialValidator = new JwtCredentialValidator(); - + let credentialValidator = new JwtCredentialValidator(new EdDSAJwsVerifier()); const decoded = credentialValidator .validate( credentialJwt, @@ -208,7 +207,7 @@ describe("#JwkStorageDocument", function() { ); // Verify the signature and obtain a decoded token. - const token = doc.verifyJws(jws, new JwsVerificationOptions()); + const token = doc.verifyJws(jws, new JwsVerificationOptions(), new EdDSAJwsVerifier()); assert.deepStrictEqual(testString, token.claims()); // Check that we can also verify it using a custom verifier @@ -251,7 +250,7 @@ describe("#JwkStorageDocument", function() { ); // Check that the credentialJwt can be decoded and verified - let credentialValidator = new JwtCredentialValidator(); + let credentialValidator = new JwtCredentialValidator(new EdDSAJwsVerifier()); const decoded = credentialValidator .validate( credentialJwt, @@ -419,7 +418,7 @@ describe("#JwkStorageDocument", function() { decodedSignature: Uint8Array, publicKey: Jwk, ): void { - verifyEdDSA(alg, signingInput, decodedSignature, publicKey); + new EdDSAJwsVerifier().verify(alg, signingInput, decodedSignature, publicKey); this._verifications += 1; return; } diff --git a/examples/0_basic/5_create_vc.rs b/examples/0_basic/5_create_vc.rs index 66c09d9b3c..1da4ce3bcc 100644 --- a/examples/0_basic/5_create_vc.rs +++ b/examples/0_basic/5_create_vc.rs @@ -11,6 +11,7 @@ use examples::create_did; use examples::MemStorage; +use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::Object; use identity_iota::credential::DecodedJwtCredential; @@ -101,14 +102,15 @@ async fn main() -> anyhow::Result<()> { // Validate the credential's signature using the issuer's DID Document, the credential's semantic structure, // that the issuance date is not in the future and that the expiration date is not in the past: - let decoded_credential: DecodedJwtCredential = JwtCredentialValidator::new() - .validate::<_, Object>( - &credential_jwt, - &issuer_document, - &JwtCredentialValidationOptions::default(), - FailFast::FirstError, - ) - .unwrap(); + let decoded_credential: DecodedJwtCredential = + JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()) + .validate::<_, Object>( + &credential_jwt, + &issuer_document, + &JwtCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); println!("VC successfully validated"); diff --git a/examples/0_basic/6_create_vp.rs b/examples/0_basic/6_create_vp.rs index 0b6bcd91cc..98174b54b2 100644 --- a/examples/0_basic/6_create_vp.rs +++ b/examples/0_basic/6_create_vp.rs @@ -11,13 +11,16 @@ use std::collections::HashMap; use examples::create_did; use examples::MemStorage; +use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::Object; use identity_iota::credential::DecodedJwtCredential; use identity_iota::credential::DecodedJwtPresentation; use identity_iota::credential::Jwt; +use identity_iota::credential::JwtCredentialValidatorUtils; use identity_iota::credential::JwtPresentationOptions; use identity_iota::credential::JwtPresentationValidationOptions; use identity_iota::credential::JwtPresentationValidator; +use identity_iota::credential::JwtPresentationValidatorUtils; use identity_iota::credential::Presentation; use identity_iota::credential::PresentationBuilder; use identity_iota::did::CoreDID; @@ -120,7 +123,7 @@ async fn main() -> anyhow::Result<()> { // Validate the credential's signature using the issuer's DID Document, the credential's semantic structure, // that the issuance date is not in the future and that the expiration date is not in the past: - JwtCredentialValidator::new() + JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()) .validate::<_, Object>( &credential_jwt, &issuer_document, @@ -191,25 +194,28 @@ async fn main() -> anyhow::Result<()> { resolver.attach_iota_handler(client); // Resolve the holder's document. - let holder_did: CoreDID = JwtPresentationValidator::extract_holder(&presentation_jwt)?; + let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; let holder: IotaDocument = resolver.resolve(&holder_did).await?; // Validate presentation. Note that this doesn't validate the included credentials. let presentation_validation_options = JwtPresentationValidationOptions::default().presentation_verifier_options(presentation_verifier_options); - let presentation: DecodedJwtPresentation = - JwtPresentationValidator::new().validate(&presentation_jwt, &holder, &presentation_validation_options)?; + let presentation: DecodedJwtPresentation = JwtPresentationValidator::with_signature_verifier( + EdDSAJwsVerifier::default(), + ) + .validate(&presentation_jwt, &holder, &presentation_validation_options)?; // Concurrently resolve the issuers' documents. let jwt_credentials: &Vec = &presentation.presentation.verifiable_credential; let issuers: Vec = jwt_credentials .iter() - .map(JwtCredentialValidator::extract_issuer_from_jwt) + .map(JwtCredentialValidatorUtils::extract_issuer_from_jwt) .collect::, _>>()?; let issuers_documents: HashMap = resolver.resolve_multiple(&issuers).await?; // Validate the credentials in the presentation. - let credential_validator: JwtCredentialValidator = JwtCredentialValidator::new(); + let credential_validator: JwtCredentialValidator = + JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); let validation_options: JwtCredentialValidationOptions = JwtCredentialValidationOptions::default() .subject_holder_relationship(holder_did.to_url().into(), SubjectHolderRelationship::AlwaysSubject); diff --git a/examples/0_basic/7_revoke_vc.rs b/examples/0_basic/7_revoke_vc.rs index 1e6af0da76..eee2ba6b4b 100644 --- a/examples/0_basic/7_revoke_vc.rs +++ b/examples/0_basic/7_revoke_vc.rs @@ -15,6 +15,7 @@ use examples::create_did; use examples::random_stronghold_path; use examples::MemStorage; use examples::API_ENDPOINT; +use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::json; use identity_iota::core::FromJson; use identity_iota::core::Object; @@ -27,6 +28,7 @@ use identity_iota::credential::FailFast; use identity_iota::credential::Jwt; use identity_iota::credential::JwtCredentialValidationOptions; use identity_iota::credential::JwtCredentialValidator; +use identity_iota::credential::JwtCredentialValidatorUtils; use identity_iota::credential::JwtValidationError; use identity_iota::credential::RevocationBitmap; use identity_iota::credential::RevocationBitmapStatus; @@ -146,8 +148,10 @@ async fn main() -> anyhow::Result<()> { ) .await?; + let validator: JwtCredentialValidator = + JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); // Validate the credential's signature using the issuer's DID Document. - JwtCredentialValidator::new().validate::<_, Object>( + validator.validate::<_, Object>( &credential_jwt, &issuer_document, &JwtCredentialValidationOptions::default(), @@ -170,8 +174,8 @@ async fn main() -> anyhow::Result<()> { .finish()?; issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?; - let validation_result: std::result::Result = - JwtCredentialValidator::new().validate( + let validation_result: std::result::Result = validator + .validate( &credential_jwt, &issuer_document, &JwtCredentialValidationOptions::default(), @@ -207,10 +211,10 @@ async fn main() -> anyhow::Result<()> { // We expect the verifiable credential to be revoked. let mut resolver: Resolver = Resolver::new(); resolver.attach_iota_handler(client); - let resolved_issuer_did: IotaDID = JwtCredentialValidator::extract_issuer_from_jwt(&credential_jwt)?; + let resolved_issuer_did: IotaDID = JwtCredentialValidatorUtils::extract_issuer_from_jwt(&credential_jwt)?; let resolved_issuer_doc: IotaDocument = resolver.resolve(&resolved_issuer_did).await?; - let validation_result = JwtCredentialValidator::new().validate::<_, Object>( + let validation_result = validator.validate::<_, Object>( &credential_jwt, &resolved_issuer_doc, &JwtCredentialValidationOptions::default(), diff --git a/examples/0_basic/8_stronghold.rs b/examples/0_basic/8_stronghold.rs index 29899fc6a2..f69fc97e59 100644 --- a/examples/0_basic/8_stronghold.rs +++ b/examples/0_basic/8_stronghold.rs @@ -3,6 +3,7 @@ use examples::get_address_with_funds; use examples::random_stronghold_path; +use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::credential::Jws; use identity_iota::document::verifiable::JwsVerificationOptions; use identity_iota::iota::IotaClientExt; @@ -16,7 +17,6 @@ use identity_iota::storage::JwsSignatureOptions; use identity_iota::storage::Storage; use identity_iota::storage::StrongholdStorage; use identity_iota::verification::jws::DecodedJws; -use identity_iota::verification::jws::EdDSAJwsVerifier; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; use iota_sdk::client::secret::stronghold::StrongholdSecretManager; diff --git a/examples/1_advanced/6_domain_linkage.rs b/examples/1_advanced/6_domain_linkage.rs index 9db9584b02..a77bc3c5b0 100644 --- a/examples/1_advanced/6_domain_linkage.rs +++ b/examples/1_advanced/6_domain_linkage.rs @@ -5,6 +5,7 @@ use examples::create_did; use examples::random_stronghold_path; use examples::MemStorage; use examples::API_ENDPOINT; +use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::Duration; use identity_iota::core::FromJson; use identity_iota::core::Object; @@ -154,12 +155,13 @@ async fn main() -> anyhow::Result<()> { let issuer_did_document: IotaDocument = resolver.resolve(&did).await?; // Validate the linkage between the Domain Linkage Credential in the configuration and the provided issuer DID. - let validation_result: Result<(), DomainLinkageValidationError> = JwtDomainLinkageValidator::new().validate_linkage( - &issuer_did_document, - &configuration_resource, - &domain_foo, - &JwtCredentialValidationOptions::default(), - ); + let validation_result: Result<(), DomainLinkageValidationError> = + JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default()).validate_linkage( + &issuer_did_document, + &configuration_resource, + &domain_foo, + &JwtCredentialValidationOptions::default(), + ); assert!(validation_result.is_ok()); // ===================================================== @@ -198,12 +200,13 @@ async fn main() -> anyhow::Result<()> { DomainLinkageConfiguration::from_json(&configuration_resource_json)?; // Validate the linkage. - let validation_result: Result<(), DomainLinkageValidationError> = JwtDomainLinkageValidator::new().validate_linkage( - &did_document, - &configuration_resource, - &domain_foo, - &JwtCredentialValidationOptions::default(), - ); + let validation_result: Result<(), DomainLinkageValidationError> = + JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default()).validate_linkage( + &did_document, + &configuration_resource, + &domain_foo, + &JwtCredentialValidationOptions::default(), + ); assert!(validation_result.is_ok()); Ok(()) } diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 05e0eada42..d0a7aeeb6e 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,7 +7,9 @@ publish = false [dependencies] anyhow = "1.0.62" -identity_iota = { path = "../identity_iota", features = ["memstore", "domain-linkage", "stronghold"] } +identity_eddsa_verifier = { path = "../identity_eddsa_verifier" } +identity_iota = { path = "../identity_iota", features = ["memstore", "domain-linkage"] } +identity_storage = { path = "../identity_storage", features = ["stronghold"] } iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } primitive-types = "0.12.1" rand = "0.8.5" diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 0f7dd59a06..961325db97 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -31,6 +31,7 @@ thiserror.workspace = true url = { version = "2.4", default-features = false } [dev-dependencies] +identity_eddsa_verifier = { version = "=0.7.0-alpha.7", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519", "std", "random"] } proptest = { version = "1.0.0", default-features = false, features = ["std"] } serde_json.workspace = true @@ -47,6 +48,6 @@ default = ["revocation-bitmap", "validator", "credential", "presentation", "doma credential = [] presentation = ["credential"] revocation-bitmap = ["dep:dataurl", "dep:flate2", "dep:roaring"] -validator = ["dep:itertools", "dep:serde_repr", "credential", "presentation", "identity_verification/eddsa"] +validator = ["dep:itertools", "dep:serde_repr", "credential", "presentation"] domain-linkage = ["validator"] domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] diff --git a/identity_credential/src/domain_linkage/domain_linkage_configuration.rs b/identity_credential/src/domain_linkage/domain_linkage_configuration.rs index 06629d4937..2e4eaf3493 100644 --- a/identity_credential/src/domain_linkage/domain_linkage_configuration.rs +++ b/identity_credential/src/domain_linkage/domain_linkage_configuration.rs @@ -3,7 +3,7 @@ use crate::credential::Jwt; use crate::error::Result; -use crate::validator::JwtCredentialValidator; +use crate::validator::JwtCredentialValidatorUtils; use crate::validator::JwtValidationError; use identity_core::common::Context; use identity_core::common::Url; @@ -97,7 +97,7 @@ impl DomainLinkageConfiguration { .0 .linked_dids .iter() - .map(JwtCredentialValidator::extract_issuer_from_jwt::) + .map(JwtCredentialValidatorUtils::extract_issuer_from_jwt::) .collect() } diff --git a/identity_credential/src/domain_linkage/domain_linkage_validator.rs b/identity_credential/src/domain_linkage/domain_linkage_validator.rs index 075523750c..aa2fd8e656 100644 --- a/identity_credential/src/domain_linkage/domain_linkage_validator.rs +++ b/identity_credential/src/domain_linkage/domain_linkage_validator.rs @@ -13,7 +13,6 @@ use identity_core::common::OneOrMany; use identity_core::common::Url; use identity_did::CoreDID; use identity_document::document::CoreDocument; -use identity_verification::jws::EdDSAJwsVerifier; use identity_verification::jws::JwsVerifier; use crate::validator::DecodedJwtCredential; @@ -22,31 +21,13 @@ use super::DomainLinkageValidationResult; /// A validator for a Domain Linkage Configuration and Credentials. -#[derive(Debug, Clone)] -pub struct JwtDomainLinkageValidator { +pub struct JwtDomainLinkageValidator { validator: JwtCredentialValidator, } -impl JwtDomainLinkageValidator { - /// Creates a new [`JwtDomainLinkageValidator`] capable of verifying a Domain Linkage Credential issued as a JWS - /// using the [`EdDSA`](::identity_verification::jose::jws::JwsAlgorithm::EdDSA) algorithm. - /// - /// See [`JwtDomainLinkageValidator::with_signature_verifier`](JwtDomainLinkageValidator::with_signature_verifier) - /// which enables you to supply a custom signature verifier if other JWS algorithms are of interest. - pub fn new() -> Self { - Self { - validator: JwtCredentialValidator::new(), - } - } -} - -impl JwtDomainLinkageValidator -where - V: JwsVerifier, -{ +impl JwtDomainLinkageValidator { /// Create a new [`JwtDomainLinkageValidator`] that delegates cryptographic signature verification to the given - /// `signature_verifier`. If you are only interested in `EdDSA` signatures (with `Ed25519`) then the default - /// constructor can be used. See [`JwtDomainLinkageValidator::new`](JwtDomainLinkageValidator::new). + /// `signature_verifier`. pub fn with_signature_verifier(signature_verifier: V) -> Self { Self { validator: JwtCredentialValidator::with_signature_verifier(signature_verifier), @@ -220,12 +201,6 @@ where } } -impl Default for JwtDomainLinkageValidator { - fn default() -> Self { - Self::new() - } -} - #[cfg(test)] mod tests { use crate::credential::Credential; @@ -248,6 +223,7 @@ mod tests { use identity_core::common::Url; use identity_did::CoreDID; use identity_document::document::CoreDocument; + use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_verification::jws::CharSet; use identity_verification::jws::CompactJwsEncoder; use identity_verification::jws::CompactJwsEncodingOptions; @@ -255,6 +231,10 @@ mod tests { use identity_verification::jws::JwsHeader; use identity_verification::MethodData; use identity_verification::VerificationMethod; + use once_cell::sync::Lazy; + + static JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519: Lazy> = + Lazy::new(|| JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default())); #[test] pub(crate) fn test_valid_credential() { @@ -262,7 +242,7 @@ mod tests { let credential: Credential = create_domain_linkage_credential(document.id()); let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_credential( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential( &document, &jwt, &url_foo(), @@ -280,7 +260,7 @@ mod tests { // Sign with `other_secret_key` to produce an invalid signature. let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &other_secret_key); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_credential( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential( &document, &jwt, &url_foo(), @@ -299,7 +279,7 @@ mod tests { credential.id = Some(Url::parse("http://random.credential.id").unwrap()); let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_credential( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential( &document, &jwt, &url_foo(), @@ -319,7 +299,7 @@ mod tests { credential.types = OneOrMany::One(Credential::::base_type().to_owned()); let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_credential( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential( &document, &jwt, &url_foo(), @@ -343,7 +323,7 @@ mod tests { ]); let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_credential( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential( &document, &jwt, &url_foo(), @@ -365,7 +345,7 @@ mod tests { } let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_credential( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential( &document, &jwt, &url_foo(), @@ -389,7 +369,7 @@ mod tests { } let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_credential( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential( &document, &jwt, &url_foo(), @@ -414,7 +394,7 @@ mod tests { } let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_credential( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential( &document, &jwt, &url_foo(), @@ -434,7 +414,7 @@ mod tests { } let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_credential( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential( &document, &jwt, &url_foo(), @@ -458,7 +438,7 @@ mod tests { let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_credential( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential( &document, &jwt, &url_foo(), @@ -481,7 +461,7 @@ mod tests { } let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_credential( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_credential( &document, &jwt, &url_foo(), @@ -502,7 +482,7 @@ mod tests { let configuration: DomainLinkageConfiguration = DomainLinkageConfiguration::new(vec![jwt.clone(), jwt]); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_linkage( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_linkage( &document, &configuration, &url_foo(), @@ -521,7 +501,7 @@ mod tests { let jwt: Jwt = sign_credential_jwt(&credential, &document, &fragment, &secret_key); let configuration: DomainLinkageConfiguration = DomainLinkageConfiguration::new(vec![jwt]); - let validation_result: DomainLinkageValidationResult = JwtDomainLinkageValidator::new().validate_linkage( + let validation_result: DomainLinkageValidationResult = JWT_DOMAIN_LINKAGE_VALIDATOR_ED25519.validate_linkage( &document, &configuration, &url_foo(), diff --git a/identity_credential/src/validator/vc_jwt_validation/decoded_jwt_credential.rs b/identity_credential/src/validator/jwt_credential_validation/decoded_jwt_credential.rs similarity index 100% rename from identity_credential/src/validator/vc_jwt_validation/decoded_jwt_credential.rs rename to identity_credential/src/validator/jwt_credential_validation/decoded_jwt_credential.rs diff --git a/identity_credential/src/validator/vc_jwt_validation/error.rs b/identity_credential/src/validator/jwt_credential_validation/error.rs similarity index 100% rename from identity_credential/src/validator/vc_jwt_validation/error.rs rename to identity_credential/src/validator/jwt_credential_validation/error.rs diff --git a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validation_options.rs b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validation_options.rs similarity index 100% rename from identity_credential/src/validator/vc_jwt_validation/credential_jwt_validation_options.rs rename to identity_credential/src/validator/jwt_credential_validation/jwt_credential_validation_options.rs diff --git a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator.rs similarity index 62% rename from identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs rename to identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator.rs index b26dbe81df..40ca2138e4 100644 --- a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs +++ b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator.rs @@ -1,50 +1,35 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::str::FromStr; - -use identity_core::common::Object; -use identity_core::common::OneOrMany; -use identity_core::common::Timestamp; -use identity_core::common::Url; use identity_core::convert::FromJson; use identity_did::CoreDID; use identity_did::DIDUrl; -use identity_did::DID; use identity_document::document::CoreDocument; use identity_document::verifiable::JwsVerificationOptions; use identity_verification::jwk::Jwk; use identity_verification::jws::DecodedJws; use identity_verification::jws::Decoder; -use identity_verification::jws::EdDSAJwsVerifier; use identity_verification::jws::JwsValidationItem; use identity_verification::jws::JwsVerifier; use super::CompoundCredentialValidationError; use super::DecodedJwtCredential; use super::JwtCredentialValidationOptions; +use super::JwtCredentialValidatorUtils; use super::JwtValidationError; use super::SignerContext; use crate::credential::Credential; use crate::credential::CredentialJwtClaims; use crate::credential::Jwt; use crate::validator::FailFast; -use crate::validator::SubjectHolderRelationship; /// A type for decoding and validating [`Credential`]s. -#[derive(Debug, Clone)] #[non_exhaustive] -pub struct JwtCredentialValidator(V); - -type ValidationUnitResult = std::result::Result; +pub struct JwtCredentialValidator(V); -impl JwtCredentialValidator -where - V: JwsVerifier, -{ +impl JwtCredentialValidator { /// Create a new [`JwtCredentialValidator`] that delegates cryptographic signature verification to the given - /// `signature_verifier`. If you are only interested in `EdDSA` signatures (with `Ed25519`) then the default - /// constructor can be used. See [`JwtCredentialValidator::new`](JwtCredentialValidator::new). + /// `signature_verifier`. pub fn with_signature_verifier(signature_verifier: V) -> Self { Self(signature_verifier) } @@ -83,7 +68,7 @@ where T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, DOC: AsRef, { - Self::validate_extended::( + Self::validate_extended::( &self.0, credential_jwt, std::slice::from_ref(issuer.as_ref()), @@ -149,24 +134,27 @@ where // Run all single concern Credential validations in turn and fail immediately if `fail_fast` is true. let expiry_date_validation = std::iter::once_with(|| { - JwtCredentialValidator::check_expires_on_or_after( + JwtCredentialValidatorUtils::check_expires_on_or_after( &credential_token.credential, options.earliest_expiry_date.unwrap_or_default(), ) }); let issuance_date_validation = std::iter::once_with(|| { - JwtCredentialValidator::check_issued_on_or_before(credential, options.latest_issuance_date.unwrap_or_default()) + JwtCredentialValidatorUtils::check_issued_on_or_before( + credential, + options.latest_issuance_date.unwrap_or_default(), + ) }); - let structure_validation = std::iter::once_with(|| JwtCredentialValidator::check_structure(credential)); + let structure_validation = std::iter::once_with(|| JwtCredentialValidatorUtils::check_structure(credential)); let subject_holder_validation = std::iter::once_with(|| { options .subject_holder_relationship .as_ref() .map(|(holder, relationship)| { - JwtCredentialValidator::check_subject_holder_relationship(credential, holder, *relationship) + JwtCredentialValidatorUtils::check_subject_holder_relationship(credential, holder, *relationship) }) .unwrap_or(Ok(())) }); @@ -179,7 +167,7 @@ where #[cfg(feature = "revocation-bitmap")] let validation_units_iter = { let revocation_validation = - std::iter::once_with(|| JwtCredentialValidator::check_status(credential, issuers, options.status)); + std::iter::once_with(|| JwtCredentialValidatorUtils::check_status(credential, issuers, options.status)); validation_units_iter.chain(revocation_validation) }; @@ -263,7 +251,7 @@ where // Check that the DID component of the parsed `kid` does indeed correspond to the issuer in the credential before // returning. - let issuer_id: CoreDID = JwtCredentialValidator::extract_issuer(&credential_token.credential)?; + let issuer_id: CoreDID = JwtCredentialValidatorUtils::extract_issuer(&credential_token.credential)?; if &issuer_id != method_id.did() { return Err(JwtValidationError::IdentifierMismatch { signer_ctx: SignerContext::Issuer, @@ -320,190 +308,12 @@ where } } -impl JwtCredentialValidator { - /// Creates a new [`JwtCredentialValidator`] capable of verifying a [`Credential`] issued as a JWS - /// using the [`EdDSA`](::identity_verification::jose::jws::JwsAlgorithm::EdDSA) algorithm. - /// - /// See [`JwtCredentialValidator::with_signature_verifier`](JwtCredentialValidator::with_signature_verifier()) - /// which enables you to supply a custom signature verifier if other JWS algorithms are of interest. - pub fn new() -> Self { - Self(EdDSAJwsVerifier::default()) - } - - /// Validates the semantic structure of the [`Credential`]. - /// - /// # Warning - /// This does not validate against the credential's schema nor the structure of the subject claims. - pub fn check_structure(credential: &Credential) -> ValidationUnitResult { - credential - .check_structure() - .map_err(JwtValidationError::CredentialStructure) - } - - /// Validate that the [`Credential`] expires on or after the specified [`Timestamp`]. - pub fn check_expires_on_or_after(credential: &Credential, timestamp: Timestamp) -> ValidationUnitResult { - let expiration_date: Option = credential.expiration_date; - (expiration_date.is_none() || expiration_date >= Some(timestamp)) - .then_some(()) - .ok_or(JwtValidationError::ExpirationDate) - } - - /// Validate that the [`Credential`] is issued on or before the specified [`Timestamp`]. - pub fn check_issued_on_or_before(credential: &Credential, timestamp: Timestamp) -> ValidationUnitResult { - (credential.issuance_date <= timestamp) - .then_some(()) - .ok_or(JwtValidationError::IssuanceDate) - } - - /// Validate that the relationship between the `holder` and the credential subjects is in accordance with - /// `relationship`. - pub fn check_subject_holder_relationship( - credential: &Credential, - holder: &Url, - relationship: SubjectHolderRelationship, - ) -> ValidationUnitResult { - let url_matches: bool = match &credential.credential_subject { - OneOrMany::One(ref credential_subject) => credential_subject.id.as_ref() == Some(holder), - OneOrMany::Many(subjects) => { - // need to check the case where the Many variant holds a vector of exactly one subject - if let [credential_subject] = subjects.as_slice() { - credential_subject.id.as_ref() == Some(holder) - } else { - // zero or > 1 subjects is interpreted to mean that the holder is not the subject - false - } - } - }; - - Some(relationship) - .filter(|relationship| match relationship { - SubjectHolderRelationship::AlwaysSubject => url_matches, - SubjectHolderRelationship::SubjectOnNonTransferable => { - url_matches || !credential.non_transferable.unwrap_or(false) - } - SubjectHolderRelationship::Any => true, - }) - .map(|_| ()) - .ok_or(JwtValidationError::SubjectHolderRelationship) - } - - /// Checks whether the credential status has been revoked. - /// - /// Only supports `RevocationBitmap2022`. - #[cfg(feature = "revocation-bitmap")] - pub fn check_status, T>( - credential: &Credential, - trusted_issuers: &[DOC], - status_check: crate::validator::StatusCheck, - ) -> ValidationUnitResult { - if status_check == crate::validator::StatusCheck::SkipAll { - return Ok(()); - } - - match &credential.credential_status { - None => Ok(()), - Some(status) => { - // Check status is supported. - if status.type_ != crate::revocation::RevocationBitmap::TYPE { - if status_check == crate::validator::StatusCheck::SkipUnsupported { - return Ok(()); - } - return Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( - "unsupported type '{}'", - status.type_ - )))); - } - let status: crate::credential::RevocationBitmapStatus = - crate::credential::RevocationBitmapStatus::try_from(status.clone()) - .map_err(JwtValidationError::InvalidStatus)?; - - // Check the credential index against the issuer's DID Document. - let issuer_did: CoreDID = Self::extract_issuer(credential)?; - trusted_issuers - .iter() - .find(|issuer| ::id(issuer.as_ref()) == &issuer_did) - .ok_or(JwtValidationError::DocumentMismatch(SignerContext::Issuer)) - .and_then(|issuer| Self::check_revocation_bitmap_status(issuer, status)) - } - } - } - - /// Check the given `status` against the matching [`RevocationBitmap`] service in the - /// issuer's DID Document. - #[cfg(feature = "revocation-bitmap")] - fn check_revocation_bitmap_status + ?Sized>( - issuer: &DOC, - status: crate::credential::RevocationBitmapStatus, - ) -> ValidationUnitResult { - use crate::revocation::RevocationDocumentExt; - - let issuer_service_url: identity_did::DIDUrl = status.id().map_err(JwtValidationError::InvalidStatus)?; - - // Check whether index is revoked. - let revocation_bitmap: crate::revocation::RevocationBitmap = issuer - .as_ref() - .resolve_revocation_bitmap(issuer_service_url.into()) - .map_err(|_| JwtValidationError::ServiceLookupError)?; - let index: u32 = status.index().map_err(JwtValidationError::InvalidStatus)?; - if revocation_bitmap.is_revoked(index) { - Err(JwtValidationError::Revoked) - } else { - Ok(()) - } - } - - /// Utility for extracting the issuer field of a [`Credential`] as a DID. - /// - /// # Errors - /// - /// Fails if the issuer field is not a valid DID. - pub fn extract_issuer(credential: &Credential) -> std::result::Result - where - D: DID, - ::Err: std::error::Error + Send + Sync + 'static, - { - D::from_str(credential.issuer.url().as_str()).map_err(|err| JwtValidationError::SignerUrl { - signer_ctx: SignerContext::Issuer, - source: err.into(), - }) - } - - /// Utility for extracting the issuer field of a credential in JWT representation as DID. - /// - /// # Errors - /// - /// If the JWT decoding fails or the issuer field is not a valid DID. - pub fn extract_issuer_from_jwt(credential: &Jwt) -> std::result::Result - where - D: DID, - ::Err: std::error::Error + Send + Sync + 'static, - { - let validation_item = Decoder::new() - .decode_compact_serialization(credential.as_str().as_bytes(), None) - .map_err(JwtValidationError::JwsDecodingError)?; - - let claims: CredentialJwtClaims<'_, Object> = CredentialJwtClaims::from_json_slice(&validation_item.claims()) - .map_err(|err| { - JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) - })?; - - D::from_str(claims.iss.url().as_str()).map_err(|err| JwtValidationError::SignerUrl { - signer_ctx: SignerContext::Issuer, - source: err.into(), - }) - } -} - -impl Default for JwtCredentialValidator { - fn default() -> Self { - Self::new() - } -} - #[cfg(test)] mod tests { use crate::credential::Subject; + use crate::validator::SubjectHolderRelationship; use identity_core::common::Duration; + use identity_core::common::Url; use once_cell::sync::Lazy; // All tests here are essentially adaptations of the old JwtCredentialValidator tests. @@ -539,7 +349,7 @@ mod tests { #[test] fn issued_on_or_before() { - assert!(JwtCredentialValidator::check_issued_on_or_before( + assert!(JwtCredentialValidatorUtils::check_issued_on_or_before( &SIMPLE_CREDENTIAL, SIMPLE_CREDENTIAL .issuance_date @@ -549,7 +359,7 @@ mod tests { .is_err()); // and now with a later timestamp - assert!(JwtCredentialValidator::check_issued_on_or_before( + assert!(JwtCredentialValidatorUtils::check_issued_on_or_before( &SIMPLE_CREDENTIAL, SIMPLE_CREDENTIAL .issuance_date @@ -569,21 +379,21 @@ mod tests { credential.non_transferable = Some(true); // checking with holder = subject passes for all defined subject holder relationships: - assert!(JwtCredentialValidator::check_subject_holder_relationship( + assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship( &credential, &actual_holder_url, SubjectHolderRelationship::AlwaysSubject ) .is_ok()); - assert!(JwtCredentialValidator::check_subject_holder_relationship( + assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship( &credential, &actual_holder_url, SubjectHolderRelationship::SubjectOnNonTransferable ) .is_ok()); - assert!(JwtCredentialValidator::check_subject_holder_relationship( + assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship( &credential, &actual_holder_url, SubjectHolderRelationship::Any @@ -594,21 +404,21 @@ mod tests { let issuer_url = Url::parse("did:core:0x1234567890").unwrap(); assert!(actual_holder_url != issuer_url); - assert!(JwtCredentialValidator::check_subject_holder_relationship( + assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship( &credential, &issuer_url, SubjectHolderRelationship::AlwaysSubject ) .is_err()); - assert!(JwtCredentialValidator::check_subject_holder_relationship( + assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship( &credential, &issuer_url, SubjectHolderRelationship::SubjectOnNonTransferable ) .is_err()); - assert!(JwtCredentialValidator::check_subject_holder_relationship( + assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship( &credential, &issuer_url, SubjectHolderRelationship::Any @@ -619,7 +429,7 @@ mod tests { credential_transferable.non_transferable = Some(false); - assert!(JwtCredentialValidator::check_subject_holder_relationship( + assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship( &credential_transferable, &issuer_url, SubjectHolderRelationship::SubjectOnNonTransferable @@ -628,7 +438,7 @@ mod tests { credential_transferable.non_transferable = None; - assert!(JwtCredentialValidator::check_subject_holder_relationship( + assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship( &credential_transferable, &issuer_url, SubjectHolderRelationship::SubjectOnNonTransferable @@ -641,21 +451,21 @@ mod tests { .credential_subject .push(Subject::with_id(actual_holder_url)); - assert!(JwtCredentialValidator::check_subject_holder_relationship( + assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship( &credential_duplicated_holder, &issuer_url, SubjectHolderRelationship::AlwaysSubject ) .is_err()); - assert!(JwtCredentialValidator::check_subject_holder_relationship( + assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship( &credential_duplicated_holder, &issuer_url, SubjectHolderRelationship::SubjectOnNonTransferable ) .is_err()); - assert!(JwtCredentialValidator::check_subject_holder_relationship( + assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship( &credential_duplicated_holder, &issuer_url, SubjectHolderRelationship::Any @@ -670,10 +480,12 @@ mod tests { .unwrap() .checked_add(Duration::minutes(1)) .unwrap(); - assert!(JwtCredentialValidator::check_expires_on_or_after(&SIMPLE_CREDENTIAL, later_than_expiration_date).is_err()); + assert!( + JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, later_than_expiration_date).is_err() + ); // and now with an earlier date let earlier_date = Timestamp::parse("2019-12-27T11:35:30Z").unwrap(); - assert!(JwtCredentialValidator::check_expires_on_or_after(&SIMPLE_CREDENTIAL, earlier_date).is_ok()); + assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, earlier_date).is_ok()); } // test with a few timestamps that should be RFC3339 compatible @@ -682,8 +494,8 @@ mod tests { fn property_based_expires_after_with_expiration_date(seconds in 0..1_000_000_000_u32) { let after_expiration_date = SIMPLE_CREDENTIAL.expiration_date.unwrap().checked_add(Duration::seconds(seconds)).unwrap(); let before_expiration_date = SIMPLE_CREDENTIAL.expiration_date.unwrap().checked_sub(Duration::seconds(seconds)).unwrap(); - assert!(JwtCredentialValidator::check_expires_on_or_after(&SIMPLE_CREDENTIAL, after_expiration_date).is_err()); - assert!(JwtCredentialValidator::check_expires_on_or_after(&SIMPLE_CREDENTIAL, before_expiration_date).is_ok()); + assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, after_expiration_date).is_err()); + assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, before_expiration_date).is_ok()); } } @@ -693,7 +505,7 @@ mod tests { let mut credential = SIMPLE_CREDENTIAL.clone(); credential.expiration_date = None; // expires after whatever the timestamp may be because the expires_after field is None. - assert!(JwtCredentialValidator::check_expires_on_or_after(&credential, Timestamp::from_unix(seconds).unwrap()).is_ok()); + assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&credential, Timestamp::from_unix(seconds).unwrap()).is_ok()); } } @@ -703,8 +515,8 @@ mod tests { let earlier_than_issuance_date = SIMPLE_CREDENTIAL.issuance_date.checked_sub(Duration::seconds(seconds)).unwrap(); let later_than_issuance_date = SIMPLE_CREDENTIAL.issuance_date.checked_add(Duration::seconds(seconds)).unwrap(); - assert!(JwtCredentialValidator::check_issued_on_or_before(&SIMPLE_CREDENTIAL, earlier_than_issuance_date).is_err()); - assert!(JwtCredentialValidator::check_issued_on_or_before(&SIMPLE_CREDENTIAL, later_than_issuance_date).is_ok()); + assert!(JwtCredentialValidatorUtils::check_issued_on_or_before(&SIMPLE_CREDENTIAL, earlier_than_issuance_date).is_err()); + assert!(JwtCredentialValidatorUtils::check_issued_on_or_before(&SIMPLE_CREDENTIAL, later_than_issuance_date).is_ok()); } } } diff --git a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs new file mode 100644 index 0000000000..4b1dc56452 --- /dev/null +++ b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs @@ -0,0 +1,192 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +use std::str::FromStr; + +use identity_core::common::Object; +use identity_core::common::OneOrMany; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use identity_core::convert::FromJson; +use identity_did::CoreDID; +use identity_did::DID; +use identity_document::document::CoreDocument; +use identity_verification::jws::Decoder; + +use super::JwtValidationError; +use super::SignerContext; +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; +use crate::credential::Jwt; +use crate::validator::SubjectHolderRelationship; + +/// Utility functions for verifying JWT credentials. +#[derive(Debug)] +#[non_exhaustive] +pub struct JwtCredentialValidatorUtils; + +type ValidationUnitResult = std::result::Result; + +impl JwtCredentialValidatorUtils { + /// Validates the semantic structure of the [`Credential`]. + /// + /// # Warning + /// This does not validate against the credential's schema nor the structure of the subject claims. + pub fn check_structure(credential: &Credential) -> ValidationUnitResult { + credential + .check_structure() + .map_err(JwtValidationError::CredentialStructure) + } + + /// Validate that the [`Credential`] expires on or after the specified [`Timestamp`]. + pub fn check_expires_on_or_after(credential: &Credential, timestamp: Timestamp) -> ValidationUnitResult { + let expiration_date: Option = credential.expiration_date; + (expiration_date.is_none() || expiration_date >= Some(timestamp)) + .then_some(()) + .ok_or(JwtValidationError::ExpirationDate) + } + + /// Validate that the [`Credential`] is issued on or before the specified [`Timestamp`]. + pub fn check_issued_on_or_before(credential: &Credential, timestamp: Timestamp) -> ValidationUnitResult { + (credential.issuance_date <= timestamp) + .then_some(()) + .ok_or(JwtValidationError::IssuanceDate) + } + + /// Validate that the relationship between the `holder` and the credential subjects is in accordance with + /// `relationship`. + pub fn check_subject_holder_relationship( + credential: &Credential, + holder: &Url, + relationship: SubjectHolderRelationship, + ) -> ValidationUnitResult { + let url_matches: bool = match &credential.credential_subject { + OneOrMany::One(ref credential_subject) => credential_subject.id.as_ref() == Some(holder), + OneOrMany::Many(subjects) => { + // need to check the case where the Many variant holds a vector of exactly one subject + if let [credential_subject] = subjects.as_slice() { + credential_subject.id.as_ref() == Some(holder) + } else { + // zero or > 1 subjects is interpreted to mean that the holder is not the subject + false + } + } + }; + + Some(relationship) + .filter(|relationship| match relationship { + SubjectHolderRelationship::AlwaysSubject => url_matches, + SubjectHolderRelationship::SubjectOnNonTransferable => { + url_matches || !credential.non_transferable.unwrap_or(false) + } + SubjectHolderRelationship::Any => true, + }) + .map(|_| ()) + .ok_or(JwtValidationError::SubjectHolderRelationship) + } + + /// Checks whether the credential status has been revoked. + /// + /// Only supports `RevocationBitmap2022`. + #[cfg(feature = "revocation-bitmap")] + pub fn check_status, T>( + credential: &Credential, + trusted_issuers: &[DOC], + status_check: crate::validator::StatusCheck, + ) -> ValidationUnitResult { + if status_check == crate::validator::StatusCheck::SkipAll { + return Ok(()); + } + + match &credential.credential_status { + None => Ok(()), + Some(status) => { + // Check status is supported. + if status.type_ != crate::revocation::RevocationBitmap::TYPE { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + return Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))); + } + let status: crate::credential::RevocationBitmapStatus = + crate::credential::RevocationBitmapStatus::try_from(status.clone()) + .map_err(JwtValidationError::InvalidStatus)?; + + // Check the credential index against the issuer's DID Document. + let issuer_did: CoreDID = Self::extract_issuer(credential)?; + trusted_issuers + .iter() + .find(|issuer| ::id(issuer.as_ref()) == &issuer_did) + .ok_or(JwtValidationError::DocumentMismatch(SignerContext::Issuer)) + .and_then(|issuer| Self::check_revocation_bitmap_status(issuer, status)) + } + } + } + + /// Check the given `status` against the matching [`RevocationBitmap`] service in the + /// issuer's DID Document. + #[cfg(feature = "revocation-bitmap")] + fn check_revocation_bitmap_status + ?Sized>( + issuer: &DOC, + status: crate::credential::RevocationBitmapStatus, + ) -> ValidationUnitResult { + use crate::revocation::RevocationDocumentExt; + + let issuer_service_url: identity_did::DIDUrl = status.id().map_err(JwtValidationError::InvalidStatus)?; + + // Check whether index is revoked. + let revocation_bitmap: crate::revocation::RevocationBitmap = issuer + .as_ref() + .resolve_revocation_bitmap(issuer_service_url.into()) + .map_err(|_| JwtValidationError::ServiceLookupError)?; + let index: u32 = status.index().map_err(JwtValidationError::InvalidStatus)?; + if revocation_bitmap.is_revoked(index) { + Err(JwtValidationError::Revoked) + } else { + Ok(()) + } + } + + /// Utility for extracting the issuer field of a [`Credential`] as a DID. + /// + /// # Errors + /// + /// Fails if the issuer field is not a valid DID. + pub fn extract_issuer(credential: &Credential) -> std::result::Result + where + D: DID, + ::Err: std::error::Error + Send + Sync + 'static, + { + D::from_str(credential.issuer.url().as_str()).map_err(|err| JwtValidationError::SignerUrl { + signer_ctx: SignerContext::Issuer, + source: err.into(), + }) + } + + /// Utility for extracting the issuer field of a credential in JWT representation as DID. + /// + /// # Errors + /// + /// If the JWT decoding fails or the issuer field is not a valid DID. + pub fn extract_issuer_from_jwt(credential: &Jwt) -> std::result::Result + where + D: DID, + ::Err: std::error::Error + Send + Sync + 'static, + { + let validation_item = Decoder::new() + .decode_compact_serialization(credential.as_str().as_bytes(), None) + .map_err(JwtValidationError::JwsDecodingError)?; + + let claims: CredentialJwtClaims<'_, Object> = CredentialJwtClaims::from_json_slice(&validation_item.claims()) + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + D::from_str(claims.iss.url().as_str()).map_err(|err| JwtValidationError::SignerUrl { + signer_ctx: SignerContext::Issuer, + source: err.into(), + }) + } +} diff --git a/identity_credential/src/validator/vc_jwt_validation/mod.rs b/identity_credential/src/validator/jwt_credential_validation/mod.rs similarity index 50% rename from identity_credential/src/validator/vc_jwt_validation/mod.rs rename to identity_credential/src/validator/jwt_credential_validation/mod.rs index 0b884d17b0..25c0d4f494 100644 --- a/identity_credential/src/validator/vc_jwt_validation/mod.rs +++ b/identity_credential/src/validator/jwt_credential_validation/mod.rs @@ -2,12 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 //! Contains functionality for validating credentials issued as JWTs. -mod credential_jwt_validation_options; -mod credential_jwt_validator; mod decoded_jwt_credential; mod error; +mod jwt_credential_validation_options; +mod jwt_credential_validator; +mod jwt_credential_validator_utils; -pub use credential_jwt_validation_options::*; -pub use credential_jwt_validator::*; pub use decoded_jwt_credential::*; pub use error::*; +pub use jwt_credential_validation_options::*; +pub use jwt_credential_validator::*; +pub use jwt_credential_validator_utils::*; diff --git a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs b/identity_credential/src/validator/jwt_presentation_validation/decoded_jwt_presentation.rs similarity index 100% rename from identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs rename to identity_credential/src/validator/jwt_presentation_validation/decoded_jwt_presentation.rs diff --git a/identity_credential/src/validator/vp_jwt_validation/error.rs b/identity_credential/src/validator/jwt_presentation_validation/error.rs similarity index 94% rename from identity_credential/src/validator/vp_jwt_validation/error.rs rename to identity_credential/src/validator/jwt_presentation_validation/error.rs index 4ab609fc9d..f0c413d2ce 100644 --- a/identity_credential/src/validator/vp_jwt_validation/error.rs +++ b/identity_credential/src/validator/jwt_presentation_validation/error.rs @@ -4,7 +4,7 @@ use std::error::Error; use std::fmt::Display; -use crate::validator::vc_jwt_validation::JwtValidationError; +use crate::validator::jwt_credential_validation::JwtValidationError; /// Errors caused by a failure to validate a [`Presentation`](crate::presentation::Presentation). #[derive(Debug)] diff --git a/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validation_options.rs b/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validation_options.rs similarity index 100% rename from identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validation_options.rs rename to identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validation_options.rs diff --git a/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs b/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator.rs similarity index 75% rename from identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs rename to identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator.rs index 3fdd5629ac..049b0e129c 100644 --- a/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs +++ b/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator.rs @@ -1,24 +1,20 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use identity_core::common::Object; use identity_core::common::Timestamp; use identity_core::common::Url; use identity_core::convert::FromJson; use identity_did::CoreDID; -use identity_did::DID; use identity_document::document::CoreDocument; use identity_verification::jws::DecodedJws; -use identity_verification::jws::Decoder; -use identity_verification::jws::EdDSAJwsVerifier; use identity_verification::jws::JwsVerifier; use std::str::FromStr; use crate::credential::Jwt; use crate::presentation::Presentation; use crate::presentation::PresentationJwtClaims; -use crate::validator::vc_jwt_validation::JwtValidationError; -use crate::validator::vc_jwt_validation::SignerContext; +use crate::validator::jwt_credential_validation::JwtValidationError; +use crate::validator::jwt_credential_validation::SignerContext; use super::CompoundJwtPresentationValidationError; use super::DecodedJwtPresentation; @@ -27,19 +23,7 @@ use super::JwtPresentationValidationOptions; /// Struct for validating [`Presentation`]. #[derive(Debug, Clone)] #[non_exhaustive] -pub struct JwtPresentationValidator(V); - -impl JwtPresentationValidator { - /// Creates a new [`JwtPresentationValidator`]. - pub fn new() -> Self { - Self(EdDSAJwsVerifier::default()) - } -} -impl Default for JwtPresentationValidator { - fn default() -> Self { - Self::new() - } -} +pub struct JwtPresentationValidator(V); impl JwtPresentationValidator where @@ -177,37 +161,3 @@ where Ok(decoded_jwt_presentation) } } - -impl JwtPresentationValidator { - /// Attempt to extract the holder of the presentation. - /// - /// # Errors: - /// * If deserialization/decoding of the presentation fails. - /// * If the holder can't be parsed as DIDs. - pub fn extract_holder(presentation: &Jwt) -> std::result::Result - where - ::Err: std::error::Error + Send + Sync + 'static, - { - let validation_item = Decoder::new() - .decode_compact_serialization(presentation.as_str().as_bytes(), None) - .map_err(JwtValidationError::JwsDecodingError)?; - - let claims: PresentationJwtClaims<'_, identity_core::common::Value, Object> = - PresentationJwtClaims::from_json_slice(&validation_item.claims()).map_err(|err| { - JwtValidationError::PresentationStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) - })?; - - let holder: H = H::from_str(claims.iss.as_str()).map_err(|err| JwtValidationError::SignerUrl { - signer_ctx: SignerContext::Holder, - source: err.into(), - })?; - Ok(holder) - } - - /// Validates the semantic structure of the `Presentation`. - pub fn check_structure(presentation: &Presentation) -> Result<(), JwtValidationError> { - presentation - .check_structure() - .map_err(JwtValidationError::PresentationStructure) - } -} diff --git a/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator_utils.rs b/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator_utils.rs new file mode 100644 index 0000000000..91468acddd --- /dev/null +++ b/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator_utils.rs @@ -0,0 +1,52 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Object; +use identity_core::convert::FromJson; +use identity_did::DID; +use identity_verification::jws::Decoder; +use std::str::FromStr; + +use crate::credential::Jwt; +use crate::presentation::Presentation; +use crate::presentation::PresentationJwtClaims; +use crate::validator::jwt_credential_validation::JwtValidationError; +use crate::validator::jwt_credential_validation::SignerContext; + +/// Utility functions for verifying JWT presentations. +#[non_exhaustive] +pub struct JwtPresentationValidatorUtils; + +impl JwtPresentationValidatorUtils { + /// Attempt to extract the holder of the presentation. + /// + /// # Errors: + /// * If deserialization/decoding of the presentation fails. + /// * If the holder can't be parsed as DIDs. + pub fn extract_holder(presentation: &Jwt) -> std::result::Result + where + ::Err: std::error::Error + Send + Sync + 'static, + { + let validation_item = Decoder::new() + .decode_compact_serialization(presentation.as_str().as_bytes(), None) + .map_err(JwtValidationError::JwsDecodingError)?; + + let claims: PresentationJwtClaims<'_, identity_core::common::Value, Object> = + PresentationJwtClaims::from_json_slice(&validation_item.claims()).map_err(|err| { + JwtValidationError::PresentationStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + let holder: H = H::from_str(claims.iss.as_str()).map_err(|err| JwtValidationError::SignerUrl { + signer_ctx: SignerContext::Holder, + source: err.into(), + })?; + Ok(holder) + } + + /// Validates the semantic structure of the `Presentation`. + pub fn check_structure(presentation: &Presentation) -> Result<(), JwtValidationError> { + presentation + .check_structure() + .map_err(JwtValidationError::PresentationStructure) + } +} diff --git a/identity_credential/src/validator/vp_jwt_validation/mod.rs b/identity_credential/src/validator/jwt_presentation_validation/mod.rs similarity index 80% rename from identity_credential/src/validator/vp_jwt_validation/mod.rs rename to identity_credential/src/validator/jwt_presentation_validation/mod.rs index c5dccc7db2..a9bb144080 100644 --- a/identity_credential/src/validator/vp_jwt_validation/mod.rs +++ b/identity_credential/src/validator/jwt_presentation_validation/mod.rs @@ -5,8 +5,10 @@ mod decoded_jwt_presentation; mod error; mod jwt_presentation_validation_options; mod jwt_presentation_validator; +mod jwt_presentation_validator_utils; pub use decoded_jwt_presentation::*; pub use error::*; pub use jwt_presentation_validation_options::*; pub use jwt_presentation_validator::*; +pub use jwt_presentation_validator_utils::*; diff --git a/identity_credential/src/validator/mod.rs b/identity_credential/src/validator/mod.rs index 4e651095b1..c630f6c2ed 100644 --- a/identity_credential/src/validator/mod.rs +++ b/identity_credential/src/validator/mod.rs @@ -3,14 +3,14 @@ //! Verifiable Credential and Presentation validators. +pub use self::jwt_credential_validation::*; +pub use self::jwt_presentation_validation::*; pub use self::options::FailFast; pub use self::options::StatusCheck; pub use self::options::SubjectHolderRelationship; -pub use self::vc_jwt_validation::*; -pub use self::vp_jwt_validation::*; +mod jwt_credential_validation; +mod jwt_presentation_validation; mod options; #[cfg(test)] pub(crate) mod test_utils; -mod vc_jwt_validation; -mod vp_jwt_validation; diff --git a/identity_eddsa_verifier/Cargo.toml b/identity_eddsa_verifier/Cargo.toml new file mode 100644 index 0000000000..9a69b0fe8f --- /dev/null +++ b/identity_eddsa_verifier/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "identity_eddsa_verifier" +version = "0.7.0-alpha.7" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords = ["iota", "identity", "jose", "jwk", "jws"] +license.workspace = true +readme = "./README.md" +repository.workspace = true +rust-version.workspace = true +description = "JWS EdDSA signature verification for IOTA Identity" + +[dependencies] +identity_jose = { version = "=0.7.0-alpha.7", path = "../identity_jose", default-features = false } +iota-crypto = { version = "0.23", default-features = false, features = ["std"] } + +[features] +ed25519 = ["iota-crypto/ed25519"] +default = ["ed25519"] diff --git a/identity_eddsa_verifier/README.md b/identity_eddsa_verifier/README.md new file mode 100644 index 0000000000..a4599bbf8f --- /dev/null +++ b/identity_eddsa_verifier/README.md @@ -0,0 +1,5 @@ +IOTA Identity - EdDSA Verifier +=== + +This crate implements a `JwsVerifier` capable of verifying EdDSA signatures. + diff --git a/identity_eddsa_verifier/src/ed25519_verifier.rs b/identity_eddsa_verifier/src/ed25519_verifier.rs new file mode 100644 index 0000000000..16d939ddb9 --- /dev/null +++ b/identity_eddsa_verifier/src/ed25519_verifier.rs @@ -0,0 +1,75 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; + +use identity_jose::jwk::EdCurve; +use identity_jose::jwk::Jwk; +use identity_jose::jwk::JwkParamsOkp; +use identity_jose::jws::SignatureVerificationError; +use identity_jose::jws::SignatureVerificationErrorKind; +use identity_jose::jws::VerificationInput; + +/// A verifier that can handle the [`JwsAlgorithm::EdDSA`](identity_jose::jws::JwsAlgorithm::EdDSA) algorithm with curve +/// [`EdCurve::Ed25519`]. +#[derive(Debug)] +#[non_exhaustive] +pub struct Ed25519Verifier; + +impl Ed25519Verifier { + /// Verify a JWS signature secured with the [`JwsAlgorithm::EdDSA`](identity_jose::jws::JwsAlgorithm::EdDSA) + /// algorithm and curve [`EdCurve::Ed25519`]. This associated method is only available when the + /// `ed25519` feature is enabled. + /// + /// This function is useful when one is composing a [`JwsVerifier`](identity_jose::jws::JwsVerifier) that delegates + /// [`JwsAlgorithm::EdDSA`](identity_jose::jws::JwsAlgorithm::EdDSA) verification with + /// curve [`EdCurve::Ed25519`] to this function. + /// + /// # Warning + /// + /// This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this + /// prior to calling the function. + pub fn verify(input: VerificationInput, public_key: &Jwk) -> Result<(), SignatureVerificationError> { + // Obtain an Ed25519 public key. + let params: &JwkParamsOkp = public_key + .try_okp_params() + .map_err(|_| SignatureVerificationErrorKind::UnsupportedKeyType)?; + + if params + .try_ed_curve() + .ok() + .filter(|curve_param| *curve_param == EdCurve::Ed25519) + .is_none() + { + return Err(SignatureVerificationErrorKind::UnsupportedKeyParams.into()); + } + + let pk: [u8; crypto::signatures::ed25519::PublicKey::LENGTH] = identity_jose::jwu::decode_b64(params.x.as_str()) + .map_err(|_| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure) + .with_custom_message("could not decode x parameter from jwk") + }) + .and_then(|value| { + TryInto::try_into(value).map_err(|_| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure) + .with_custom_message("invalid public key length") + }) + })?; + + let public_key_ed25519 = crypto::signatures::ed25519::PublicKey::try_from(pk).map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) + })?; + + let signature_arr = + <[u8; crypto::signatures::ed25519::Signature::LENGTH]>::try_from(input.decoded_signature.deref()) + .map_err(|_| SignatureVerificationErrorKind::InvalidSignature)?; + + let signature = crypto::signatures::ed25519::Signature::from_bytes(signature_arr); + + if crypto::signatures::ed25519::PublicKey::verify(&public_key_ed25519, &signature, &input.signing_input) { + Ok(()) + } else { + Err(SignatureVerificationErrorKind::InvalidSignature.into()) + } + } +} diff --git a/identity_eddsa_verifier/src/eddsa_verifier.rs b/identity_eddsa_verifier/src/eddsa_verifier.rs new file mode 100644 index 0000000000..a61e100568 --- /dev/null +++ b/identity_eddsa_verifier/src/eddsa_verifier.rs @@ -0,0 +1,35 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_jose::jwk::Jwk; +use identity_jose::jws::JwsVerifier; +use identity_jose::jws::SignatureVerificationError; +use identity_jose::jws::SignatureVerificationErrorKind; +use identity_jose::jws::VerificationInput; + +/// An implementor of [`JwsVerifier`] that can handle the +/// [`JwsAlgorithm::EdDSA`](identity_jose::jws::JwsAlgorithm::EdDSA) algorithm. +#[derive(Debug)] +#[non_exhaustive] +pub struct EdDSAJwsVerifier; + +impl Default for EdDSAJwsVerifier { + /// Constructs an [`EdDSAJwsVerifier`]. This is the only way to obtain an [`EdDSAJwsVerifier`]. + fn default() -> Self { + Self + } +} + +impl JwsVerifier for EdDSAJwsVerifier { + /// This implements verification of JWS signatures signed with the + /// [`JwsAlgorithm::EdDSA`](identity_jose::jws::JwsAlgorithm::EdDSA) algorithm. + // Allow unused variables in case of no-default-features. + #[allow(unused_variables)] + fn verify(&self, input: VerificationInput, public_key: &Jwk) -> std::result::Result<(), SignatureVerificationError> { + match input.alg { + #[cfg(feature = "ed25519")] + identity_jose::jws::JwsAlgorithm::EdDSA => crate::Ed25519Verifier::verify(input, public_key), + _ => Err(SignatureVerificationErrorKind::UnsupportedAlg.into()), + } + } +} diff --git a/identity_eddsa_verifier/src/lib.rs b/identity_eddsa_verifier/src/lib.rs new file mode 100644 index 0000000000..8c9aa2e9cd --- /dev/null +++ b/identity_eddsa_verifier/src/lib.rs @@ -0,0 +1,10 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "ed25519")] +mod ed25519_verifier; +mod eddsa_verifier; + +#[cfg(feature = "ed25519")] +pub use ed25519_verifier::*; +pub use eddsa_verifier::*; diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index c09b767ce9..28ade1d6e5 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -28,7 +28,7 @@ rand = "0.8.5" tokio = { version = "1.29.0", features = ["full"] } [features] -default = ["revocation-bitmap", "client", "iota-client", "resolver", "eddsa"] +default = ["revocation-bitmap", "client", "iota-client", "resolver"] # Exposes the `IotaIdentityClient` and `IotaIdentityClientExt` traits. client = ["identity_iota_core/client"] @@ -55,15 +55,9 @@ domain-linkage = ["identity_credential/domain-linkage"] # Enables fetching domain linkage configuration files. domain-linkage-fetch = ["identity_credential/domain-linkage-fetch"] -# Enables JWS verification with the EdDSA algorithm -eddsa = ["identity_verification/eddsa"] - # Exposes in-memory implementations of the storage traits intended exclusively for testing. memstore = ["identity_storage/memstore"] -# Exposes secure implementations of the storage traits using stronghold. -stronghold = ["identity_storage/stronghold"] - [package.metadata.docs.rs] # To build locally: # RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps --workspace --open diff --git a/identity_jose/Cargo.toml b/identity_jose/Cargo.toml index bacddec403..79eb781324 100644 --- a/identity_jose/Cargo.toml +++ b/identity_jose/Cargo.toml @@ -20,11 +20,6 @@ subtle = { version = "2.5", default-features = false } thiserror.workspace = true zeroize = { version = "1.6", default-features = false, features = ["std", "zeroize_derive"] } -[features] -# Enables jws verification based on the EdDSA algorithm. -eddsa = ["iota-crypto/ed25519"] -default = ["eddsa"] - [dev-dependencies] anyhow = "1" iota-crypto = { version = "0.23", features = ["ed25519", "random", "hmac"] } diff --git a/identity_jose/src/jws/custom_verification/jws_verifier.rs b/identity_jose/src/jws/custom_verification/jws_verifier.rs index ff6a7ddcc1..219d900b53 100644 --- a/identity_jose/src/jws/custom_verification/jws_verifier.rs +++ b/identity_jose/src/jws/custom_verification/jws_verifier.rs @@ -5,9 +5,6 @@ use super::SignatureVerificationError; use crate::jwk::Jwk; use crate::jws::JwsAlgorithm; -#[cfg(any(feature = "eddsa", doc))] -pub use eddsa_verifier::*; - /// Input a [`JwsVerifier`] verifies. pub struct VerificationInput { /// The `alg` parsed from the protected header. @@ -53,6 +50,12 @@ pub trait JwsVerifier { fn verify(&self, input: VerificationInput, public_key: &Jwk) -> Result<(), SignatureVerificationError>; } +impl JwsVerifier for Box { + fn verify(&self, input: VerificationInput, public_key: &Jwk) -> Result<(), SignatureVerificationError> { + ::verify(self, input, public_key) + } +} + // ================================================================================================================= // Implementation // ================================================================================================================ @@ -79,105 +82,3 @@ where self.0(input, public_key) } } - -#[cfg(any(feature = "eddsa", doc))] -mod eddsa_verifier { - use std::ops::Deref; - - use super::*; - use crate::jwk::EdCurve; - use crate::jwk::JwkParamsOkp; - use crate::jws::SignatureVerificationErrorKind; - - /// An implementor of [`JwsVerifier`] that can handle the - /// [`JwsAlgorithm::EdDSA`](crate::jws::JwsAlgorithm::EdDSA) algorithm. - /// - /// See [`Self::verify`](EdDSAJwsVerifier::verify). - /// - /// NOTE: This type can only be constructed when the `eddsa` feature is enabled. - #[derive(Debug)] - #[non_exhaustive] - pub struct EdDSAJwsVerifier; - - impl EdDSAJwsVerifier { - /// Verify a JWS signature secured with the [`JwsAlgorithm::EdDSA`](crate::jws::JwsAlgorithm::EdDSA) algorithm. - /// Only the [`EdCurve::Ed25519`] variant is supported for now. This associated method is only available when the - /// `eddsa` feature is enabled. - /// - /// This function is useful when one is building a [`JwsVerifier`] that handles the - /// [`JwsAlgorithm::EdDSA`](crate::jws::JwsAlgorithm::EdDSA) in the same manner as the [`EdDSAJwsVerifier`] - /// hence extending its capabilities. - /// - /// # Warning - /// This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this - /// prior to calling the function. - pub fn verify_eddsa(input: VerificationInput, public_key: &Jwk) -> Result<(), SignatureVerificationError> { - // Obtain an Ed25519 public key - let params: &JwkParamsOkp = public_key - .try_okp_params() - .map_err(|_| SignatureVerificationErrorKind::UnsupportedKeyType)?; - - if params - .try_ed_curve() - .ok() - .filter(|curve_param| *curve_param == EdCurve::Ed25519) - .is_none() - { - return Err(SignatureVerificationErrorKind::UnsupportedKeyParams.into()); - } - - let pk: [u8; crypto::signatures::ed25519::PublicKey::LENGTH] = crate::jwu::decode_b64(params.x.as_str()) - .map_err(|_| { - SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure) - .with_custom_message("could not decode x parameter from jwk") - }) - .and_then(|value| { - TryInto::try_into(value).map_err(|_| { - SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure) - .with_custom_message("invalid public key length") - }) - })?; - - let public_key_ed25519 = crypto::signatures::ed25519::PublicKey::try_from(pk).map_err(|err| { - SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) - })?; - - let signature_arr = - <[u8; crypto::signatures::ed25519::Signature::LENGTH]>::try_from(input.decoded_signature.deref()) - .map_err(|_| SignatureVerificationErrorKind::InvalidSignature)?; - - let signature = crypto::signatures::ed25519::Signature::from_bytes(signature_arr); - - if crypto::signatures::ed25519::PublicKey::verify(&public_key_ed25519, &signature, &input.signing_input) { - Ok(()) - } else { - Err(SignatureVerificationErrorKind::InvalidSignature.into()) - } - } - } - - impl Default for EdDSAJwsVerifier { - /// Constructs a [`EdDSAJwsVerifier`]. This is the only way to obtain an [`EdDSAJwsVerifier`] and - /// is only available when the `eddsa` feature is set. - fn default() -> Self { - Self - } - } - - impl JwsVerifier for EdDSAJwsVerifier { - /// This implements verification of jws signatures signed with the `EdDSA` algorithm. For now - /// [`EdDSAJwsVerifier::verify`](EdDSAJwsVerifier::verify) can only handle the `alg = EdDSA` with - /// `crv = Ed25519`, but the implementation may also support `crv = Ed448` in the future. - fn verify( - &self, - input: VerificationInput, - public_key: &Jwk, - ) -> std::result::Result<(), SignatureVerificationError> { - match input.alg { - // EdDSA is the only supported algorithm for now, we can consider supporting more by default in the future. - JwsAlgorithm::EdDSA => EdDSAJwsVerifier::verify_eddsa(input, public_key), - _ => Err(SignatureVerificationErrorKind::UnsupportedAlg.into()), - } - } - } -} diff --git a/identity_jose/src/tests/rfc8037.rs b/identity_jose/src/tests/rfc8037.rs index 2e76eeb970..aada7a7369 100644 --- a/identity_jose/src/tests/rfc8037.rs +++ b/identity_jose/src/tests/rfc8037.rs @@ -4,8 +4,6 @@ use crate::jwk::Jwk; use crate::jws::CompactJwsEncoder; use crate::jws::Decoder; -#[cfg(feature = "eddsa")] -use crate::jws::EdDSAJwsVerifier; use crate::jws::JwsAlgorithm; use crate::jws::JwsHeader; use crate::jws::JwsVerifierFn; @@ -54,10 +52,15 @@ fn test_rfc8037_ed25519() { #[cfg(feature = "eddsa")] { + let jws_signature_verifier = JwsVerifierFn::from(|input: VerificationInput, key: &Jwk| match input.alg { + JwsAlgorithm::EdDSA => ed25519::verify(input, key), + other => unimplemented!("{other}"), + }); + let decoder = Decoder::new(); let token_with_default = decoder .decode_compact_serialization(jws.as_bytes(), None) - .and_then(|decoded| decoded.verify(&EdDSAJwsVerifier::default(), &public)) + .and_then(|decoded| decoded.verify(&jws_signature_verifier, &public)) .unwrap(); assert_eq!(token, token_with_default); } diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index 373a4a5503..de995e8606 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -33,13 +33,14 @@ zeroize = { version = "1.6.0", default_features = false, optional = true } [dev-dependencies] identity_credential = { version = "=0.7.0-alpha.7", path = "../identity_credential", features = ["revocation-bitmap"] } +identity_eddsa_verifier = { version = "=0.7.0-alpha.7", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } once_cell = { version = "1.18", default-features = false } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } [features] default = ["iota-document", "memstore"] # Exposes in-memory implementations of the storage traits intended exclusively for testing. -memstore = ["dep:tokio", "dep:rand", "dep:iota-crypto", "identity_verification/eddsa"] +memstore = ["dep:tokio", "dep:rand", "dep:iota-crypto"] # Enables `Send` + `Sync` bounds for the storage traits. send-sync-storage = [] # Implements the JwkStorageDocumentExt trait for IotaDocument diff --git a/identity_storage/src/storage/tests/api.rs b/identity_storage/src/storage/tests/api.rs index 2f477ffc98..e442755ad7 100644 --- a/identity_storage/src/storage/tests/api.rs +++ b/identity_storage/src/storage/tests/api.rs @@ -10,7 +10,7 @@ use identity_credential::validator::JwtCredentialValidationOptions; use identity_did::DIDUrl; use identity_document::document::CoreDocument; use identity_document::verifiable::JwsVerificationOptions; -use identity_verification::jose::jws::EdDSAJwsVerifier; +use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_verification::jose::jws::JwsAlgorithm; use identity_verification::jwk::Jwk; use identity_verification::jws::DecodedJws; @@ -321,7 +321,8 @@ async fn signing_credential() { .await .unwrap(); // Verify the credential - let validator = identity_credential::validator::JwtCredentialValidator::new(); + let validator = + identity_credential::validator::JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); assert!(validator .validate::<_, Object>( &jws, diff --git a/identity_storage/src/storage/tests/credential_jws.rs b/identity_storage/src/storage/tests/credential_jws.rs index 6a30c66b76..750733c776 100644 --- a/identity_storage/src/storage/tests/credential_jws.rs +++ b/identity_storage/src/storage/tests/credential_jws.rs @@ -8,6 +8,7 @@ use identity_credential::credential::Credential; use identity_credential::validator::JwtCredentialValidationOptions; use identity_document::document::CoreDocument; use identity_document::verifiable::JwsVerificationOptions; +use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_verification::jose::jws::JwsAlgorithm; use identity_verification::MethodScope; @@ -105,7 +106,8 @@ async fn signing_credential_with_nonce_and_scope() { .await .unwrap(); - let validator = identity_credential::validator::JwtCredentialValidator::new(); + let validator = + identity_credential::validator::JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); assert!(validator .validate::<_, Object>( &jws, @@ -163,7 +165,8 @@ async fn signing_credential_with_b64() { .await .unwrap(); - let validator = identity_credential::validator::JwtCredentialValidator::new(); + let validator = + identity_credential::validator::JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); let decoded = validator .validate::<_, Object>( &jws, @@ -211,7 +214,8 @@ async fn custom_claims() { .await .unwrap(); - let validator = identity_credential::validator::JwtCredentialValidator::new(); + let validator = + identity_credential::validator::JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); let decoded = validator .validate::<_, Object>( &jws, diff --git a/identity_storage/src/storage/tests/credential_validation.rs b/identity_storage/src/storage/tests/credential_validation.rs index 94f5705c67..b1b3d873c8 100644 --- a/identity_storage/src/storage/tests/credential_validation.rs +++ b/identity_storage/src/storage/tests/credential_validation.rs @@ -13,12 +13,15 @@ use identity_credential::revocation::RevocationDocumentExt; use identity_credential::validator::FailFast; use identity_credential::validator::JwtCredentialValidationOptions; use identity_credential::validator::JwtCredentialValidator; +use identity_credential::validator::JwtCredentialValidatorUtils; use identity_credential::validator::JwtValidationError; use identity_credential::validator::StatusCheck; use identity_did::DID; use identity_document::document::CoreDocument; use identity_document::service::Service; use identity_document::verifiable::JwsVerificationOptions; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use once_cell::sync::Lazy; use crate::storage::tests::test_utils; use crate::storage::tests::test_utils::CredentialSetup; @@ -26,6 +29,9 @@ use crate::storage::tests::test_utils::Setup; use crate::storage::JwkDocumentExt; use crate::storage::JwsSignatureOptions; +static JWT_CREDENTIAL_VALIDATOR_ED25519: Lazy> = + Lazy::new(|| JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default())); + async fn invalid_expiration_or_issuance_date_impl(setup: Setup) where T: JwkDocumentExt + AsRef, @@ -66,7 +72,7 @@ where .earliest_expiry_date(expires_on_or_after); // validate and extract the nested error according to our expectations - let validation_errors = JwtCredentialValidator::new() + let validation_errors = JWT_CREDENTIAL_VALIDATOR_ED25519 .validate::<_, Object>(&jws, &issuer_doc, &options, FailFast::FirstError) .unwrap_err() .validation_errors; @@ -89,7 +95,7 @@ where .earliest_expiry_date(expires_on_or_after); // validate and extract the nested error according to our expectations - let validation_errors = JwtCredentialValidator::new() + let validation_errors = JWT_CREDENTIAL_VALIDATOR_ED25519 .validate::<_, Object>(&jws, &issuer_doc, &options, FailFast::FirstError) .unwrap_err() .validation_errors; @@ -144,7 +150,7 @@ where let options = JwtCredentialValidationOptions::default() .latest_issuance_date(issued_on_or_before) .earliest_expiry_date(expires_on_or_after); - assert!(JwtCredentialValidator::new() + assert!(JWT_CREDENTIAL_VALIDATOR_ED25519 .validate::<_, Object>(&jwt, &issuer_doc, &options, FailFast::FirstError) .is_ok()); } @@ -183,7 +189,7 @@ where // the credential was not signed by this issuer // check that `verify_signature` returns the expected error - let error = JwtCredentialValidator::new() + let error = JWT_CREDENTIAL_VALIDATOR_ED25519 .verify_signature::<_, Object>(&jwt, &[&subject_doc], &JwsVerificationOptions::default()) .unwrap_err(); @@ -193,7 +199,7 @@ where let options = JwtCredentialValidationOptions::default(); // validate and extract the nested error according to our expectations - let validation_errors = JwtCredentialValidator::new() + let validation_errors = JWT_CREDENTIAL_VALIDATOR_ED25519 .validate::<_, Object>(&jwt, &subject_doc, &options, FailFast::FirstError) .unwrap_err() .validation_errors; @@ -246,7 +252,7 @@ where .await .unwrap(); - let err = JwtCredentialValidator::new() + let err = JWT_CREDENTIAL_VALIDATOR_ED25519 .verify_signature::<_, Object>(&jwt, &[&issuer_doc], &JwsVerificationOptions::default()) .unwrap_err(); @@ -262,7 +268,7 @@ where .earliest_expiry_date(expires_on_or_after); // validate and extract the nested error according to our expectations - let validation_errors = JwtCredentialValidator::new() + let validation_errors = JWT_CREDENTIAL_VALIDATOR_ED25519 .validate::<_, Object>(&jwt, &issuer_doc, &options, FailFast::FirstError) .unwrap_err() .validation_errors; @@ -309,7 +315,7 @@ where // 0: missing status always succeeds. for status_check in [StatusCheck::Strict, StatusCheck::SkipUnsupported, StatusCheck::SkipAll] { - assert!(JwtCredentialValidator::check_status(&credential, &[&issuer_doc], status_check).is_ok()); + assert!(JwtCredentialValidatorUtils::check_status(&credential, &[&issuer_doc], status_check).is_ok()); } // 1: unsupported status type. @@ -323,7 +329,7 @@ where (StatusCheck::SkipAll, true), ] { assert_eq!( - JwtCredentialValidator::check_status(&credential, &[&issuer_doc], status_check).is_ok(), + JwtCredentialValidatorUtils::check_status(&credential, &[&issuer_doc], status_check).is_ok(), expected ); } @@ -340,7 +346,7 @@ where (StatusCheck::SkipAll, true), ] { assert_eq!( - JwtCredentialValidator::check_status(&credential, &[&issuer_doc], status_check).is_ok(), + JwtCredentialValidatorUtils::check_status(&credential, &[&issuer_doc], status_check).is_ok(), expected ); } @@ -352,7 +358,7 @@ where // 3: un-revoked index always succeeds. for status_check in [StatusCheck::Strict, StatusCheck::SkipUnsupported, StatusCheck::SkipAll] { - assert!(JwtCredentialValidator::check_status(&credential, &[&issuer_doc], status_check).is_ok()); + assert!(JwtCredentialValidatorUtils::check_status(&credential, &[&issuer_doc], status_check).is_ok()); } // 4: revoked index. @@ -363,7 +369,7 @@ where (StatusCheck::SkipAll, true), ] { assert_eq!( - JwtCredentialValidator::check_status(&credential, &[&issuer_doc], status_check).is_ok(), + JwtCredentialValidatorUtils::check_status(&credential, &[&issuer_doc], status_check).is_ok(), expected ); } @@ -419,14 +425,14 @@ where .latest_issuance_date(issued_on_or_before) .earliest_expiry_date(expires_on_or_after); - let validation_errors = JwtCredentialValidator::new() + let validation_errors = JWT_CREDENTIAL_VALIDATOR_ED25519 .validate::<_, Object>(&jws, &issuer_doc, &options, FailFast::FirstError) .unwrap_err() .validation_errors; assert!(validation_errors.len() == 1); - let validation_errors = JwtCredentialValidator::new() + let validation_errors = JWT_CREDENTIAL_VALIDATOR_ED25519 .validate::<_, Object>(&jws, &issuer_doc, &options, FailFast::AllErrors) .unwrap_err() .validation_errors; diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index aab5cb2f74..911333f9e7 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -14,12 +14,15 @@ use identity_credential::presentation::PresentationBuilder; use identity_credential::validator::DecodedJwtPresentation; use identity_credential::validator::JwtPresentationValidationOptions; use identity_credential::validator::JwtPresentationValidator; +use identity_credential::validator::JwtPresentationValidatorUtils; use identity_credential::validator::JwtValidationError; use identity_did::CoreDID; use identity_did::DID; use identity_document::document::CoreDocument; +use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_verification::jws::JwsAlgorithm; use identity_verification::MethodScope; +use once_cell::sync::Lazy; use crate::key_storage::JwkMemStore; use crate::storage::tests::test_utils::generate_credential; @@ -31,6 +34,9 @@ use crate::JwsSignatureOptions; use super::test_utils::CredentialSetup; +static JWT_PRESENTATION_VALIDATOR_ED25519: Lazy> = + Lazy::new(|| JwtPresentationValidator::with_signature_verifier(EdDSAJwsVerifier::default())); + #[tokio::test] async fn test_valid_presentation() { test_valid_presentation_impl(setup_coredocument(None, None).await).await; @@ -67,8 +73,7 @@ where .await .unwrap(); - let validator: JwtPresentationValidator = JwtPresentationValidator::new(); - let decoded_presentation: DecodedJwtPresentation = validator + let decoded_presentation: DecodedJwtPresentation = JWT_PRESENTATION_VALIDATOR_ED25519 .validate::<_, Jwt, Object>( &presentation_jwt, &setup.subject_doc, @@ -125,11 +130,10 @@ where .await .unwrap(); - let holder = JwtPresentationValidator::extract_holder::(&presentation_jwt).unwrap(); + let holder = JwtPresentationValidatorUtils::extract_holder::(&presentation_jwt).unwrap(); assert_eq!(holder.to_url(), setup.subject_doc.as_ref().id().to_url()); - let validator: JwtPresentationValidator = JwtPresentationValidator::new(); - let decoded_presentation: DecodedJwtPresentation = validator + let decoded_presentation: DecodedJwtPresentation = JWT_PRESENTATION_VALIDATOR_ED25519 .validate::<_, _, Object>( &presentation_jwt, &setup.subject_doc, @@ -207,8 +211,8 @@ where ) .await .unwrap(); - let validator: JwtPresentationValidator = JwtPresentationValidator::new(); - let validation_error: JwtValidationError = validator + + let validation_error: JwtValidationError = JWT_PRESENTATION_VALIDATOR_ED25519 .validate::<_, Jwt, Object>( &presentation_jwt, &setup.subject_doc, @@ -264,8 +268,7 @@ where .await .unwrap(); - let validator: JwtPresentationValidator = JwtPresentationValidator::new(); - let validation_error: JwtValidationError = validator + let validation_error: JwtValidationError = JWT_PRESENTATION_VALIDATOR_ED25519 .validate::<_, Jwt, Object>( &presentation_jwt, &setup.subject_doc, @@ -286,7 +289,7 @@ where validation_options = validation_options.earliest_expiry_date(Timestamp::now_utc().checked_sub(Duration::days(2)).unwrap()); - validator + JWT_PRESENTATION_VALIDATOR_ED25519 .validate::<_, Jwt, Object>(&presentation_jwt, &setup.subject_doc, &validation_options) .unwrap(); } @@ -329,8 +332,7 @@ where .await .unwrap(); - let validator: JwtPresentationValidator = JwtPresentationValidator::new(); - let validation_error: JwtValidationError = validator + let validation_error: JwtValidationError = JWT_PRESENTATION_VALIDATOR_ED25519 .validate::<_, Jwt, Object>( &presentation_jwt, &setup.subject_doc, @@ -350,7 +352,7 @@ where validation_options = validation_options.latest_issuance_date(Timestamp::now_utc().checked_add(Duration::hours(2)).unwrap()); - let validation_ok: bool = validator + let validation_ok: bool = JWT_PRESENTATION_VALIDATOR_ED25519 .validate::<_, Jwt, Object>(&presentation_jwt, &setup.subject_doc, &validation_options) .is_ok(); assert!(validation_ok); @@ -390,8 +392,7 @@ where .await .unwrap(); - let validator: JwtPresentationValidator = JwtPresentationValidator::new(); - let validation_error: JwtValidationError = validator + let validation_error: JwtValidationError = JWT_PRESENTATION_VALIDATOR_ED25519 .validate::<_, Jwt, Object>( &presentation_jwt, &setup.subject_doc, diff --git a/identity_verification/Cargo.toml b/identity_verification/Cargo.toml index f54a00a717..be6fcdee28 100644 --- a/identity_verification/Cargo.toml +++ b/identity_verification/Cargo.toml @@ -16,9 +16,5 @@ serde.workspace = true strum.workspace = true thiserror.workspace = true -[features] -# Enables jws verification based on the EdDSA algorithm. -eddsa = ["identity_jose/eddsa"] - [dev-dependencies] serde_json.workspace = true