Skip to content

Commit

Permalink
Add EcDSA verifier (#1353)
Browse files Browse the repository at this point in the history
* add ecdsa verifier

* add identity_ecdsa_verifier to workspace, add license headers

* Update identity_ecdsa_verifier/Cargo.toml

Co-authored-by: wulfraem <wulfraem@users.noreply.github.com>

* Update identity_ecdsa_verifier/src/secp256k1.rs

Co-authored-by: wulfraem <wulfraem@users.noreply.github.com>

* Update identity_ecdsa_verifier/Cargo.toml

Co-authored-by: wulfraem <wulfraem@users.noreply.github.com>

* Update identity_ecdsa_verifier/src/secp256k1.rs

Co-authored-by: wulfraem <wulfraem@users.noreply.github.com>

* Update identity_ecdsa_verifier/src/secp256r1.rs

Co-authored-by: wulfraem <wulfraem@users.noreply.github.com>

* add feedback

* add OpenSSL installation to windows runner in CI

* update license headers and authors for ecdsa verifier

* update license template to allow multiple contributors

---------

Co-authored-by: Sebastian Wolfram <wulfraem@users.noreply.github.com>
  • Loading branch information
Aconitin and wulfraem committed May 14, 2024
1 parent 149bfac commit 9abdb38
Show file tree
Hide file tree
Showing 12 changed files with 482 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: Ensure, OpenSSL is available in Windows
if: matrix.os == 'windows-latest'
run: |
echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
vcpkg install openssl:x64-windows-static-md
- name: Setup Rust and cache
uses: './.github/actions/rust/rust-setup'
with:
Expand Down
2 changes: 1 addition & 1 deletion .license_template
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Copyright {20\d{2}(-20\d{2})?} IOTA Stiftung
// Copyright {20\d{2}(-20\d{2})?} IOTA Stiftung{(?:, .+)?}
// SPDX-License-Identifier: Apache-2.0
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ members = [
"identity_verification",
"identity_stronghold",
"identity_jose",
"identity_ecdsa_verifier",
"identity_eddsa_verifier",
"examples",
]
Expand Down
32 changes: 32 additions & 0 deletions identity_ecdsa_verifier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "identity_ecdsa_verifier"
version = "0.1.0"
authors = ["IOTA Stiftung", "Filancore GmbH"]
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 ECDSA signature verification for IOTA Identity"

[lints]
workspace = true

[dependencies]
identity_verification = { version = "=1.2.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 }
signature = { version = "2", default-features = false }

[dev-dependencies]
josekit = "0.8.6"
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"]
3 changes: 3 additions & 0 deletions identity_ecdsa_verifier/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ECDSA Verifier

This crate implements a `JwsVerifier` capable of verifying EcDSA signatures with algorithms `ES256` and `ES256K`.
34 changes: 34 additions & 0 deletions identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH
// SPDX-License-Identifier: Apache-2.0

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()),
}
}
}
29 changes: 29 additions & 0 deletions identity_ecdsa_verifier/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH
// SPDX-License-Identifier: Apache-2.0

#![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;
93 changes: 93 additions & 0 deletions identity_ecdsa_verifier/src/secp256k1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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::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(&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;
}

match signature::Verifier::verify(&verifying_key, &input.signing_input, &signature) {
Ok(()) => Ok(()),
Err(err) => {
Err(SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err))
}
}
}
}
89 changes: 89 additions & 0 deletions identity_ecdsa_verifier/src/secp256r1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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::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(&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)
})?;

match signature::Verifier::verify(&verifying_key, &input.signing_input, &signature) {
Ok(()) => Ok(()),
Err(err) => {
Err(SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err))
}
}
}
}
5 changes: 5 additions & 0 deletions identity_ecdsa_verifier/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH
// SPDX-License-Identifier: Apache-2.0

mod secp256;
mod secp256k;
77 changes: 77 additions & 0 deletions identity_ecdsa_verifier/src/tests/secp256.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH
// SPDX-License-Identifier: Apache-2.0

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()
}
}

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);
}

0 comments on commit 9abdb38

Please sign in to comment.