diff --git a/bindings/wasm/src/verification/jws_verifier.rs b/bindings/wasm/src/verification/jws_verifier.rs index bd016910c8..7df5d2b6a2 100644 --- a/bindings/wasm/src/verification/jws_verifier.rs +++ b/bindings/wasm/src/verification/jws_verifier.rs @@ -120,4 +120,28 @@ impl WasmEcDSAJwsVerifier { }; EcDSAJwsVerifier::default().verify(input, &publicKey.0).wasm_result() } + + /// Pre-hashed version of {@link EcDSAJwsVerifier.verify}. + /// # Warning + /// Users of this function **MUST** make sure `signingInput` is the result + /// of a cryptographically-secure hashing algorithm. + #[wasm_bindgen(js_name = verifyPrehashed)] + #[allow(non_snake_case)] + pub fn verify_prehashed( + &self, + alg: WasmJwsAlgorithm, + signingInput: &[u8], + decodedSignature: &[u8], + publicKey: &WasmJwk, + ) -> Result<(), JsValue> { + let alg = JwsAlgorithm::try_from(alg)?; + let input = VerificationInput { + alg, + signing_input: signingInput.into(), + decoded_signature: decodedSignature.into(), + }; + EcDSAJwsVerifier::default() + .verify_prehashed(input, &publicKey.0) + .wasm_result() + } } diff --git a/identity_ecdsa_verifier/Cargo.toml b/identity_ecdsa_verifier/Cargo.toml index 6829d41ae0..3493af449c 100644 --- a/identity_ecdsa_verifier/Cargo.toml +++ b/identity_ecdsa_verifier/Cargo.toml @@ -15,6 +15,7 @@ description = "JWS ECDSA signature verification for IOTA Identity" workspace = true [dependencies] +ecdsa = { version = "0.16.9", default-features = false, features = ["verifying", "alloc"] } identity_verification = { version = "=1.4.0", path = "../identity_verification", default-features = false } k256 = { version = "0.13.3", default-features = false, features = ["std", "ecdsa", "ecdsa-core"], optional = true } p256 = { version = "0.13.2", default-features = false, features = ["std", "ecdsa", "ecdsa-core"], optional = true } diff --git a/identity_ecdsa_verifier/src/common.rs b/identity_ecdsa_verifier/src/common.rs new file mode 100644 index 0000000000..41224cd196 --- /dev/null +++ b/identity_ecdsa_verifier/src/common.rs @@ -0,0 +1,83 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref as _; + +use ecdsa::elliptic_curve::sec1::FromEncodedPoint; +use ecdsa::elliptic_curve::sec1::ModulusSize; +use ecdsa::elliptic_curve::sec1::ToEncodedPoint; +use ecdsa::elliptic_curve::CurveArithmetic; +use ecdsa::EncodedPoint; +use ecdsa::PrimeCurve; +use ecdsa::Signature; +use ecdsa::SignatureSize; +use ecdsa::VerifyingKey; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkParamsEc; +use identity_verification::jws::SignatureVerificationError; +use identity_verification::jws::SignatureVerificationErrorKind; +use identity_verification::jws::VerificationInput; +use identity_verification::jwu; +use signature::digest::generic_array::ArrayLength; + +fn jwk_to_verifying_key(jwk: &Jwk) -> Result, SignatureVerificationError> +where + C: PrimeCurve + CurveArithmetic, + C::FieldBytesSize: ModulusSize, + C::AffinePoint: FromEncodedPoint + ToEncodedPoint, +{ + // Obtain an Elliptic Curve public key. + let params: &JwkParamsEc = jwk + .try_ec_params() + .map_err(|_| SignatureVerificationError::new(SignatureVerificationErrorKind::UnsupportedKeyType))?; + + // Concatenate x and y coordinates as required by + // EncodedPoint::from_untagged_bytes. + let public_key_bytes = { + let x_bytes = jwu::decode_b64(¶ms.x) + .map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) + })? + .into_iter(); + let y_bytes = jwu::decode_b64(¶ms.y) + .map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) + })? + .into_iter(); + + x_bytes.chain(y_bytes).collect() + }; + + // The JWK contains the uncompressed x and y coordinates, so we can create the + // encoded point directly without prefixing an SEC1 tag. + let encoded_point = EncodedPoint::::from_untagged_bytes(&public_key_bytes); + let verifying_key = VerifyingKey::::from_encoded_point(&encoded_point) + .map_err(|e| SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(e))?; + + Ok(verifying_key) +} + +pub(crate) fn verify_signature( + input: &VerificationInput, + public_key: &Jwk, + verifying_fn: F, +) -> Result<(), SignatureVerificationError> +where + C: PrimeCurve + CurveArithmetic, + C::FieldBytesSize: ModulusSize, + C::AffinePoint: FromEncodedPoint + ToEncodedPoint, + SignatureSize: ArrayLength, + F: FnOnce(&VerifyingKey, &[u8], &Signature) -> Result<(), signature::Error>, +{ + let verifying_key = jwk_to_verifying_key(public_key)?; + let mut signature = Signature::::from_slice(input.decoded_signature.deref()).map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err) + })?; + + if let Some(normalized) = signature.normalize_s() { + signature = normalized; + } + + verifying_fn(&verifying_key, &input.signing_input, &signature) + .map_err(|e| SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(e)) +} diff --git a/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs b/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs index 6371b40b78..9f708e6014 100644 --- a/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs +++ b/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs @@ -32,3 +32,22 @@ impl JwsVerifier for EcDSAJwsVerifier { } } } + +impl EcDSAJwsVerifier { + /// Verifies the provided JWS signature `input` using `public_key`. + /// # Warning + /// `input.signing_input` must be the result of a cryptographically secure hashing algorithm. + pub fn verify_prehashed( + &self, + input: identity_verification::jws::VerificationInput, + public_key: &identity_verification::jwk::Jwk, + ) -> Result<(), identity_verification::jws::SignatureVerificationError> { + match input.alg { + #[cfg(feature = "es256")] + JwsAlgorithm::ES256 => crate::Secp256R1Verifier::verify_prehashed(&input, public_key), + #[cfg(feature = "es256k")] + JwsAlgorithm::ES256K => crate::Secp256K1Verifier::verify_prehashed(&input, public_key), + _ => Err(SignatureVerificationErrorKind::UnsupportedAlg.into()), + } + } +} diff --git a/identity_ecdsa_verifier/src/lib.rs b/identity_ecdsa_verifier/src/lib.rs index 6136a3eae1..3d9ada7736 100644 --- a/identity_ecdsa_verifier/src/lib.rs +++ b/identity_ecdsa_verifier/src/lib.rs @@ -19,6 +19,8 @@ mod secp256k1; #[cfg(feature = "es256")] mod secp256r1; +pub(crate) mod common; + pub use ecdsa_jws_verifier::*; #[cfg(feature = "es256k")] pub use secp256k1::*; diff --git a/identity_ecdsa_verifier/src/secp256k1.rs b/identity_ecdsa_verifier/src/secp256k1.rs index 9c77412cc8..5d0855a041 100644 --- a/identity_ecdsa_verifier/src/secp256k1.rs +++ b/identity_ecdsa_verifier/src/secp256k1.rs @@ -1,18 +1,14 @@ // Copyright 2020-2024 IOTA Stiftung, Filancore GmbH // SPDX-License-Identifier: Apache-2.0 -use std::ops::Deref; - -use identity_verification::jwk::JwkParamsEc; +use identity_verification::jwk::Jwk; use identity_verification::jws::SignatureVerificationError; -use identity_verification::jws::SignatureVerificationErrorKind; -use identity_verification::jwu::{self}; -use k256::ecdsa::Signature; +use identity_verification::jws::VerificationInput; use k256::ecdsa::VerifyingKey; -use k256::elliptic_curve::sec1::FromEncodedPoint; -use k256::elliptic_curve::subtle::CtOption; -use k256::EncodedPoint; -use k256::PublicKey; +use signature::hazmat::PrehashVerifier; +use signature::Verifier; + +use crate::common; /// A verifier that can handle the /// [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K) @@ -35,59 +31,17 @@ impl Secp256K1Verifier { /// /// # Warning /// - /// This function does not check whether `alg = ES256K` in the protected + /// This function does not check whether `alg = ES256K` is in the protected /// header. Callers are expected to assert this prior to calling the /// function. - pub fn verify( - input: &identity_verification::jws::VerificationInput, - public_key: &identity_verification::jwk::Jwk, - ) -> Result<(), SignatureVerificationError> { - // Obtain a K256 public key. - let params: &JwkParamsEc = public_key - .try_ec_params() - .map_err(|_| SignatureVerificationErrorKind::UnsupportedKeyType)?; - - // Concatenate x and y coordinates as required by - // EncodedPoint::from_untagged_bytes. - let public_key_bytes = jwu::decode_b64(¶ms.x) - .map_err(|err| { - SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) - })? - .into_iter() - .chain(jwu::decode_b64(¶ms.y).map_err(|err| { - SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) - })?) - .collect(); - - // The JWK contains the uncompressed x and y coordinates, so we can create the - // encoded point directly without prefixing an SEC1 tag. - let encoded_point: EncodedPoint = EncodedPoint::from_untagged_bytes(&public_key_bytes); - let public_key: PublicKey = { - let opt_public_key: CtOption = PublicKey::from_encoded_point(&encoded_point); - if opt_public_key.is_none().into() { - return Err(SignatureVerificationError::new( - SignatureVerificationErrorKind::KeyDecodingFailure, - )); - } else { - opt_public_key.unwrap() - } - }; - - let verifying_key: VerifyingKey = VerifyingKey::from(public_key); - - let mut signature: Signature = Signature::try_from(input.decoded_signature.deref()).map_err(|err| { - SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err) - })?; - - if let Some(normalized) = signature.normalize_s() { - signature = normalized; - } + pub fn verify(input: &VerificationInput, public_key: &Jwk) -> Result<(), SignatureVerificationError> { + common::verify_signature(input, public_key, VerifyingKey::verify) + } - match signature::Verifier::verify(&verifying_key, &input.signing_input, &signature) { - Ok(()) => Ok(()), - Err(err) => { - Err(SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err)) - } - } + /// Pre-hashed variant of [`Secp256K1Verifier::verify`]. + /// # Warning + /// `input.signing_input` **MUST** be the result of a cryptographically secure hashing algorithm. + pub fn verify_prehashed(input: &VerificationInput, public_key: &Jwk) -> Result<(), SignatureVerificationError> { + common::verify_signature(input, public_key, VerifyingKey::verify_prehash) } } diff --git a/identity_ecdsa_verifier/src/secp256r1.rs b/identity_ecdsa_verifier/src/secp256r1.rs index 09201570d0..fa45877538 100644 --- a/identity_ecdsa_verifier/src/secp256r1.rs +++ b/identity_ecdsa_verifier/src/secp256r1.rs @@ -1,18 +1,14 @@ // Copyright 2020-2024 IOTA Stiftung, Filancore GmbH // SPDX-License-Identifier: Apache-2.0 -use std::ops::Deref; - -use identity_verification::jwk::JwkParamsEc; +use identity_verification::jwk::Jwk; use identity_verification::jws::SignatureVerificationError; -use identity_verification::jws::SignatureVerificationErrorKind; -use identity_verification::jwu::{self}; -use p256::ecdsa::Signature; +use identity_verification::jws::VerificationInput; use p256::ecdsa::VerifyingKey; -use p256::elliptic_curve::sec1::FromEncodedPoint; -use p256::elliptic_curve::subtle::CtOption; -use p256::EncodedPoint; -use p256::PublicKey; +use signature::hazmat::PrehashVerifier; +use signature::Verifier; + +use crate::common; /// A verifier that can handle the /// [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256) @@ -38,52 +34,14 @@ impl Secp256R1Verifier { /// This function does not check whether `alg = ES256` in the protected /// header. Callers are expected to assert this prior to calling the /// function. - pub fn verify( - input: &identity_verification::jws::VerificationInput, - public_key: &identity_verification::jwk::Jwk, - ) -> Result<(), SignatureVerificationError> { - // Obtain a P256 public key. - let params: &JwkParamsEc = public_key - .try_ec_params() - .map_err(|_| SignatureVerificationErrorKind::UnsupportedKeyType)?; - - // Concatenate x and y coordinates as required by - // EncodedPoint::from_untagged_bytes. - let public_key_bytes = jwu::decode_b64(¶ms.x) - .map_err(|err| { - SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) - })? - .into_iter() - .chain(jwu::decode_b64(¶ms.y).map_err(|err| { - SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) - })?) - .collect(); - - // The JWK contains the uncompressed x and y coordinates, so we can create the - // encoded point directly without prefixing an SEC1 tag. - let encoded_point: EncodedPoint = EncodedPoint::from_untagged_bytes(&public_key_bytes); - let public_key: PublicKey = { - let opt_public_key: CtOption = PublicKey::from_encoded_point(&encoded_point); - if opt_public_key.is_none().into() { - return Err(SignatureVerificationError::new( - SignatureVerificationErrorKind::KeyDecodingFailure, - )); - } else { - opt_public_key.unwrap() - } - }; - - let verifying_key: VerifyingKey = VerifyingKey::from(public_key); - - let signature: Signature = Signature::try_from(input.decoded_signature.deref()).map_err(|err| { - SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err) - })?; + pub fn verify(input: &VerificationInput, public_key: &Jwk) -> Result<(), SignatureVerificationError> { + common::verify_signature(input, public_key, VerifyingKey::verify) + } - match signature::Verifier::verify(&verifying_key, &input.signing_input, &signature) { - Ok(()) => Ok(()), - Err(err) => { - Err(SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err)) - } - } + /// Pre-hashed variant of [`Secp256R1Verifier::verify`]. + /// # Warning + /// `input.signing_input` **MUST** be the result of a cryptographically secure hashing algorithm. + pub fn verify_prehashed(input: &VerificationInput, public_key: &Jwk) -> Result<(), SignatureVerificationError> { + common::verify_signature(input, public_key, VerifyingKey::verify_prehash) } } diff --git a/identity_eddsa_verifier/src/ed25519_verifier.rs b/identity_eddsa_verifier/src/ed25519_verifier.rs index 16d939ddb9..1201272c25 100644 --- a/identity_eddsa_verifier/src/ed25519_verifier.rs +++ b/identity_eddsa_verifier/src/ed25519_verifier.rs @@ -3,6 +3,8 @@ use std::ops::Deref; +use crypto::signatures::ed25519::PublicKey; +use crypto::signatures::ed25519::Signature; use identity_jose::jwk::EdCurve; use identity_jose::jwk::Jwk; use identity_jose::jwk::JwkParamsOkp; @@ -31,45 +33,48 @@ impl Ed25519Verifier { /// 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)?; + let public_key = jwk_to_public_key(public_key)?; - if params - .try_ed_curve() - .ok() - .filter(|curve_param| *curve_param == EdCurve::Ed25519) - .is_none() - { - return Err(SignatureVerificationErrorKind::UnsupportedKeyParams.into()); - } + let signature = input + .decoded_signature + .deref() + .try_into() + .map(Signature::from_bytes) + .map_err(|_| SignatureVerificationErrorKind::InvalidSignature)?; - 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") - }) - })?; + public_key + .verify(&signature, &input.signing_input) + .then_some(()) + .ok_or_else(|| SignatureVerificationErrorKind::InvalidSignature.into()) + } +} - let public_key_ed25519 = crypto::signatures::ed25519::PublicKey::try_from(pk).map_err(|err| { - SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) - })?; +fn jwk_to_public_key(jwk: &Jwk) -> Result { + let params: &JwkParamsOkp = jwk + .try_okp_params() + .map_err(|_| SignatureVerificationErrorKind::UnsupportedKeyType)?; - let signature_arr = - <[u8; crypto::signatures::ed25519::Signature::LENGTH]>::try_from(input.decoded_signature.deref()) - .map_err(|_| SignatureVerificationErrorKind::InvalidSignature)?; + if params + .try_ed_curve() + .ok() + .filter(|curve_param| *curve_param == EdCurve::Ed25519) + .is_none() + { + return Err(SignatureVerificationErrorKind::UnsupportedKeyParams.into()); + } - let signature = crypto::signatures::ed25519::Signature::from_bytes(signature_arr); + let pk: [u8; 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") + }) + })?; - if crypto::signatures::ed25519::PublicKey::verify(&public_key_ed25519, &signature, &input.signing_input) { - Ok(()) - } else { - Err(SignatureVerificationErrorKind::InvalidSignature.into()) - } - } + PublicKey::try_from(pk) + .map_err(|err| SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err)) }