diff --git a/src/core/crypto.rs b/src/core/crypto.rs index e6d17e78..346532b1 100644 --- a/src/core/crypto.rs +++ b/src/core/crypto.rs @@ -5,7 +5,7 @@ use ring::signature as ring_signature; use crate::types::Base64UrlEncodedBytes; use crate::{JsonWebKey, SignatureVerificationError, SigningError}; -use super::{CoreJsonWebKey, CoreJsonWebKeyType}; +use super::{jwk::CoreJsonCurveType, CoreJsonWebKey, CoreJsonWebKeyType}; use std::ops::Deref; @@ -46,14 +46,45 @@ fn rsa_public_key( ) -> Result<(&Base64UrlEncodedBytes, &Base64UrlEncodedBytes), String> { if *key.key_type() != CoreJsonWebKeyType::RSA { Err("RSA key required".to_string()) - } else if let Some(n) = key.n.as_ref() { - if let Some(e) = key.e.as_ref() { - Ok((n, e)) - } else { - Err("RSA exponent `e` is missing".to_string()) - } } else { - Err("RSA modulus `n` is missing".to_string()) + let n = key + .n + .as_ref() + .ok_or_else(|| "RSA modulus `n` is missing".to_string())?; + let e = key + .e + .as_ref() + .ok_or_else(|| "RSA exponent `e` is missing".to_string())?; + Ok((n, e)) + } +} + +fn ec_public_key( + key: &CoreJsonWebKey, +) -> Result< + ( + &Base64UrlEncodedBytes, + &Base64UrlEncodedBytes, + &CoreJsonCurveType, + ), + String, +> { + if *key.key_type() != CoreJsonWebKeyType::EllipticCurve { + Err("EC key required".to_string()) + } else { + let x = key + .x + .as_ref() + .ok_or_else(|| "EC `x` part is missing".to_string())?; + let y = key + .y + .as_ref() + .ok_or_else(|| "EC `y` part is missing".to_string())?; + let crv = key + .crv + .as_ref() + .ok_or_else(|| "EC `crv` part is missing".to_string())?; + Ok((x, y, crv)) } } @@ -73,3 +104,30 @@ pub fn verify_rsa_signature( .verify(params, msg, signature) .map_err(|_| SignatureVerificationError::CryptoError("bad signature".to_string())) } +/// According to RFC5480, Section-2.2 implementations of Elliptic Curve Cryptography MUST support the uncompressed form. +/// The first octet of the octet string indicates whether the uncompressed or compressed form is used. For the uncompressed +/// form, the first octet has to be 0x04. +/// According to https://briansmith.org/rustdoc/ring/signature/index.html#ecdsa__fixed-details-fixed-length-pkcs11-style-ecdsa-signatures, +/// to recover the X and Y coordinates from an octet string, the Octet-String-To-Elliptic-Curve-Point Conversion +/// is used (Section 2.3.4 of https://www.secg.org/sec1-v2.pdf). + +pub fn verify_ec_signature( + key: &CoreJsonWebKey, + params: &'static ring_signature::EcdsaVerificationAlgorithm, + msg: &[u8], + signature: &[u8], +) -> Result<(), SignatureVerificationError> { + let (x, y, crv) = ec_public_key(&key).map_err(SignatureVerificationError::InvalidKey)?; + if *crv == CoreJsonCurveType::P521 { + return Err(SignatureVerificationError::UnsupportedAlg( + "P521".to_string(), + )); + } + let mut pk = vec![0x04]; + pk.extend(x.deref()); + pk.extend(y.deref()); + let public_key = ring_signature::UnparsedPublicKey::new(params, pk); + public_key + .verify(msg, signature) + .map_err(|_| SignatureVerificationError::CryptoError("EC Signature was wrong".to_string())) +} diff --git a/src/core/jwk.rs b/src/core/jwk.rs index ed82b54f..9b488bde 100644 --- a/src/core/jwk.rs +++ b/src/core/jwk.rs @@ -4,8 +4,8 @@ use ring::rand; use ring::signature as ring_signature; use ring::signature::KeyPair; -use crate::types::helpers::deserialize_option_or_none; use crate::types::Base64UrlEncodedBytes; +use crate::types::{helpers::deserialize_option_or_none, JsonCurveType}; use crate::{ JsonWebKey, JsonWebKeyId, JsonWebKeyType, JsonWebKeyUse, JwsSigningAlgorithm, PrivateSigningKey, SignatureVerificationError, SigningError, @@ -48,6 +48,33 @@ pub struct CoreJsonWebKey { )] pub(crate) e: Option, + //Elliptic Curve + #[serde( + default, + deserialize_with = "deserialize_option_or_none", + skip_serializing_if = "Option::is_none" + )] + pub(crate) crv: Option, + #[serde( + default, + deserialize_with = "deserialize_option_or_none", + skip_serializing_if = "Option::is_none" + )] + pub(crate) x: Option, + #[serde( + default, + deserialize_with = "deserialize_option_or_none", + skip_serializing_if = "Option::is_none" + )] + pub(crate) y: Option, + + #[serde( + default, + deserialize_with = "deserialize_option_or_none", + skip_serializing_if = "Option::is_none" + )] + pub(crate) d: Option, + // Used for symmetric keys, which we only generate internally from the client secret; these // are never part of the JWK set. #[serde( @@ -71,6 +98,34 @@ impl CoreJsonWebKey { n: Some(Base64UrlEncodedBytes::new(n)), e: Some(Base64UrlEncodedBytes::new(e)), k: None, + crv: None, + x: None, + y: None, + d: None, + } + } + /// Instantiate a new EC public key from the raw x (`x`) and y(`y`) part of the curve, + /// along with an optional (but recommended) key ID. + /// + /// The key ID is used for matching signed JSON Web Tokens with the keys used for verifying + /// their signatures. + pub fn new_ec( + x: Vec, + y: Vec, + crv: CoreJsonCurveType, + kid: Option, + ) -> Self { + Self { + kty: CoreJsonWebKeyType::EllipticCurve, + use_: Some(CoreJsonWebKeyUse::Signature), + kid, + n: None, + e: None, + k: None, + crv: Some(crv), + x: Some(Base64UrlEncodedBytes::new(x)), + y: Some(Base64UrlEncodedBytes::new(y)), + d: None, } } } @@ -93,6 +148,10 @@ impl JsonWebKey n: None, e: None, k: Some(Base64UrlEncodedBytes::new(key)), + crv: None, + x: None, + y: None, + d: None, } } @@ -162,6 +221,34 @@ impl JsonWebKey CoreJwsSigningAlgorithm::HmacSha512 => { crypto::verify_hmac(self, hmac::HMAC_SHA512, message, signature) } + CoreJwsSigningAlgorithm::EcdsaP256Sha256 => { + if matches!(self.crv, Some(CoreJsonCurveType::P256)) { + crypto::verify_ec_signature( + self, + &ring_signature::ECDSA_P256_SHA256_FIXED, + message, + signature, + ) + } else { + Err(SignatureVerificationError::InvalidKey( + "Key uses different CRV than JWT".to_string(), + )) + } + } + CoreJwsSigningAlgorithm::EcdsaP384Sha384 => { + if matches!(self.crv, Some(CoreJsonCurveType::P384)) { + crypto::verify_ec_signature( + self, + &ring_signature::ECDSA_P384_SHA384_FIXED, + message, + signature, + ) + } else { + Err(SignatureVerificationError::InvalidKey( + "Key uses different CRV than JWT".to_string(), + )) + } + } ref other => Err(SignatureVerificationError::UnsupportedAlg( variant_name(other).to_string(), )), @@ -339,6 +426,10 @@ impl .into(), )), k: None, + crv: None, + x: None, + y: None, + d: None, } } } @@ -369,6 +460,30 @@ pub enum CoreJsonWebKeyType { } impl JsonWebKeyType for CoreJsonWebKeyType {} +/// +/// Type of EC-Curve +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +pub enum CoreJsonCurveType { + /// + /// P-256 Curve + /// + #[serde(rename = "P-256")] + P256, + /// + /// P-384 Curve + /// + #[serde(rename = "P-384")] + P384, + /// + /// P-521 Curve (currently not supported) + /// + #[serde(rename = "P-521")] + P521, +} +impl JsonCurveType for CoreJsonWebKeyType {} + /// /// Usage restriction for a JSON Web key. /// @@ -407,16 +522,16 @@ impl JsonWebKeyUse for CoreJsonWebKeyUse { mod tests { use ring::test::rand::FixedByteRandom; - use crate::jwt::tests::TEST_RSA_PUB_KEY; + use crate::jwt::tests::{TEST_EC_PUB_KEY_P256, TEST_EC_PUB_KEY_P384, TEST_RSA_PUB_KEY}; use crate::types::Base64UrlEncodedBytes; use crate::types::{JsonWebKey, JsonWebKeyId}; use crate::verification::SignatureVerificationError; - use super::SigningError; use super::{ CoreHmacKey, CoreJsonWebKey, CoreJsonWebKeyType, CoreJsonWebKeyUse, CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey, PrivateSigningKey, }; + use super::{CoreJsonCurveType, SigningError}; #[test] fn test_core_jwk_deserialization_rsa() { @@ -459,6 +574,39 @@ mod tests { assert_eq!(key.e, Some(Base64UrlEncodedBytes::new(vec![1, 0, 1]))); assert_eq!(key.k, None); } + #[test] + fn test_core_jwk_deserialization_ec() { + let json = "{ + \"kty\": \"EC\", + \"use\": \"sig\", + \"kid\": \"2011-04-29\", + \"crv\": \"P-256\", + \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", + \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" + }"; + + let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); + assert_eq!(key.kty, CoreJsonWebKeyType::EllipticCurve); + assert_eq!(key.use_, Some(CoreJsonWebKeyUse::Signature)); + assert_eq!(key.kid, Some(JsonWebKeyId::new("2011-04-29".to_string()))); + assert_eq!(key.crv, Some(CoreJsonCurveType::P256)); + assert_eq!( + key.y, + Some(Base64UrlEncodedBytes::new(vec![ + 0x4a, 0xd0, 0xef, 0x28, 0x88, 0x17, 0xa8, 0x0c, 0x49, 0xe8, 0x3b, 0x9e, 0x6d, 0x11, + 0x21, 0xfb, 0x5b, 0xe0, 0xbd, 0x94, 0x56, 0xdd, 0xd7, 0xeb, 0x3b, 0x14, 0x88, 0x2b, + 0x30, 0x6d, 0x44, 0x8d + ])) + ); + assert_eq!( + key.x, + Some(Base64UrlEncodedBytes::new(vec![ + 0x91, 0x70, 0x86, 0x64, 0x8a, 0xf7, 0xa0, 0x8e, 0xac, 0x29, 0xb9, 0xd3, 0xea, 0xb4, + 0x6c, 0x21, 0xdc, 0x45, 0x5f, 0x0d, 0xff, 0x55, 0xb2, 0xe4, 0xfd, 0xcb, 0xde, 0x6a, + 0x38, 0x2a, 0x5e, 0x4f + ])) + ); + } #[test] fn test_core_jwk_deserialization_symmetric() { @@ -546,6 +694,95 @@ mod tests { .expect_err("signature verification should fail"); } + #[test] + fn test_ecdsa_verification() { + let key_p256: CoreJsonWebKey = + serde_json::from_str(TEST_EC_PUB_KEY_P256).expect("deserialization failed"); + let key_p384: CoreJsonWebKey = + serde_json::from_str(TEST_EC_PUB_KEY_P384).expect("deserialization failed"); + let pkcs1_signing_input = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\ + hhbXBsZSJ9.\ + SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ + lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ + b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ + UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; + let signature_p256 = "EnKCtAHhzhqxV2GTr1VEurse2kQ7oHpFoVqM66sYGlmahDRGSlfrVAsGCzdLv66OS2Qf1zt6OPHX-5ZAkMgzlA"; + let signature_p384 = "B_9oDAabMasZ2Yt_cnAS21owaN0uWSInQBPxTqqiM3N3XjkksBRMGqguJLV5WoSMcvqgXwHTTQtbHGuh0Uf4g6LEr7XtO1T2KCttQR27d5YbvVZdORrzCm0Nsm1zkV-i"; + + //test p256 + verify_signature( + &key_p256, + &CoreJwsSigningAlgorithm::EcdsaP256Sha256, + pkcs1_signing_input, + signature_p256, + ); + + //wrong algo should fail before ring validation + if let Some(err) = key_p256 + .verify_signature( + &CoreJwsSigningAlgorithm::EcdsaP384Sha384, + pkcs1_signing_input.as_bytes(), + signature_p256.as_bytes(), + ) + .err() + { + let error_msg = "Key uses different CRV than JWT".to_string(); + match err { + SignatureVerificationError::InvalidKey(msg) => { + if msg != error_msg { + panic!("The error should be about different CRVs") + } + } + _ => panic!("We should fail before actual validation"), + } + } + // suppose we have alg specified correctly, but the signature given is actually a p384 + key_p256 + .verify_signature( + &CoreJwsSigningAlgorithm::EcdsaP256Sha256, + pkcs1_signing_input.as_bytes(), + signature_p384.as_bytes(), + ) + .expect_err("verification should fail"); + + //test p384 + verify_signature( + &key_p384, + &CoreJwsSigningAlgorithm::EcdsaP384Sha384, + pkcs1_signing_input, + signature_p384, + ); + + // suppose we have alg specified correctly, but the signature given is actually a p256 + key_p384 + .verify_signature( + &CoreJwsSigningAlgorithm::EcdsaP384Sha384, + pkcs1_signing_input.as_bytes(), + signature_p256.as_bytes(), + ) + .expect_err("verification should fail"); + + //wrong algo should fail before ring validation + if let Some(err) = key_p384 + .verify_signature( + &CoreJwsSigningAlgorithm::EcdsaP256Sha256, + pkcs1_signing_input.as_bytes(), + signature_p384.as_bytes(), + ) + .err() + { + let error_msg = "Key uses different CRV than JWT".to_string(); + match err { + SignatureVerificationError::InvalidKey(msg) => { + if msg != error_msg { + panic!("The error should be about different CRVs") + } + } + _ => panic!("We should fail before actual validation"), + } + } + } + #[test] fn test_rsa_pkcs1_verification() { let key: CoreJsonWebKey = diff --git a/src/jwt.rs b/src/jwt.rs index 40df630e..88c89df8 100644 --- a/src/jwt.rs +++ b/src/jwt.rs @@ -500,6 +500,23 @@ pub mod tests { \"e\": \"AQAB\" }"; + pub const TEST_EC_PUB_KEY_P256: &str = r#"{ + "kty": "EC", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "crv": "P-256", + "x": "t6PHivOTggpaX9lkMkis2p8kMhy-CktJAFTz6atReZw", + "y": "ODobXupKlD0DeM1yRd7bX4XFNBO1HOgCT1UCu0KY3lc" + }"#; + pub const TEST_EC_PUB_KEY_P384: &str = r#"{ + "kty": "EC", + "kid": "bilbo.baggins@hobbiton.example", + "use": "sig", + "crv" : "P-384", + "x": "9ywsUbxX59kJXFRiWHcx97wRKNiF8Hc9F5wI08n8h2ek_qAl0veEc36k1Qz6KLiL", + "y": "6PWlqjRbaV7V8ohDscM243IneuLZmxDGLiGNA1w69fQhEDsvZtKLUQ5KiHLgR3op" + }"#; + // This is the PEM form of the test private key from: // https://tools.ietf.org/html/rfc7520#section-3.4 pub const TEST_RSA_PRIV_KEY: &str = "-----BEGIN RSA PRIVATE KEY-----\n\ diff --git a/src/types.rs b/src/types.rs index b148e8c5..456d4ab7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -281,6 +281,14 @@ pub trait JsonWebKeyType: { } +/// +/// Curve type (e.g., P256). +/// +pub trait JsonCurveType: + Clone + Debug + DeserializeOwned + PartialEq + Serialize + 'static +{ +} + /// /// Allowed key usage. /// diff --git a/src/verification.rs b/src/verification.rs index aa991b50..dbac87c4 100644 --- a/src/verification.rs +++ b/src/verification.rs @@ -1384,6 +1384,10 @@ mod tests { n: None, e: None, k: Some(Base64UrlEncodedBytes::new(vec![1, 2, 3, 4])), + crv: None, + x: None, + y: None, + d: None, }]), ) .verified_claims(valid_rs256_jwt.clone()) @@ -1405,6 +1409,10 @@ mod tests { n: Some(n), e: Some(e), k: None, + crv: None, + x: None, + y: None, + d: None, }]), ) .verified_claims(valid_rs256_jwt.clone())