Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a verify_prehashed method to identity.rs's JWS verifiers #1420

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions bindings/wasm/src/verification/jws_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
1 change: 1 addition & 0 deletions identity_ecdsa_verifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
83 changes: 83 additions & 0 deletions identity_ecdsa_verifier/src/common.rs
Original file line number Diff line number Diff line change
@@ -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<C>(jwk: &Jwk) -> Result<VerifyingKey<C>, SignatureVerificationError>
where
C: PrimeCurve + CurveArithmetic,
C::FieldBytesSize: ModulusSize,
C::AffinePoint: FromEncodedPoint<C> + ToEncodedPoint<C>,
{
// 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(&params.x)
.map_err(|err| {
SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err)
})?
.into_iter();
let y_bytes = jwu::decode_b64(&params.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::<C>::from_untagged_bytes(&public_key_bytes);
let verifying_key = VerifyingKey::<C>::from_encoded_point(&encoded_point)
.map_err(|e| SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(e))?;

Ok(verifying_key)
}

pub(crate) fn verify_signature<C, F>(
input: &VerificationInput,
public_key: &Jwk,
verifying_fn: F,
) -> Result<(), SignatureVerificationError>
where
C: PrimeCurve + CurveArithmetic,
C::FieldBytesSize: ModulusSize,
C::AffinePoint: FromEncodedPoint<C> + ToEncodedPoint<C>,
SignatureSize<C>: ArrayLength<u8>,
F: FnOnce(&VerifyingKey<C>, &[u8], &Signature<C>) -> Result<(), signature::Error>,
{
let verifying_key = jwk_to_verifying_key(public_key)?;
let mut signature = Signature::<C>::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))
}
19 changes: 19 additions & 0 deletions identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
}
}
}
2 changes: 2 additions & 0 deletions identity_ecdsa_verifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down
76 changes: 15 additions & 61 deletions identity_ecdsa_verifier/src/secp256k1.rs
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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(&params.x)
.map_err(|err| {
SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err)
})?
.into_iter()
.chain(jwu::decode_b64(&params.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> = 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)
}
}
70 changes: 14 additions & 56 deletions identity_ecdsa_verifier/src/secp256r1.rs
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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(&params.x)
.map_err(|err| {
SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err)
})?
.into_iter()
.chain(jwu::decode_b64(&params.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> = 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)
}
}
Loading
Loading