diff --git a/identity_ecdsa_verifier/Cargo.toml b/identity_ecdsa_verifier/Cargo.toml new file mode 100644 index 0000000000..f2d0fc025a --- /dev/null +++ b/identity_ecdsa_verifier/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "identity_ecdsa_verifier" +# TODO: description for consistency? +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +identity_verification.workspace = true +k256 = { workspace = true, features = ["std", "ecdsa", "ecdsa-core"], optional = true } +p256 = { workspace = true, features = ["std", "ecdsa", "ecdsa-core"], optional = true } +signature.workspace = true + +[dev-dependencies] +josekit.workspace = true +serde_json.workspace = true + +[features] +default = ["es256", "es256k"] +# Enables the EcDSAJwsVerifier to verify JWS with alg = ES256. +es256 = ["dep:p256"] +# Enables the EcDSAJwsVerifier to verify JWS with alg = ES256K. +es256k = ["dep:k256"] diff --git a/identity_ecdsa_verifier/README.md b/identity_ecdsa_verifier/README.md new file mode 100644 index 0000000000..4ccb0f36b9 --- /dev/null +++ b/identity_ecdsa_verifier/README.md @@ -0,0 +1,3 @@ +# ECDSA Verifier + +This crate implements a `JwsVerifier` capable of verifying EcDSA signatures with algorithms `ES256` and `ES256K`. diff --git a/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs b/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs new file mode 100644 index 0000000000..a14014d92d --- /dev/null +++ b/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs @@ -0,0 +1,31 @@ +use identity_verification::jws::JwsAlgorithm; +use identity_verification::jws::JwsVerifier; +use identity_verification::jws::SignatureVerificationErrorKind; + +/// An implementor of [`JwsVerifier`](identity_verification::jws::JwsVerifier) +/// that can handle a selection of EcDSA algorithms. +/// +/// The following algorithms are supported, if the respective feature on the +/// crate is activated: +/// +/// - [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256). +/// - [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K). +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct EcDSAJwsVerifier {} + +impl JwsVerifier for EcDSAJwsVerifier { + fn verify( + &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(&input, public_key), + #[cfg(feature = "es256k")] + JwsAlgorithm::ES256K => crate::Secp256K1Verifier::verify(&input, public_key), + _ => Err(SignatureVerificationErrorKind::UnsupportedAlg.into()), + } + } +} diff --git a/identity_ecdsa_verifier/src/lib.rs b/identity_ecdsa_verifier/src/lib.rs new file mode 100644 index 0000000000..c5192a1859 --- /dev/null +++ b/identity_ecdsa_verifier/src/lib.rs @@ -0,0 +1,26 @@ +#![doc = include_str!("./../README.md")] +#![warn( + rust_2018_idioms, + unreachable_pub, + missing_docs, + rustdoc::missing_crate_level_docs, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::private_doc_tests, + clippy::missing_safety_doc +)] + +mod ecdsa_jws_verifier; +#[cfg(feature = "es256k")] +mod secp256k1; +#[cfg(feature = "es256")] +mod secp256r1; + +pub use ecdsa_jws_verifier::*; +#[cfg(feature = "es256k")] +pub use secp256k1::*; +#[cfg(feature = "es256")] +pub use secp256r1::*; + +#[cfg(test)] +mod tests; diff --git a/identity_ecdsa_verifier/src/secp256k1.rs b/identity_ecdsa_verifier/src/secp256k1.rs new file mode 100644 index 0000000000..f7f18a4b22 --- /dev/null +++ b/identity_ecdsa_verifier/src/secp256k1.rs @@ -0,0 +1,93 @@ +use std::ops::Deref; + +use identity_verification::jwk::JwkParamsEc; +use identity_verification::jws::SignatureVerificationError; +use identity_verification::jws::SignatureVerificationErrorKind; +use identity_verification::jwu::{self}; +use k256::ecdsa::Signature; +use k256::ecdsa::VerifyingKey; +use k256::elliptic_curve::sec1::FromEncodedPoint; +use k256::elliptic_curve::subtle::CtOption; +use k256::EncodedPoint; +use k256::PublicKey; + +/// A verifier that can handle the +/// [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K) +/// algorithm. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Secp256K1Verifier {} + +impl Secp256K1Verifier { + /// Verify a JWS signature secured with the + /// [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K) + /// algorithm. + /// + /// This function is useful when one is building a + /// [`JwsVerifier`](identity_verification::jws::JwsVerifier) that + /// handles the + /// [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K) + /// in the same manner as the [`Secp256K1Verifier`] hence extending its + /// capabilities. + /// + /// # Warning + /// + /// This function does not check whether `alg = ES256K` 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() + // opt_public_key.expect("we should have asserted that the + // contained value is some") + } + }; + + 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) + })?; + + match signature::Verifier::verify(&verifying_key, &input.signing_input, &signature) { + Ok(()) => Ok(()), + Err(err) => Err(SignatureVerificationError::new( + SignatureVerificationErrorKind::InvalidSignature, + ) + .with_source(err)), + } + } +} diff --git a/identity_ecdsa_verifier/src/secp256r1.rs b/identity_ecdsa_verifier/src/secp256r1.rs new file mode 100644 index 0000000000..f10ee27d30 --- /dev/null +++ b/identity_ecdsa_verifier/src/secp256r1.rs @@ -0,0 +1,93 @@ +use std::ops::Deref; + +use identity_verification::jwk::JwkParamsEc; +use identity_verification::jws::SignatureVerificationError; +use identity_verification::jws::SignatureVerificationErrorKind; +use identity_verification::jwu::{self}; +use p256::ecdsa::Signature; +use p256::ecdsa::VerifyingKey; +use p256::elliptic_curve::sec1::FromEncodedPoint; +use p256::elliptic_curve::subtle::CtOption; +use p256::EncodedPoint; +use p256::PublicKey; + +/// A verifier that can handle the +/// [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256) +/// algorithm. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Secp256R1Verifier {} + +impl Secp256R1Verifier { + /// Verify a JWS signature secured with the + /// [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256) + /// algorithm. + /// + /// This function is useful when one is building a + /// [`JwsVerifier`](identity_verification::jws::JwsVerifier) that + /// handles the + /// [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256) + /// in the same manner as the [`Secp256R1Verifier`] hence extending its + /// capabilities. + /// + /// # Warning + /// + /// 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() + // opt_public_key.expect("we should have asserted that the + // contained value is some") + } + }; + + 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) + })?; + + match signature::Verifier::verify(&verifying_key, &input.signing_input, &signature) { + Ok(()) => Ok(()), + Err(err) => Err(SignatureVerificationError::new( + SignatureVerificationErrorKind::InvalidSignature, + ) + .with_source(err)), + } + } +} diff --git a/identity_ecdsa_verifier/src/tests.rs b/identity_ecdsa_verifier/src/tests.rs new file mode 100644 index 0000000000..ba4b326179 --- /dev/null +++ b/identity_ecdsa_verifier/src/tests.rs @@ -0,0 +1,182 @@ +mod es256 { + use identity_verification::jwk::EcCurve; + use identity_verification::jwk::Jwk; + use identity_verification::jwk::JwkParamsEc; + use identity_verification::jwu; + use p256::ecdsa::Signature; + use p256::ecdsa::SigningKey; + use p256::SecretKey; + + pub(crate) fn expand_p256_jwk(jwk: &Jwk) -> SecretKey { + let params: &JwkParamsEc = jwk.try_ec_params().unwrap(); + + if params.try_ec_curve().unwrap() != EcCurve::P256 { + panic!("expected a P256 curve"); + } + + let sk_bytes = params.d.as_ref().map(jwu::decode_b64).unwrap().unwrap(); + SecretKey::from_slice(&sk_bytes).unwrap() + } + + pub(crate) fn sign(message: &[u8], private_key: &Jwk) -> impl AsRef<[u8]> { + let sk: SecretKey = expand_p256_jwk(private_key); + let signing_key: SigningKey = SigningKey::from(sk); + let signature: Signature = signature::Signer::sign(&signing_key, message); + signature.to_bytes() + } +} + +mod es256k1 { + use identity_verification::jwk::EcCurve; + use identity_verification::jwk::Jwk; + use identity_verification::jwk::JwkParamsEc; + use identity_verification::jwu; + use k256::ecdsa::Signature; + use k256::ecdsa::SigningKey; + use k256::SecretKey; + + pub(crate) fn expand_k256_jwk(jwk: &Jwk) -> SecretKey { + let params: &JwkParamsEc = jwk.try_ec_params().unwrap(); + + if params.try_ec_curve().unwrap() != EcCurve::Secp256K1 { + panic!("expected a Secp256K1 curve"); + } + + let sk_bytes = params.d.as_ref().map(jwu::decode_b64).unwrap().unwrap(); + SecretKey::from_slice(&sk_bytes).unwrap() + } + + pub(crate) fn sign(message: &[u8], private_key: &Jwk) -> impl AsRef<[u8]> { + let sk: SecretKey = expand_k256_jwk(private_key); + let signing_key: SigningKey = SigningKey::from(sk); + let signature: Signature = signature::Signer::sign(&signing_key, message); + signature.to_bytes() + } +} + +use identity_verification::jwk::Jwk; +use identity_verification::jws; +use identity_verification::jws::JwsHeader; + +use crate::EcDSAJwsVerifier; + +#[test] +fn test_es256_rfc7515() { + // Test Vector taken from https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.3. + let tv_header: &str = r#"{"alg":"ES256"}"#; + let tv_claims: &[u8] = &[ + 123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 112, + 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, + 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, + 111, 116, 34, 58, 116, 114, 117, 101, 125, + ]; + let tv_encoded: &[u8] = b"eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.e4ZrhZdbFQ7630Tq51E6RQiJaae9bFNGJszIhtusEwzvO21rzH76Wer6yRn2Zb34VjIm3cVRl0iQctbf4uBY3w"; + let tv_private_key: &str = r#" + { + "kty": "EC", + "crv": "P-256", + "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", + "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", + "d": "jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI" + } + "#; + + let header: JwsHeader = serde_json::from_str(tv_header).unwrap(); + let jwk: Jwk = serde_json::from_str(tv_private_key).unwrap(); + let encoder: jws::CompactJwsEncoder<'_> = + jws::CompactJwsEncoder::new(tv_claims, &header).unwrap(); + let signing_input: &[u8] = encoder.signing_input(); + let encoded: String = { + let signature = es256::sign(signing_input, &jwk); + encoder.into_jws(signature.as_ref()) + }; + assert_eq!(encoded.as_bytes(), tv_encoded); + + let jws_verifier = EcDSAJwsVerifier::default(); + let decoder = jws::Decoder::new(); + let token = decoder + .decode_compact_serialization(tv_encoded, None) + .and_then(|decoded| decoded.verify(&jws_verifier, &jwk)) + .unwrap(); + + assert_eq!(token.protected, header); + assert_eq!(token.claims, tv_claims); +} + +#[test] +fn test_es256k_verifier() { + let tv_header: &str = r#"{ + "typ": "JWT", + "alg":"ES256K" + }"#; + let tv_private_key: &str = r#" + { + "kty":"EC", + "crv":"secp256k1", + "d":"y0zUV7bLeUG_kDOvACFHnSmtH7j8MSJek25R2wJbWWg", + "x":"BBobbZkiC8E4C4EYekPNJkcXFCsMNHhh0AV2USy_xSs", + "y":"VQcPHjIQClX0b5TLluFl6jpIf9U-norWC0oEvIQRNyU" + }"#; + let tv_claims: &[u8] = br#"{"key":"value"}"#; + + let header: JwsHeader = serde_json::from_str(tv_header).unwrap(); + let jwk: Jwk = serde_json::from_str(tv_private_key).unwrap(); + let encoder: jws::CompactJwsEncoder<'_> = + jws::CompactJwsEncoder::new(tv_claims, &header).unwrap(); + let signing_input: &[u8] = encoder.signing_input(); + let encoded: String = { + let signature = es256k1::sign(signing_input, &jwk); + encoder.into_jws(signature.as_ref()) + }; + + let jws_verifier = EcDSAJwsVerifier::default(); + let jwk: Jwk = serde_json::from_str(tv_private_key).unwrap(); + let decoder = jws::Decoder::new(); + assert!(decoder + .decode_compact_serialization(encoded.as_bytes(), None) + .and_then(|decoded| decoded.verify(&jws_verifier, &jwk)) + .is_ok()); +} + +/// In the absence of official test vectors for secp256k1, +/// this ensures we can verify JWTs created by other libraries. +mod test_es256k_josekit { + use identity_verification::jws; + use josekit::jwk::alg::ec::EcKeyPair; + use josekit::jwk::Jwk; + use josekit::jws::JwsHeader; + use josekit::jwt::JwtPayload; + + use crate::EcDSAJwsVerifier; + + #[test] + fn test_es256k_josekit() { + let alg = josekit::jws::ES256K; + + let private_key: &str = r#" + { + "kty":"EC", + "crv":"secp256k1", + "d":"y0zUV7bLeUG_kDOvACFHnSmtH7j8MSJek25R2wJbWWg", + "x":"BBobbZkiC8E4C4EYekPNJkcXFCsMNHhh0AV2USy_xSs", + "y":"VQcPHjIQClX0b5TLluFl6jpIf9U-norWC0oEvIQRNyU" + }"#; + let josekit_jwk: Jwk = serde_json::from_str(private_key).unwrap(); + let mut src_header = JwsHeader::new(); + src_header.set_token_type("JWT"); + let mut src_payload = JwtPayload::new(); + src_payload.set_claim("key", Some("value".into())).unwrap(); + let eckp = EcKeyPair::from_jwk(&josekit_jwk).unwrap(); + let signer = alg.signer_from_jwk(&eckp.to_jwk_key_pair()).unwrap(); + let jwt_string = + josekit::jwt::encode_with_signer(&src_payload, &src_header, &signer).unwrap(); + + let jws_verifier = EcDSAJwsVerifier::default(); + let decoder = jws::Decoder::new(); + let jwk: identity_verification::jwk::Jwk = serde_json::from_str(private_key).unwrap(); + assert!(decoder + .decode_compact_serialization(jwt_string.as_bytes(), None) + .and_then(|decoded| decoded.verify(&jws_verifier, &jwk)) + .is_ok()); + } +}