Skip to content

Commit fd96c1c

Browse files
committed
feat: add ES512 algorithm support with P-521 elliptic curve
- Add p521 dependency to Cargo.toml (version 0.13.0 with ecdsa feature) - Add ES512 to Algorithm enum and AlgorithmFamily mappings - Implement ES512 signer and verifier using P521 elliptic curve - Register Es512Signer and Es512Verifier in CryptoProvider factory - Add ES512 to KeyAlgorithm enum for JWK support - Custom macros for P521 due to API differences (no from_pkcs8_der, no sign_recoverable) - Manual PKCS8 extraction for 66-byte P521 private key - Add comprehensive ES512 tests (3 new test functions) - Create ES512 test keys in both PEM and PKCS8 formats - Gate ES512 tests behind use_pem feature flag
1 parent abbc307 commit fd96c1c

10 files changed

Lines changed: 238 additions & 3 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ ed25519-dalek = { version = "2.1.1", optional = true, features = ["pkcs8"] }
4040
hmac = { version = "0.12.1", optional = true }
4141
p256 = { version = "0.13.2", optional = true, features = ["ecdsa"] }
4242
p384 = { version = "0.13.0", optional = true, features = ["ecdsa"] }
43+
p521 = { version = "0.13.0", optional = true, features = ["ecdsa"] }
4344
rand = { version = "0.8.5", optional = true, features = ["std"], default-features = false }
4445
rsa = { version = "0.9.6", optional = true }
4546
sha2 = { version = "0.10.7", optional = true, features = ["oid"] }
@@ -66,7 +67,7 @@ criterion = { version = "0.8", default-features = false }
6667
[features]
6768
default = ["use_pem"]
6869
use_pem = ["pem", "simple_asn1"]
69-
rust_crypto = ["ed25519-dalek", "hmac", "p256", "p384", "rand", "rsa", "sha2"]
70+
rust_crypto = ["ed25519-dalek", "hmac", "p256", "p384", "p521", "rand", "rsa", "sha2"]
7071
aws_lc_rs = ["aws-lc-rs"]
7172

7273
[[bench]]

src/algorithms.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ impl AlgorithmFamily {
3030
Algorithm::PS384,
3131
Algorithm::PS512,
3232
],
33-
Self::Ec => &[Algorithm::ES256, Algorithm::ES384],
33+
Self::Ec => &[Algorithm::ES256, Algorithm::ES384, Algorithm::ES512],
3434
Self::Ed => &[Algorithm::EdDSA],
3535
}
3636
}
@@ -52,6 +52,8 @@ pub enum Algorithm {
5252
ES256,
5353
/// ECDSA using SHA-384
5454
ES384,
55+
/// ECDSA using SHA-512
56+
ES512,
5557

5658
/// RSASSA-PKCS1-v1_5 using SHA-256
5759
RS256,
@@ -80,6 +82,7 @@ impl FromStr for Algorithm {
8082
"HS512" => Ok(Algorithm::HS512),
8183
"ES256" => Ok(Algorithm::ES256),
8284
"ES384" => Ok(Algorithm::ES384),
85+
"ES512" => Ok(Algorithm::ES512),
8386
"RS256" => Ok(Algorithm::RS256),
8487
"RS384" => Ok(Algorithm::RS384),
8588
"PS256" => Ok(Algorithm::PS256),
@@ -102,7 +105,7 @@ impl Algorithm {
102105
| Algorithm::PS256
103106
| Algorithm::PS384
104107
| Algorithm::PS512 => AlgorithmFamily::Rsa,
105-
Algorithm::ES256 | Algorithm::ES384 => AlgorithmFamily::Ec,
108+
Algorithm::ES256 | Algorithm::ES384 | Algorithm::ES512 => AlgorithmFamily::Ec,
106109
Algorithm::EdDSA => AlgorithmFamily::Ed,
107110
}
108111
}

src/crypto/rust_crypto/ecdsa.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ use p256::ecdsa::{
1111
use p384::ecdsa::{
1212
Signature as Signature384, SigningKey as SigningKey384, VerifyingKey as VerifyingKey384,
1313
};
14+
use p521::ecdsa::{
15+
Signature as Signature521, SigningKey as SigningKey521, VerifyingKey as VerifyingKey521,
16+
};
1417
use rsa::pkcs8::DecodePrivateKey;
1518
use signature::{Error, Signer, Verifier};
1619

@@ -85,3 +88,127 @@ define_ecdsa_signer!(Es384Signer, Algorithm::ES384, SigningKey384);
8588

8689
define_ecdsa_verifier!(Es256Verifier, Algorithm::ES256, VerifyingKey256, Signature256);
8790
define_ecdsa_verifier!(Es384Verifier, Algorithm::ES384, VerifyingKey384, Signature384);
91+
92+
// P521 (ES512) requires custom macros instead of the generic ones because:
93+
// 1. SigningKey521 doesn't implement DecodePrivateKey (no from_pkcs8_der), so we manually extract the key
94+
// 2. SigningKey521 doesn't have sign_recoverable(), only the regular sign() method
95+
// These API differences in the p521 crate necessitate separate implementations.
96+
// P521 (ES512) signer - requires PKCS8 extraction
97+
macro_rules! define_p521_signer {
98+
($name:ident, $alg:expr) => {
99+
pub struct $name(SigningKey521);
100+
101+
impl $name {
102+
pub(crate) fn new(encoding_key: &EncodingKey) -> Result<Self> {
103+
if encoding_key.family() != AlgorithmFamily::Ec {
104+
return Err(new_error(ErrorKind::InvalidKeyFormat));
105+
}
106+
107+
// For P521, we need to extract the 66-byte key from PKCS8 DER format
108+
let pkcs8_der = encoding_key.inner();
109+
let key_bytes = extract_p521_key_from_pkcs8(pkcs8_der)?;
110+
111+
// Convert to FieldBytes and create SigningKey
112+
let field_bytes: &p521::FieldBytes = key_bytes
113+
.as_slice()
114+
.try_into()
115+
.map_err(|_| ErrorKind::InvalidEcdsaKey)?;
116+
117+
Ok(Self(
118+
SigningKey521::from_bytes(field_bytes)
119+
.map_err(|_| ErrorKind::InvalidEcdsaKey)?,
120+
))
121+
}
122+
}
123+
124+
impl Signer<Vec<u8>> for $name {
125+
fn try_sign(&self, msg: &[u8]) -> std::result::Result<Vec<u8>, Error> {
126+
let signature: Signature521 = self.0.sign(msg);
127+
Ok(signature.to_vec())
128+
}
129+
}
130+
131+
impl JwtSigner for $name {
132+
fn algorithm(&self) -> Algorithm {
133+
$alg
134+
}
135+
}
136+
};
137+
}
138+
139+
// P521 (ES512) verifier
140+
macro_rules! define_p521_verifier {
141+
($name:ident, $alg:expr) => {
142+
pub struct $name(VerifyingKey521);
143+
144+
impl $name {
145+
pub(crate) fn new(decoding_key: &DecodingKey) -> Result<Self> {
146+
if decoding_key.family() != AlgorithmFamily::Ec {
147+
return Err(new_error(ErrorKind::InvalidKeyFormat));
148+
}
149+
150+
Ok(Self(
151+
VerifyingKey521::from_sec1_bytes(decoding_key.as_bytes())
152+
.map_err(|_| ErrorKind::InvalidEcdsaKey)?,
153+
))
154+
}
155+
}
156+
157+
impl Verifier<Vec<u8>> for $name {
158+
fn verify(&self, msg: &[u8], signature: &Vec<u8>) -> std::result::Result<(), Error> {
159+
self.0
160+
.verify(msg, &Signature521::from_slice(signature).map_err(Error::from_source)?)
161+
.map_err(Error::from_source)?;
162+
Ok(())
163+
}
164+
}
165+
166+
impl JwtVerifier for $name {
167+
fn algorithm(&self) -> Algorithm {
168+
$alg
169+
}
170+
}
171+
};
172+
}
173+
174+
define_p521_signer!(Es512Signer, Algorithm::ES512);
175+
define_p521_verifier!(Es512Verifier, Algorithm::ES512);
176+
177+
/// Extract the 66-byte P-521 private key from PKCS8 DER format
178+
fn extract_p521_key_from_pkcs8(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
179+
use rsa::pkcs8::PrivateKeyInfo;
180+
181+
// Decode as PKCS8 structure
182+
let private_key_info = PrivateKeyInfo::try_from(pkcs8_der)
183+
.map_err(|_| ErrorKind::InvalidKeyFormat)?;
184+
185+
// The private key bytes should be in the private_key field
186+
// For P-521 in PKCS8, this is a DER-encoded ECPrivateKey which contains the 66-byte key
187+
let private_key_bytes = private_key_info.private_key;
188+
189+
// Parse the ECPrivateKey structure (which is a SEQUENCE with the key as an OCTET STRING)
190+
use simple_asn1::ASN1Block;
191+
let asn1_blocks = simple_asn1::from_der(private_key_bytes)
192+
.map_err(|_| ErrorKind::InvalidKeyFormat)?;
193+
194+
for block in asn1_blocks {
195+
if let ASN1Block::Sequence(_, entries) = block {
196+
// ECPrivateKey ::= SEQUENCE {
197+
// version INTEGER { ecPrivkeyVer1(0) }
198+
// privateKey OCTET STRING,
199+
// parameters [0] ECParameters OPTIONAL,
200+
// publicKey [1] BIT STRING OPTIONAL
201+
// }
202+
if entries.len() >= 2 {
203+
// The second element (index 1) should be the privateKey OCTET STRING
204+
if let ASN1Block::OctetString(_, key_bytes) = &entries[1] {
205+
if key_bytes.len() == 66 {
206+
return Ok(key_bytes.clone());
207+
}
208+
}
209+
}
210+
}
211+
}
212+
213+
Err(new_error(ErrorKind::InvalidKeyFormat))
214+
}

src/crypto/rust_crypto/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ fn new_signer(algorithm: &Algorithm, key: &EncodingKey) -> Result<Box<dyn JwtSig
7070
Algorithm::HS512 => Box::new(hmac::Hs512Signer::new(key)?) as Box<dyn JwtSigner>,
7171
Algorithm::ES256 => Box::new(ecdsa::Es256Signer::new(key)?) as Box<dyn JwtSigner>,
7272
Algorithm::ES384 => Box::new(ecdsa::Es384Signer::new(key)?) as Box<dyn JwtSigner>,
73+
Algorithm::ES512 => Box::new(ecdsa::Es512Signer::new(key)?) as Box<dyn JwtSigner>,
7374
Algorithm::RS256 => Box::new(rsa::Rsa256Signer::new(key)?) as Box<dyn JwtSigner>,
7475
Algorithm::RS384 => Box::new(rsa::Rsa384Signer::new(key)?) as Box<dyn JwtSigner>,
7576
Algorithm::RS512 => Box::new(rsa::Rsa512Signer::new(key)?) as Box<dyn JwtSigner>,
@@ -92,6 +93,7 @@ fn new_verifier(
9293
Algorithm::HS512 => Box::new(hmac::Hs512Verifier::new(key)?) as Box<dyn JwtVerifier>,
9394
Algorithm::ES256 => Box::new(ecdsa::Es256Verifier::new(key)?) as Box<dyn JwtVerifier>,
9495
Algorithm::ES384 => Box::new(ecdsa::Es384Verifier::new(key)?) as Box<dyn JwtVerifier>,
96+
Algorithm::ES512 => Box::new(ecdsa::Es512Verifier::new(key)?) as Box<dyn JwtVerifier>,
9597
Algorithm::RS256 => Box::new(rsa::Rsa256Verifier::new(key)?) as Box<dyn JwtVerifier>,
9698
Algorithm::RS384 => Box::new(rsa::Rsa384Verifier::new(key)?) as Box<dyn JwtVerifier>,
9799
Algorithm::RS512 => Box::new(rsa::Rsa512Verifier::new(key)?) as Box<dyn JwtVerifier>,

src/jwk.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ pub enum KeyAlgorithm {
164164
ES256,
165165
/// ECDSA using SHA-384
166166
ES384,
167+
/// ECDSA using SHA-512
168+
ES512,
167169

168170
/// RSASSA-PKCS1-v1_5 using SHA-256
169171
RS256,
@@ -207,6 +209,7 @@ impl FromStr for KeyAlgorithm {
207209
"HS512" => Ok(KeyAlgorithm::HS512),
208210
"ES256" => Ok(KeyAlgorithm::ES256),
209211
"ES384" => Ok(KeyAlgorithm::ES384),
212+
"ES512" => Ok(KeyAlgorithm::ES512),
210213
"RS256" => Ok(KeyAlgorithm::RS256),
211214
"RS384" => Ok(KeyAlgorithm::RS384),
212215
"PS256" => Ok(KeyAlgorithm::PS256),
@@ -444,6 +447,7 @@ impl Jwk {
444447
Algorithm::HS512 => KeyAlgorithm::HS512,
445448
Algorithm::ES256 => KeyAlgorithm::ES256,
446449
Algorithm::ES384 => KeyAlgorithm::ES384,
450+
Algorithm::ES512 => KeyAlgorithm::ES512,
447451
Algorithm::RS256 => KeyAlgorithm::RS256,
448452
Algorithm::RS384 => KeyAlgorithm::RS384,
449453
Algorithm::RS512 => KeyAlgorithm::RS512,

tests/ecdsa/mod.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,73 @@ fn ec_jwk_from_key() {
191191
.unwrap()
192192
);
193193
}
194+
195+
// ES512 Tests
196+
#[cfg(feature = "use_pem")]
197+
#[test]
198+
#[wasm_bindgen_test]
199+
fn es512_round_trip_sign_verification_pem() {
200+
let privkey_pem = include_bytes!("private_es512_key.pem");
201+
let pubkey_pem = include_bytes!("public_es512_key.pem");
202+
203+
let encrypted =
204+
sign(b"hello world", &EncodingKey::from_ec_pem(privkey_pem).unwrap(), Algorithm::ES512)
205+
.unwrap();
206+
let is_valid = verify(
207+
&encrypted,
208+
b"hello world",
209+
&DecodingKey::from_ec_pem(pubkey_pem).unwrap(),
210+
Algorithm::ES512,
211+
)
212+
.unwrap();
213+
assert!(is_valid);
214+
}
215+
216+
#[cfg(feature = "use_pem")]
217+
#[test]
218+
#[wasm_bindgen_test]
219+
fn es512_round_trip_claim() {
220+
let privkey_pem = include_bytes!("private_es512_key.pem");
221+
let pubkey_pem = include_bytes!("public_es512_key.pem");
222+
let my_claims = Claims {
223+
sub: "es512@example.com".to_string(),
224+
company: "ACME".to_string(),
225+
exp: OffsetDateTime::now_utc().unix_timestamp() + 10000,
226+
};
227+
let token = encode(
228+
&Header::new(Algorithm::ES512),
229+
&my_claims,
230+
&EncodingKey::from_ec_pem(privkey_pem).unwrap(),
231+
)
232+
.unwrap();
233+
let token_data = decode::<Claims>(
234+
&token,
235+
&DecodingKey::from_ec_pem(pubkey_pem).unwrap(),
236+
&Validation::new(Algorithm::ES512),
237+
)
238+
.unwrap();
239+
assert_eq!(my_claims, token_data.claims);
240+
}
241+
242+
#[cfg(feature = "use_pem")]
243+
#[test]
244+
#[wasm_bindgen_test]
245+
fn es512_sign_and_verify() {
246+
let privkey_pem = include_bytes!("private_es512_key.pem");
247+
let pubkey_pem = include_bytes!("public_es512_key.pem");
248+
let message = b"test message for ES512";
249+
250+
// Sign the message
251+
let encrypted = sign(message, &EncodingKey::from_ec_pem(privkey_pem).unwrap(), Algorithm::ES512)
252+
.unwrap();
253+
254+
// Verify the signature
255+
let is_valid = verify(
256+
&encrypted,
257+
message,
258+
&DecodingKey::from_ec_pem(pubkey_pem).unwrap(),
259+
Algorithm::ES512,
260+
)
261+
.unwrap();
262+
assert!(is_valid);
263+
}

tests/ecdsa/private_es512_key.pem

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIASpHRYZx6l+CIFdI2
3+
9MO1GGnfy4eyWXApZLmQUm9nbZCX2MDY6VB63umkLii3h+ng899S2GNqpWpqK4oc
4+
TOwlL16hgYkDgYYABABoIJ4A1xiM93QfTORva8sVTWyrqNFC8VaTA9wNbHTV+6U/
5+
SyG1IiQ/wjdmHNzZmXMNah/ICrJGcvrJkN8Ol3tEFgD346qAuxWQp5OF4Fvadluo
6+
uN/z8IPoeGtWIcTeU2xiJMBohyAKBR4j7yCKVVrQ7FFZ6di4LikqgloUeaMeGLop
7+
OA==
8+
-----END PRIVATE KEY-----

tests/ecdsa/private_es512_key.pk8

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIASBiTacIylSnlw2mi
3+
We9ggHXP2wQgQVjWd4GkbPpK54c1hn3j4qHxNroYE3O5A1JkIHCBvMxAmDZexpZP
4+
W0+vG1KhgYkDgYYABACPR/NfSO17emjwePAo/R95JsUGT1ensaDsIE+K86LaqF30
5+
Ji/sg0eW+OOkQG4tVplFzVIDBftPA/gLzUdMslr2OABPthB0tgMnDU99O8+w0n5m
6+
WbbZ9rs2T1WW6nkGHPH1aJ/4hNuz8HgZ8Tyg66k2ugwH+i9HDSMPK5gbxOF4K1x0
7+
yA==
8+
-----END PRIVATE KEY-----

tests/ecdsa/public_es512_key.pem

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAaCCeANcYjPd0H0zkb2vLFU1sq6jR
3+
QvFWkwPcDWx01fulP0shtSIkP8I3Zhzc2ZlzDWofyAqyRnL6yZDfDpd7RBYA9+Oq
4+
gLsVkKeTheBb2nZbqLjf8/CD6HhrViHE3lNsYiTAaIcgCgUeI+8gilVa0OxRWenY
5+
uC4pKoJaFHmjHhi6KTg=
6+
-----END PUBLIC KEY-----

tests/ecdsa/public_es512_key.pk8

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAj0fzX0jte3po8HjwKP0feSbFBk9X
3+
p7Gg7CBPivOi2qhd9CYv7INHlvjjpEBuLVaZRc1SAwX7TwP4C81HTLJa9jgAT7YQ
4+
dLYDJw1PfTvPsNJ+Zlm22fa7Nk9Vlup5Bhzx9Wif+ITbs/B4GfE8oOupNroMB/ov
5+
Rw0jDyuYG8TheCtcdMg=
6+
-----END PUBLIC KEY-----

0 commit comments

Comments
 (0)