Skip to content

Commit

Permalink
feat(picky): add support for P521 NIST curve EC keys (#262)
Browse files Browse the repository at this point in the history
  • Loading branch information
pacmancoder authored Apr 5, 2024
1 parent d054232 commit 518d5ea
Show file tree
Hide file tree
Showing 21 changed files with 293 additions and 71 deletions.
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions ffi/dotnet/Devolutions.Picky/Generated/EcCurve.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions ffi/dotnet/Devolutions.Picky/Generated/RawEcCurve.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions ffi/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ impl From<picky::key::EcCurve> for EcCurve {
match value {
picky::key::EcCurve::NistP256 => Self::NistP256,
picky::key::EcCurve::NistP384 => Self::NistP384,
picky::key::EcCurve::NistP521 => Self::NistP521,
}
}
}
Expand All @@ -16,6 +17,7 @@ impl From<EcCurve> for picky::key::EcCurve {
match value {
EcCurve::NistP256 => Self::NistP256,
EcCurve::NistP384 => Self::NistP384,
EcCurve::NistP521 => Self::NistP521,
}
}
}
Expand Down Expand Up @@ -70,6 +72,8 @@ pub mod ffi {
NistP256,
/// NIST P-384
NistP384,
/// NIST P-521
NistP521,
}

/// Known Edwards curve-based algorithm name
Expand Down
15 changes: 15 additions & 0 deletions ffi/wasm/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions ffi/wasm/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ impl From<picky::key::EcCurve> for EcCurve {
match value {
picky::key::EcCurve::NistP256 => Self::NistP256,
picky::key::EcCurve::NistP384 => Self::NistP384,
picky::key::EcCurve::NistP521 => Self::NistP521,
}
}
}
Expand All @@ -17,6 +18,7 @@ impl From<EcCurve> for picky::key::EcCurve {
match value {
EcCurve::NistP256 => Self::NistP256,
EcCurve::NistP384 => Self::NistP384,
EcCurve::NistP521 => Self::NistP521,
}
}
}
Expand All @@ -29,6 +31,8 @@ pub enum EcCurve {
NistP256,
/// NIST P-384
NistP384,
/// NIST P-521
NistP521,
}

impl From<picky::key::EdAlgorithm> for EdAlgorithm {
Expand Down
1 change: 1 addition & 0 deletions picky/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand Down
44 changes: 44 additions & 0 deletions picky/src/jose/jwe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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 });
Expand Down
27 changes: 20 additions & 7 deletions picky/src/jose/jwk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}),
Expand Down Expand Up @@ -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()?);
Expand Down Expand Up @@ -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();
Expand Down
39 changes: 22 additions & 17 deletions picky/src/jose/jws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
};

Expand Down Expand Up @@ -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"),
};

Expand Down Expand Up @@ -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 {
Expand All @@ -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"}"#;
Expand Down
Loading

0 comments on commit 518d5ea

Please sign in to comment.