From 518d5ea82a73f18264c0c7a3d89e8f23f7974851 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Fri, 5 Apr 2024 16:44:51 +0300 Subject: [PATCH] feat(picky): add support for P521 NIST curve EC keys (#262) --- Cargo.lock | 15 ++++ .../Devolutions.Picky/Generated/EcCurve.cs | 4 ++ .../Devolutions.Picky/Generated/RawEcCurve.cs | 4 ++ ffi/src/key.rs | 4 ++ ffi/wasm/Cargo.lock | 15 ++++ ffi/wasm/src/key.rs | 4 ++ picky/Cargo.toml | 1 + picky/src/jose/jwe.rs | 44 ++++++++++++ picky/src/jose/jwk.rs | 27 +++++-- picky/src/jose/jws.rs | 39 ++++++----- picky/src/key/ec.rs | 56 ++++++++------- picky/src/key/mod.rs | 35 ++++++++++ picky/src/lib.rs | 10 +++ picky/src/signature.rs | 70 ++++++++++++++++--- picky/src/ssh/decode.rs | 2 +- picky/src/ssh/mod.rs | 7 +- picky/src/ssh/private_key.rs | 11 +-- test_assets/jose/jwk_ec_p521.json | 1 + test_assets/jose/jwt_sig_es512.txt | 1 + test_assets/private_keys/ec-nist521-pk_1.key | 8 +++ test_assets/public_keys/ec-nist521-pk_1.key | 6 ++ 21 files changed, 293 insertions(+), 71 deletions(-) create mode 100644 test_assets/jose/jwk_ec_p521.json create mode 100644 test_assets/jose/jwt_sig_es512.txt create mode 100644 test_assets/private_keys/ec-nist521-pk_1.key create mode 100644 test_assets/public_keys/ec-nist521-pk_1.key diff --git a/Cargo.lock b/Cargo.lock index a1e0f60a..44daef7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1192,6 +1192,20 @@ dependencies = [ "sha2", ] +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -1256,6 +1270,7 @@ dependencies = [ "num-bigint-dig", "p256", "p384", + "p521", "pbkdf2", "picky-asn1", "picky-asn1-der", diff --git a/ffi/dotnet/Devolutions.Picky/Generated/EcCurve.cs b/ffi/dotnet/Devolutions.Picky/Generated/EcCurve.cs index dd7dfc0a..f1082583 100644 --- a/ffi/dotnet/Devolutions.Picky/Generated/EcCurve.cs +++ b/ffi/dotnet/Devolutions.Picky/Generated/EcCurve.cs @@ -24,4 +24,8 @@ public enum EcCurve /// NIST P-384 /// NistP384 = 1, + /// + /// NIST P-521 + /// + NistP521 = 2, } diff --git a/ffi/dotnet/Devolutions.Picky/Generated/RawEcCurve.cs b/ffi/dotnet/Devolutions.Picky/Generated/RawEcCurve.cs index a192d055..5b80944c 100644 --- a/ffi/dotnet/Devolutions.Picky/Generated/RawEcCurve.cs +++ b/ffi/dotnet/Devolutions.Picky/Generated/RawEcCurve.cs @@ -24,4 +24,8 @@ public enum EcCurve /// NIST P-384 /// NistP384 = 1, + /// + /// NIST P-521 + /// + NistP521 = 2, } diff --git a/ffi/src/key.rs b/ffi/src/key.rs index 92a2121d..dced84df 100644 --- a/ffi/src/key.rs +++ b/ffi/src/key.rs @@ -7,6 +7,7 @@ impl From for EcCurve { match value { picky::key::EcCurve::NistP256 => Self::NistP256, picky::key::EcCurve::NistP384 => Self::NistP384, + picky::key::EcCurve::NistP521 => Self::NistP521, } } } @@ -16,6 +17,7 @@ impl From for picky::key::EcCurve { match value { EcCurve::NistP256 => Self::NistP256, EcCurve::NistP384 => Self::NistP384, + EcCurve::NistP521 => Self::NistP521, } } } @@ -70,6 +72,8 @@ pub mod ffi { NistP256, /// NIST P-384 NistP384, + /// NIST P-521 + NistP521, } /// Known Edwards curve-based algorithm name diff --git a/ffi/wasm/Cargo.lock b/ffi/wasm/Cargo.lock index 83fb62e8..fc5da7be 100644 --- a/ffi/wasm/Cargo.lock +++ b/ffi/wasm/Cargo.lock @@ -572,6 +572,20 @@ dependencies = [ "sha2", ] +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -621,6 +635,7 @@ dependencies = [ "num-bigint-dig", "p256", "p384", + "p521", "picky-asn1", "picky-asn1-der", "picky-asn1-x509", diff --git a/ffi/wasm/src/key.rs b/ffi/wasm/src/key.rs index 0c00b797..4a4ae6d4 100644 --- a/ffi/wasm/src/key.rs +++ b/ffi/wasm/src/key.rs @@ -8,6 +8,7 @@ impl From for EcCurve { match value { picky::key::EcCurve::NistP256 => Self::NistP256, picky::key::EcCurve::NistP384 => Self::NistP384, + picky::key::EcCurve::NistP521 => Self::NistP521, } } } @@ -17,6 +18,7 @@ impl From for picky::key::EcCurve { match value { EcCurve::NistP256 => Self::NistP256, EcCurve::NistP384 => Self::NistP384, + EcCurve::NistP521 => Self::NistP521, } } } @@ -29,6 +31,8 @@ pub enum EcCurve { NistP256, /// NIST P-384 NistP384, + /// NIST P-521 + NistP521, } impl From for EdAlgorithm { diff --git a/picky/Cargo.toml b/picky/Cargo.toml index 9eee9302..d2d5d9ee 100644 --- a/picky/Cargo.toml +++ b/picky/Cargo.toml @@ -53,6 +53,7 @@ x25519-dalek = { version = "2", features = ["static_secrets"] } p256 = { version = "0.13", features = ["ecdh"] } p384 = { version = "0.13", features = ["ecdh"] } +p521 = { version = "0.13", features = ["ecdh"] } rsa = { version = "0.9.2", features = ["std"] } diff --git a/picky/src/jose/jwe.rs b/picky/src/jose/jwe.rs index 762ba51c..d8d5378a 100644 --- a/picky/src/jose/jwe.rs +++ b/picky/src/jose/jwe.rs @@ -1064,6 +1064,24 @@ fn generate_ecdh_shared_secret( (shared_secret, epk) } + NamedEcCurve::Known(EcCurve::NistP521) => { + let public_key = p521::PublicKey::from_sec1_bytes(ec.encoded_point()).map_err(|e| { + let source = KeyError::EC { + context: format!("Cannot parse p521 encoded point from bytes: {e}"), + }; + JweError::Key { source } + })?; + + let secret = p521::ecdh::EphemeralSecret::random(&mut OsRng); + + let shared_secret = Zeroizing::new(secret.diffie_hellman(&public_key).raw_secret_bytes().to_vec()); + let epk = PublicKey::from_ec_encoded_components( + &NamedEcCurve::Known(EcCurve::NistP521).into(), + secret.public_key().to_sec1_bytes().as_ref(), + ); + + (shared_secret, epk) + } NamedEcCurve::Unsupported(oid) => { let source = KeyError::unsupported_curve(oid, "ECDH-ES JWE algorithm"); return Err(JweError::Key { source }); @@ -1199,6 +1217,32 @@ fn calculate_ecdh_shared_secret( Zeroizing::new(shared_secret) } + NamedEcCurve::Known(EcCurve::NistP521) => { + let public_key = p521::PublicKey::from_sec1_bytes(public_key.encoded_point()).map_err(|e| { + let source = KeyError::EC { + context: format!("Cannot parse p521 encoded point from bytes: {e}"), + }; + JweError::Key { source } + })?; + + let secret_bytes_validated = + EcCurve::NistP521.validate_component(EcComponent::Secret(private_key.secret()))?; + + let secret = p521::SecretKey::from_bytes( + p521::elliptic_curve::generic_array::GenericArray::from_slice(secret_bytes_validated), + ) + .map_err(|e| KeyError::EC { + context: format!("Cannot parse p521 secret from bytes: {e}"), + })?; + + // p521 crate doesn't have high level API for static ECDH secrets + let shared_secret = + p521::elliptic_curve::ecdh::diffie_hellman(secret.to_nonzero_scalar(), public_key.as_affine()) + .raw_secret_bytes() + .to_vec(); + + Zeroizing::new(shared_secret) + } NamedEcCurve::Unsupported(oid) => { let source = KeyError::unsupported_curve(oid, "ECDH-ES JWE algorithm"); return Err(JweError::Key { source }); diff --git a/picky/src/jose/jwk.rs b/picky/src/jose/jwk.rs index ae9ee9e3..6ea2c59b 100644 --- a/picky/src/jose/jwk.rs +++ b/picky/src/jose/jwk.rs @@ -370,6 +370,24 @@ impl Jwk { }), } } + NamedEcCurve::Known(EcCurve::NistP521) => { + let point = p521::EncodedPoint::from_bytes(ec_key.encoded_point()).map_err(|_| { + JwkError::InvalidEcPublicKey { + cause: "invalid P-521 EC point encoding".to_string(), + } + })?; + + match (point.x(), point.y()) { + (Some(x), Some(y)) => Ok(Self::new(JwkKeyType::new_ec_key( + JwkEcPublicKeyCurve::P521, + x.as_slice(), + y.as_slice(), + ))), + _ => Err(JwkError::InvalidEcPublicKey { + cause: "Invalid P-521 curve EC public point coordinates".to_string(), + }), + } + } NamedEcCurve::Unsupported(_) => Err(JwkError::UnsupportedAlgorithm { algorithm: "Unsupported EC curve", }), @@ -414,13 +432,7 @@ impl Jwk { let curve = match ec.crv { JwkEcPublicKeyCurve::P256 => EcCurve::NistP256, JwkEcPublicKeyCurve::P384 => EcCurve::NistP384, - JwkEcPublicKeyCurve::P521 => { - // To construct encoded point from coponents we need to use curve-specific - // arithmetic, which is currently not supported by picky. - return Err(JwkError::UnsupportedAlgorithm { - algorithm: "P-521 EC curve", - }); - } + JwkEcPublicKeyCurve::P521 => EcCurve::NistP521, }; let x = BigUint::from_bytes_be(&ec.x_signed_bytes_be()?); @@ -694,6 +706,7 @@ mod tests { #[rstest] #[case(test_files::JOSE_JWK_EC_P256_JSON)] #[case(test_files::JOSE_JWK_EC_P384_JSON)] + #[case(test_files::JOSE_JWK_EC_P521_JSON)] fn ecdsa_key_roundtrip(#[case] json: &str) { let decoded = Jwk::from_json(json).unwrap(); let encoded = decoded.to_json().unwrap(); diff --git a/picky/src/jose/jws.rs b/picky/src/jose/jws.rs index e1396853..bc1bde00 100644 --- a/picky/src/jose/jws.rs +++ b/picky/src/jose/jws.rs @@ -320,12 +320,7 @@ impl Jws { let curve = match self.header.alg { JwsAlg::ES256 => EcCurve::NistP256, JwsAlg::ES384 => EcCurve::NistP384, - JwsAlg::ES512 => { - return Err(SignatureError::Ec { - context: "ECDSA with SHA-512 is not supported".to_string(), - } - .into()) - } + JwsAlg::ES512 => EcCurve::NistP521, _ => unreachable!("Checked in match above"), }; @@ -455,12 +450,7 @@ pub fn verify_signature(encoded_token: &str, public_key: &PublicKey, algorithm: let curve = match algorithm { JwsAlg::ES256 => EcCurve::NistP256, JwsAlg::ES384 => EcCurve::NistP384, - JwsAlg::ES512 => { - return Err(SignatureError::Ec { - context: "ECDSA with SHA-512 is not supported".to_string(), - } - .into()) - } + JwsAlg::ES512 => EcCurve::NistP521, _ => unreachable!("Checked in match above"), }; @@ -530,9 +520,10 @@ mod tests { } #[rstest] - #[case(JwsAlg::ES256, test_files::EC_NIST256_PK_1, test_files::JOSE_JWT_SIG_ES256)] - #[case(JwsAlg::ES384, test_files::EC_NIST384_PK_1, test_files::JOSE_JWT_SIG_ES384)] - fn ecdsa_algorithm(#[case] alg: JwsAlg, #[case] key_pem: &str, #[case] expected: &str) { + #[case(JwsAlg::ES256, test_files::EC_NIST256_PK_1)] + #[case(JwsAlg::ES384, test_files::EC_NIST384_PK_1)] + #[case(JwsAlg::ES512, test_files::EC_NIST521_PK_1)] + fn ecdsa_sign_verify(#[case] alg: JwsAlg, #[case] key_pem: &str) { let key = PrivateKey::from_pem_str(key_pem).unwrap(); let jwt = Jws { @@ -545,11 +536,25 @@ mod tests { // Check encode + sign let encoded = jwt.encode(&key).unwrap(); - assert_eq!(encoded, expected); // Check decode + verify let jws = RawJws::decode(&encoded).unwrap(); - jws.verify(&key.to_public_key().unwrap()).unwrap(); + jws.clone().verify(&key.to_public_key().unwrap()).unwrap(); + + assert_eq!(&jws.header, &jwt.header); + assert_eq!(&jws.payload, &jwt.payload); + } + + #[rstest] + #[case(test_files::EC_NIST256_PK_1, test_files::JOSE_JWT_SIG_ES256)] + #[case(test_files::EC_NIST384_PK_1, test_files::JOSE_JWT_SIG_ES384)] + #[case(test_files::EC_NIST521_PK_1, test_files::JOSE_JWT_SIG_ES512)] + fn ecdsa_parse_and_verify(#[case] key_pem: &str, #[case] signature: &str) { + let key = PrivateKey::from_pem_str(key_pem).unwrap(); + + // Check decode + verify + let jws = RawJws::decode(signature).unwrap(); + jws.clone().verify(&key.to_public_key().unwrap()).unwrap(); } const JWT_ED25519_BODY: &str = r#"{"username":"kataras"}"#; diff --git a/picky/src/key/ec.rs b/picky/src/key/ec.rs index d1e18e59..26f17208 100644 --- a/picky/src/key/ec.rs +++ b/picky/src/key/ec.rs @@ -42,6 +42,8 @@ pub enum EcCurve { NistP256, /// NIST P-384 curve (secp384r1) NistP384, + /// NIST P-521 curve (secp521r1) + NistP521, } impl EcCurve { @@ -59,6 +61,11 @@ impl EcCurve { use p384::elliptic_curve::FieldBytesSize; as Unsigned>::USIZE } + EcCurve::NistP521 => { + use p521::elliptic_curve::generic_array::typenum::Unsigned; + use p521::elliptic_curve::FieldBytesSize; + as Unsigned>::USIZE + } } } @@ -88,26 +95,12 @@ pub(crate) enum NamedEcCurve { Unsupported(ObjectIdentifier), } -impl NamedEcCurve { - #[cfg(feature = "ssh")] - pub fn new_nist_p521() -> Self { - Self::Unsupported(oids::secp521r1()) - } - - #[cfg(feature = "ssh")] - pub fn is_nist_p521(&self) -> bool { - match self { - Self::Unsupported(oid) => oid == &oids::secp521r1(), - _ => false, - } - } -} - impl Display for EcCurve { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::NistP256 => write!(f, "NIST-P256"), Self::NistP384 => write!(f, "NIST-P384"), + Self::NistP521 => write!(f, "NIST-P521"), } } } @@ -130,6 +123,7 @@ impl From<&'_ ObjectIdentifier> for NamedEcCurve { match oid.as_str() { oids::SECP256R1 => NamedEcCurve::Known(EcCurve::NistP256), oids::SECP384R1 => NamedEcCurve::Known(EcCurve::NistP384), + oids::SECP521R1 => NamedEcCurve::Known(EcCurve::NistP521), _ => NamedEcCurve::Unsupported(value.clone()), } } @@ -141,6 +135,7 @@ impl From for ObjectIdentifier { NamedEcCurve::Known(curve) => match curve { EcCurve::NistP256 => oids::secp256r1(), EcCurve::NistP384 => oids::secp384r1(), + EcCurve::NistP521 => oids::secp521r1(), }, NamedEcCurve::Unsupported(oid) => oid, } @@ -213,6 +208,24 @@ pub(crate) fn calculate_public_ec_key( Ok(Some(public_key.to_bytes().to_vec())) } + NamedEcCurve::Known(EcCurve::NistP521) => { + use p521::{ + elliptic_curve::{generic_array::GenericArray as GenericArrayP521, sec1::ToEncodedPoint as _}, + SecretKey as SecretKeyP521, + }; + + let private_key_validated = EcCurve::NistP521.validate_component(EcComponent::Secret(private_key))?; + + let secret_bytes = GenericArrayP521::from_slice(private_key_validated); + let secret_key = SecretKeyP521::from_bytes(secret_bytes).map_err(|_| KeyError::EC { + context: "Failed to construct P521 SecretKey from private key bytes".to_string(), + })?; + + // Calculate public key from secret key + let public_key = secret_key.public_key().as_affine().to_encoded_point(compress); + + Ok(Some(public_key.to_bytes().to_vec())) + } NamedEcCurve::Unsupported(_) => Ok(None), } } @@ -312,25 +325,18 @@ mod tests { #[rstest] #[case(test_files::EC_NIST256_NOPUBLIC_DER_PK_1)] #[case(test_files::EC_NIST384_NOPUBLIC_DER_PK_1)] + #[case(test_files::EC_NIST521_NOPUBLIC_DER_PK_1)] fn ecdsa_private_key_without_public(#[case] key_pem: &str) { // This should succeed for supported curves let key = PrivateKey::from_pem_str(key_pem).unwrap(); key.to_public_key().unwrap().to_pem_str().unwrap(); } - #[test] - fn ecdsa_private_key_without_public_unsupported_curve() { - // We should be able to parse private keys without public keys and unknown curve, - // but we `to_public_key` will fail anyway because we don't implement arithmetic for that - // curve - let key = PrivateKey::from_pem_str(test_files::EC_NIST521_NOPUBLIC_DER_PK_1).unwrap(); - key.to_public_key().unwrap_err(); - } - #[rstest] // Known curves #[case(test_files::EC_NIST256_PK_1_PUB)] #[case(test_files::EC_NIST384_PK_1_PUB)] + #[case(test_files::EC_NIST521_PK_1_PUB)] // Unsupported curve, should still work as long as pem contains the public key // (in that case no arithmetic operations are performed on the key) #[case(test_files::EC_PUBLIC_KEY_SECP256K1_PEM)] @@ -354,7 +360,7 @@ mod tests { #[rstest] #[case(test_files::EC_NIST256_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP256))] #[case(test_files::EC_NIST384_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP384))] - #[case(test_files::EC_NIST521_DER_PK_1, NamedEcCurve::Unsupported(oids::secp521r1()))] + #[case(test_files::EC_NIST521_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP521))] fn ecdsa_key_pair_from_ec_private_key(#[case] key: &str, #[case] curve: NamedEcCurve) { let pk = PrivateKey::from_pem_str(key).unwrap(); let pair = EcdsaKeypair::try_from(&pk).unwrap(); diff --git a/picky/src/key/mod.rs b/picky/src/key/mod.rs index 483c6c60..6918e8d0 100644 --- a/picky/src/key/mod.rs +++ b/picky/src/key/mod.rs @@ -263,6 +263,9 @@ impl PrivateKey { use p384::elliptic_curve::generic_array::GenericArray as GenericArrayP384; use p384::EncodedPoint as EncodedPointP384; + use p521::elliptic_curve::generic_array::GenericArray as GenericArrayP521; + use p521::EncodedPoint as EncodedPointP521; + let curve_oid: ObjectIdentifier = NamedEcCurve::Known(curve).into(); let px_bytes = expand_ec_field(point_x.to_bytes_be(), curve); let py_bytes = expand_ec_field(point_y.to_bytes_be(), curve); @@ -283,6 +286,12 @@ impl PrivateKey { let point = EncodedPointP384::from_affine_coordinates(x, y, COMPRESS_EC_POINT_BY_DEFAULT); point.as_bytes().to_vec() } + EcCurve::NistP521 => { + let x = GenericArrayP521::from_slice(px_validated); + let y = GenericArrayP521::from_slice(py_validated); + let point = EncodedPointP521::from_affine_coordinates(x, y, COMPRESS_EC_POINT_BY_DEFAULT); + point.as_bytes().to_vec() + } }; let secret = secret.to_bytes_be(); @@ -649,6 +658,18 @@ impl PrivateKey { .to_vec(); (secret, point) } + EcCurve::NistP521 => { + use p521::elliptic_curve::sec1::ToEncodedPoint; + + let key = p521::SecretKey::random(&mut OsRng); + let secret = key.to_bytes().to_vec(); + let point = key + .public_key() + .to_encoded_point(COMPRESS_EC_POINT_BY_DEFAULT) + .as_bytes() + .to_vec(); + (secret, point) + } }; let inner = PrivateKeyInfo::new_ec_encryption( @@ -857,6 +878,20 @@ impl PublicKey { COMPRESS_EC_POINT_BY_DEFAULT, ); + Ok(Self::from_ec_encoded_components( + &NamedEcCurve::Known(curve).into(), + p.as_bytes(), + )) + } + EcCurve::NistP521 => { + use p521::elliptic_curve::generic_array::GenericArray as GenericArrayP521; + + let p = p521::EncodedPoint::from_affine_coordinates( + GenericArrayP521::from_slice(px_validated), + GenericArrayP521::from_slice(py_validated), + COMPRESS_EC_POINT_BY_DEFAULT, + ); + Ok(Self::from_ec_encoded_components( &NamedEcCurve::Known(curve).into(), p.as_bytes(), diff --git a/picky/src/lib.rs b/picky/src/lib.rs index 9b78186f..acde4e66 100644 --- a/picky/src/lib.rs +++ b/picky/src/lib.rs @@ -61,6 +61,10 @@ mod test_files { // openssl ec -in ec-secp384r1-priv-key.pem -pubout > ec-secp384r1-pub-key.pem pub const EC_NIST384_PK_1_PUB: &str = include_str!("../../test_assets/public_keys/ec-nist384-pk_1.key"); + // openssl ecparam -name secp521r1 -genkey -noout -out ec-secp521r1-priv-key.pem + // openssl ec -in ec-secp521r1-priv-key.pem -pubout > ec-secp521r1-pub-key.pem + pub const EC_NIST521_PK_1_PUB: &str = include_str!("../../test_assets/public_keys/ec-nist521-pk_1.key"); + // openssl ecparam -name secp256k1 -genkey -noout -out ec-secp256k1-priv-key.pem // openssl ec -in ec-secp256k1-priv-key.pem -pubout > ec-secp256k1-pub-key.pem pub const EC_PUBLIC_KEY_SECP256K1_PEM: &str = include_str!("../../test_assets/public_keys/ec-secp256k1-pk_1.key"); @@ -112,6 +116,8 @@ mod test_files { }} cfg_if::cfg_if! { if #[cfg(feature = "jose")] { + pub const EC_NIST521_PK_1: &str = include_str!("../../test_assets/private_keys/ec-nist521-pk_1.key"); + pub const JOSE_JWT_SIG_EXAMPLE: &str = include_str!("../../test_assets/jose/jwt_sig_example.txt"); pub const JOSE_JWT_SIG_WITH_EXP: &str = @@ -123,6 +129,8 @@ mod test_files { include_str!("../../test_assets/jose/jwk_ec_p256.json"); pub const JOSE_JWK_EC_P384_JSON: &str = include_str!("../../test_assets/jose/jwk_ec_p384.json"); + pub const JOSE_JWK_EC_P521_JSON: &str = + include_str!("../../test_assets/jose/jwk_ec_p521.json"); pub const JOSE_JWK_ED25519_JSON: &str = include_str!("../../test_assets/jose/jwk_ed25519.json"); pub const JOSE_JWK_X25519_JSON: &str = @@ -132,6 +140,8 @@ mod test_files { include_str!("../../test_assets/jose/jwt_sig_es256.txt"); pub const JOSE_JWT_SIG_ES384: &str = include_str!("../../test_assets/jose/jwt_sig_es384.txt"); + pub const JOSE_JWT_SIG_ES512: &str = + include_str!("../../test_assets/jose/jwt_sig_es512.txt"); /// Test data was gathered from https://github.com/golang-jwt/jwt pub const JOSE_JWT_SIG_ED25519_GO: &str = diff --git a/picky/src/signature.rs b/picky/src/signature.rs index f3bff583..3bb5bbe3 100644 --- a/picky/src/signature.rs +++ b/picky/src/signature.rs @@ -219,6 +219,30 @@ impl SignatureAlgorithm { ), }), }, + NamedEcCurve::Known(EcCurve::NistP521) => match picky_hash_algo { + HashAlgorithm::SHA2_512 => { + let secret_validated = + EcCurve::NistP521.validate_component(EcComponent::Secret(ec_keypair.secret()))?; + + let key_bytes = + p521::elliptic_curve::generic_array::GenericArray::from_slice(secret_validated); + + let key = + p521::ecdsa::SigningKey::from_bytes(key_bytes).map_err(|e| SignatureError::Ec { + context: format!("Cannot decode p521 EC keypair: {}", e), + })?; + let sig: p521::ecdsa::Signature = key.try_sign(msg).map_err(|e| SignatureError::Ec { + context: format!("Cannot produce p521 signature: {}", e), + })?; + Ok(sig.to_der().as_bytes().to_vec()) + } + _ => Err(SignatureError::UnsupportedAlgorithm { + algorithm: format!( + "ECDSA P-521 curve with {:?} hash algorithm is not supported", + picky_hash_algo + ), + }), + }, NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "signing").into()), } } @@ -370,6 +394,37 @@ impl SignatureAlgorithm { })?; vkey.verify(msg, &signature).map_err(|_| SignatureError::BadSignature)? } + HashAlgorithm::SHA2_512 => { + use p521::ecdsa::signature::Verifier; + + match curve { + EcCurve::NistP521 => {} + curve => { + return Err(SignatureError::UnsupportedAlgorithm { + algorithm: format!("SHA512 hash algorithm can't be used with `{}` curve", curve), + }) + } + }; + + let encoded_point = + p521::EncodedPoint::from_bytes(ec_pub_key.encoded_point()).map_err(|e| { + SignatureError::Ec { + context: format!("Cannot parse p521 public key from der bytes: {}", e), + } + })?; + + let vkey = p521::ecdsa::VerifyingKey::from_encoded_point(&encoded_point).map_err(|e| { + SignatureError::Ec { + context: format!("Cannot parse p521 encoded point: {}", e), + } + })?; + + let signature = + p521::ecdsa::Signature::from_der(signature).map_err(|e| SignatureError::Ec { + context: format!("Cannot parse p521 signature: {}", e), + })?; + vkey.verify(msg, &signature).map_err(|_| SignatureError::BadSignature)? + } _ => { return Err(SignatureError::UnsupportedAlgorithm { algorithm: format!("ECDSA with {:?} hash algorithm is not supported", picky_hash_algo), @@ -430,6 +485,7 @@ impl SignatureAlgorithm { #[cfg(test)] mod ec_tests { use super::*; + use crate::test_files; use rstest::*; const EC_PRIVATE_KEY_NIST256_PEM: &str = r#"-----BEGIN EC PRIVATE KEY----- @@ -443,14 +499,6 @@ MIGkAgEBBDDT8VOfdzHbIRaWOO1F0vgotY2qM2FfYS3zpdKE7Vqbh26hFsUw+iaG GmGnT+29kg+gBwYFK4EEACKhZANiAAQFvVVUKRdN3/bqaEpDA1aHu8FEd3ujuyS0 AadG6QAiZxH37BGumBcyTTeGHyArqb+GTpsHTUXASbP+P+p5JgkfF9wBMF1SVTvu ACZOYcqzGbsAXXdMYqewckhc42ye0u0= ------END EC PRIVATE KEY-----"#; - - const EC_PRIVATE_KEY_NIST512_PEM: &str = r#"-----BEGIN EC PRIVATE KEY----- -MIHcAgEBBEIBhqphIGu2PmlcEb6xADhhSCpgPUulB0s4L2qOgolRgaBx4fNgINFE -mBsSyHJncsWG8WFEuUzAYy/YKz2lP0Qx6Z2gBwYFK4EEACOhgYkDgYYABABwBevJ -w/+Xh6I98ruzoTX3MNTsbgnc+glenJRCbEJkjbJrObFhbfgqP52r1lAy2RxuShGi -NYJJzNPT6vR1abS32QFtvTH7YbYa6OWk9dtGNY/cYxgx1nQyhUuofdW7qbbfu/Ww -TP2oFsPXRAavZCh4AbWUn8bAHmzNRyuJonQBKlQlVQ== -----END EC PRIVATE KEY-----"#; #[rstest] @@ -495,9 +543,9 @@ csaQwO9jFvbQFIpCvcMRjaunLfhIWiYDdg== } #[rstest] - #[case(EC_PRIVATE_KEY_NIST256_PEM, HashAlgorithm::SHA2_256, true)] - #[case(EC_PRIVATE_KEY_NIST384_PEM, HashAlgorithm::SHA2_384, true)] - #[case(EC_PRIVATE_KEY_NIST512_PEM, HashAlgorithm::SHA2_512, false)] // EC Nist 512 is not supported by ring yet + #[case(test_files::EC_NIST256_PK_1, HashAlgorithm::SHA2_256, true)] + #[case(test_files::EC_NIST384_PK_1, HashAlgorithm::SHA2_384, true)] + #[case(test_files::EC_NIST521_PK_1, HashAlgorithm::SHA2_512, true)] fn sign_and_verify(#[case] key_pem: &str, #[case] hash: HashAlgorithm, #[case] sign_successful: bool) { let private_key = PrivateKey::from_pem_str(key_pem).unwrap(); diff --git a/picky/src/ssh/decode.rs b/picky/src/ssh/decode.rs index 774d7ef6..442091a5 100644 --- a/picky/src/ssh/decode.rs +++ b/picky/src/ssh/decode.rs @@ -433,7 +433,7 @@ impl SshComplexTypeDecode for SshCertificate { let curve = match cert_key_type { SshCertKeyType::EcdsaSha2Nistp256V01 => NamedEcCurve::Known(EcCurve::NistP256), SshCertKeyType::EcdsaSha2Nistp384V01 => NamedEcCurve::Known(EcCurve::NistP384), - SshCertKeyType::EcdsaSha2Nistp521V01 => NamedEcCurve::new_nist_p521(), + SshCertKeyType::EcdsaSha2Nistp521V01 => NamedEcCurve::Known(EcCurve::NistP521), _ => unreachable!("Already validated in match above"), }; diff --git a/picky/src/ssh/mod.rs b/picky/src/ssh/mod.rs index 898a6c1c..76fb9572 100644 --- a/picky/src/ssh/mod.rs +++ b/picky/src/ssh/mod.rs @@ -44,9 +44,7 @@ impl EcCurveSshExt for NamedEcCurve { match self { NamedEcCurve::Known(EcCurve::NistP256) => Ok(key_type::ECDSA_SHA2_NIST_P256), NamedEcCurve::Known(EcCurve::NistP384) => Ok(key_type::ECDSA_SHA2_NIST_P384), - // Special handling: we don't support any arithmetic on P521, but we at least - // should be able to read and write it back correctly. - NamedEcCurve::Unsupported(_) if self.is_nist_p521() => Ok(key_type::ECDSA_SHA2_NIST_P521), + NamedEcCurve::Known(EcCurve::NistP521) => Ok(key_type::ECDSA_SHA2_NIST_P521), NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "ssh key type serialization")), } } @@ -55,8 +53,7 @@ impl EcCurveSshExt for NamedEcCurve { match self { NamedEcCurve::Known(EcCurve::NistP256) => Ok(key_identifier::ECDSA_SHA2_NIST_P256), NamedEcCurve::Known(EcCurve::NistP384) => Ok(key_identifier::ECDSA_SHA2_NIST_P384), - // See comment inside function above - NamedEcCurve::Unsupported(_) if self.is_nist_p521() => Ok(key_identifier::ECDSA_SHA2_NIST_P521), + NamedEcCurve::Known(EcCurve::NistP521) => Ok(key_identifier::ECDSA_SHA2_NIST_P521), NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "ssh key identifier serialization")), } } diff --git a/picky/src/ssh/private_key.rs b/picky/src/ssh/private_key.rs index 98728fd3..94363280 100644 --- a/picky/src/ssh/private_key.rs +++ b/picky/src/ssh/private_key.rs @@ -607,8 +607,6 @@ pub mod tests { #[rstest] #[case(test_files::SSH_PRIVATE_KEY_EC_P256)] #[case(test_files::SSH_PRIVATE_KEY_EC_P384)] - // Note that even if P521 curve arithmetic is not supported yet, we still can read and write - // ssh keys with this curve. #[case(test_files::SSH_PRIVATE_KEY_EC_P521)] fn ecdsa_keys_unencrypted(#[case] pem: &str) { let key = SshPrivateKey::from_pem_str(pem, None).unwrap(); @@ -688,9 +686,12 @@ pub mod tests { } } - #[test] - fn test_ec_private_key_generation() { - let private_key = SshPrivateKey::generate_ec(EcCurve::NistP256, Option::Some("123".to_string()), None).unwrap(); + #[rstest] + #[case(EcCurve::NistP256)] + #[case(EcCurve::NistP384)] + #[case(EcCurve::NistP521)] + fn test_ec_private_key_generation(#[case] curve: EcCurve) { + let private_key = SshPrivateKey::generate_ec(curve, Option::Some("123".to_string()), None).unwrap(); let data = private_key.to_pem().unwrap(); let parsed = SshPrivateKey::from_pem(&data, Option::Some("123".to_string())).unwrap(); diff --git a/test_assets/jose/jwk_ec_p521.json b/test_assets/jose/jwk_ec_p521.json new file mode 100644 index 00000000..c0234531 --- /dev/null +++ b/test_assets/jose/jwk_ec_p521.json @@ -0,0 +1 @@ +{"kty":"EC","crv":"P-521","x":"ALP4k6QQiVKMbtfw9joWZ4XA4pQ2VIDDjDSO2fEgpCxleHey8vJGc-pll5qBnikRoXD9JPvhWGm9R_QN24rIqBqg","y":"AZTQtIcK0D2c8Og1pVoU0Z-tFMbnzBMvcgKGMeQuATL2mxQXmh8cmKfRHs8FATZtk8oDkFHmn7RtezFFFAAFtgm1"} \ No newline at end of file diff --git a/test_assets/jose/jwt_sig_es512.txt b/test_assets/jose/jwt_sig_es512.txt new file mode 100644 index 00000000..d6740d5e --- /dev/null +++ b/test_assets/jose/jwt_sig_es512.txt @@ -0,0 +1 @@ +eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.AVkpZPakU6IPunZtJqliftQiogDd1M1v4JuLBa-PvDxRsL63prnYqNeul--0PIpeVGGJ5IzTTuueXZlCYveLuxEmAI6sQJJVEYJz43BgNjotnpL19ostMM68rX0xDnap3sTFSsV-7BbGwhOWSr1V2xeZNOtsC4i0tbKnsgCdlmdr-zx6 \ No newline at end of file diff --git a/test_assets/private_keys/ec-nist521-pk_1.key b/test_assets/private_keys/ec-nist521-pk_1.key new file mode 100644 index 00000000..c2b6cb48 --- /dev/null +++ b/test_assets/private_keys/ec-nist521-pk_1.key @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAC+Ii5OdcST+DHxTj +lQA0SdRne/HDdxzJpcU4yySrpWQNJeCn1ZU/m6FNasH6c7apIPAvcsptPUUSH1wF +CLJjPn2hgYkDgYYABACbi9IkdNkzWyh6FhHWE5u+78xk+4J2kpN6wU/LT24T8vhf +Jmux7ts9rUMIocqt/CtgEf+Cor+mb7KVyjY78CLJ5QE4JsqpI8oBoYJBnHv5axYp +9QwFyqvjh003kRcXaMpqaE6Hne4ZYte1snr3cLcGrR7Rmt7jK/pX3PhsRsVzBXdW +3g== +-----END PRIVATE KEY----- diff --git a/test_assets/public_keys/ec-nist521-pk_1.key b/test_assets/public_keys/ec-nist521-pk_1.key new file mode 100644 index 00000000..8eb499f3 --- /dev/null +++ b/test_assets/public_keys/ec-nist521-pk_1.key @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAm4vSJHTZM1soehYR1hObvu/MZPuC +dpKTesFPy09uE/L4XyZrse7bPa1DCKHKrfwrYBH/gqK/pm+ylco2O/AiyeUBOCbK +qSPKAaGCQZx7+WsWKfUMBcqr44dNN5EXF2jKamhOh53uGWLXtbJ693C3Bq0e0Zre +4yv6V9z4bEbFcwV3Vt4= +-----END PUBLIC KEY-----