Skip to content

Commit 225dcbc

Browse files
committed
feat(ecdsa): refactor ES512 with macro-based implementation and comprehensive tests
- Refactor P-521 signer/verifier into define_p521_signer! and define_p521_verifier! macros - Implement safe PKCS8 key extraction using slice-to-array conversion (no unsafe code) - Custom ASN.1 DER parser for P-521 nested ECPrivateKey structure (66-byte key extraction) - Add three ES512 integration tests (sign/verify, PEM, claim round-trip) - Include P-521 test keys in PEM and DER formats for test suite
1 parent 009d84a commit 225dcbc

6 files changed

Lines changed: 215 additions & 43 deletions

File tree

src/crypto/rust_crypto/ecdsa.rs

Lines changed: 118 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -89,61 +89,136 @@ define_ecdsa_signer!(Es384Signer, Algorithm::ES384, SigningKey384);
8989
define_ecdsa_verifier!(Es256Verifier, Algorithm::ES256, VerifyingKey256, Signature256);
9090
define_ecdsa_verifier!(Es384Verifier, Algorithm::ES384, VerifyingKey384, Signature384);
9191

92-
// P521 (ES512) uses a different API - no sign_recoverable
93-
pub struct Es512Signer(SigningKey521);
92+
// P521 (ES512) signer - uses different API (no sign_recoverable, different PKCS8 extraction)
93+
macro_rules! define_p521_signer {
94+
($name:ident, $alg:expr) => {
95+
pub struct $name(SigningKey521);
9496

95-
impl Es512Signer {
96-
pub(crate) fn new(encoding_key: &EncodingKey) -> Result<Self> {
97-
if encoding_key.family != AlgorithmFamily::Ec {
98-
return Err(new_error(ErrorKind::InvalidKeyFormat));
97+
impl $name {
98+
pub(crate) fn new(encoding_key: &EncodingKey) -> Result<Self> {
99+
if encoding_key.family != AlgorithmFamily::Ec {
100+
return Err(new_error(ErrorKind::InvalidKeyFormat));
101+
}
102+
103+
// Extract the raw 66-byte key from PKCS8 DER format
104+
let pkcs8_der = encoding_key.inner();
105+
let key_bytes = extract_p521_key_from_pkcs8(pkcs8_der)?;
106+
107+
// Verify correct length and convert to fixed-size array safely
108+
if key_bytes.len() != 66 {
109+
return Err(new_error(ErrorKind::InvalidEcdsaKey));
110+
}
111+
112+
// Safe conversion using slice_as_array pattern
113+
let mut key_array = [0u8; 66];
114+
key_array.copy_from_slice(&key_bytes);
115+
116+
// Convert array to GenericArray reference using From trait
117+
let field_bytes: &p521::FieldBytes = key_array.as_slice().try_into()
118+
.map_err(|_| ErrorKind::InvalidEcdsaKey)?;
119+
120+
Ok(Self(
121+
SigningKey521::from_bytes(field_bytes)
122+
.map_err(|_| ErrorKind::InvalidEcdsaKey)?,
123+
))
124+
}
99125
}
100126

101-
Ok(Self(
102-
SigningKey521::from_bytes(encoding_key.inner().into())
103-
.map_err(|_| ErrorKind::InvalidEcdsaKey)?,
104-
))
105-
}
106-
}
127+
impl Signer<Vec<u8>> for $name {
128+
fn try_sign(&self, msg: &[u8]) -> std::result::Result<Vec<u8>, Error> {
129+
let signature: Signature521 = self.0.sign(msg);
130+
Ok(signature.to_vec())
131+
}
132+
}
107133

108-
impl Signer<Vec<u8>> for Es512Signer {
109-
fn try_sign(&self, msg: &[u8]) -> std::result::Result<Vec<u8>, Error> {
110-
let signature: Signature521 = self.0.sign(msg);
111-
Ok(signature.to_vec())
112-
}
134+
impl JwtSigner for $name {
135+
fn algorithm(&self) -> Algorithm {
136+
$alg
137+
}
138+
}
139+
};
113140
}
114141

115-
impl JwtSigner for Es512Signer {
116-
fn algorithm(&self) -> Algorithm {
117-
Algorithm::ES512
118-
}
119-
}
142+
// P521 (ES512) verifier
143+
macro_rules! define_p521_verifier {
144+
($name:ident, $alg:expr) => {
145+
pub struct $name(VerifyingKey521);
120146

121-
pub struct Es512Verifier(VerifyingKey521);
147+
impl $name {
148+
pub(crate) fn new(decoding_key: &DecodingKey) -> Result<Self> {
149+
if decoding_key.family != AlgorithmFamily::Ec {
150+
return Err(new_error(ErrorKind::InvalidKeyFormat));
151+
}
122152

123-
impl Es512Verifier {
124-
pub(crate) fn new(decoding_key: &DecodingKey) -> Result<Self> {
125-
if decoding_key.family != AlgorithmFamily::Ec {
126-
return Err(new_error(ErrorKind::InvalidKeyFormat));
153+
Ok(Self(
154+
VerifyingKey521::from_sec1_bytes(decoding_key.as_bytes())
155+
.map_err(|_| ErrorKind::InvalidEcdsaKey)?,
156+
))
157+
}
127158
}
128159

129-
Ok(Self(
130-
VerifyingKey521::from_sec1_bytes(decoding_key.as_bytes())
131-
.map_err(|_| ErrorKind::InvalidEcdsaKey)?,
132-
))
133-
}
134-
}
160+
impl Verifier<Vec<u8>> for $name {
161+
fn verify(&self, msg: &[u8], signature: &Vec<u8>) -> std::result::Result<(), Error> {
162+
self.0
163+
.verify(msg, &Signature521::from_slice(signature).map_err(Error::from_source)?)
164+
.map_err(Error::from_source)?;
165+
Ok(())
166+
}
167+
}
135168

136-
impl Verifier<Vec<u8>> for Es512Verifier {
137-
fn verify(&self, msg: &[u8], signature: &Vec<u8>) -> std::result::Result<(), Error> {
138-
self.0
139-
.verify(msg, &Signature521::from_slice(signature).map_err(Error::from_source)?)
140-
.map_err(Error::from_source)?;
141-
Ok(())
142-
}
169+
impl JwtVerifier for $name {
170+
fn algorithm(&self) -> Algorithm {
171+
$alg
172+
}
173+
}
174+
};
143175
}
144176

145-
impl JwtVerifier for Es512Verifier {
146-
fn algorithm(&self) -> Algorithm {
147-
Algorithm::ES512
177+
define_p521_signer!(Es512Signer, Algorithm::ES512);
178+
define_p521_verifier!(Es512Verifier, Algorithm::ES512);
179+
180+
/// Extract the 66-byte P-521 private key from PKCS8 DER format
181+
///
182+
/// P-521 keys in PKCS8 format have a different structure than the standard P256/P384:
183+
/// PKCS8 ::= SEQUENCE {
184+
/// version INTEGER,
185+
/// algorithm AlgorithmIdentifier,
186+
/// PrivateKey OCTET STRING
187+
/// }
188+
/// The PrivateKey octet string contains a DER-encoded ECPrivateKey SEQUENCE:
189+
/// ECPrivateKey ::= SEQUENCE {
190+
/// version INTEGER,
191+
/// privateKey OCTET STRING (66 bytes for P-521)
192+
/// }
193+
fn extract_p521_key_from_pkcs8(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
194+
let asn1_blocks = simple_asn1::from_der(pkcs8_der)
195+
.map_err(|_| ErrorKind::InvalidKeyFormat)?;
196+
197+
for block in asn1_blocks {
198+
if let simple_asn1::ASN1Block::Sequence(_, entries) = block {
199+
// The third element (index 2) should be the privateKey OCTET STRING
200+
if entries.len() >= 3 {
201+
if let simple_asn1::ASN1Block::OctetString(_, value) = &entries[2] {
202+
// The value is DER-encoded and contains a SEQUENCE with the actual key
203+
if let Ok(inner_blocks) = simple_asn1::from_der(value) {
204+
for inner_block in inner_blocks {
205+
if let simple_asn1::ASN1Block::Sequence(_, inner_entries) = inner_block {
206+
// Look for the OCTET STRING within this sequence
207+
for inner_entry in inner_entries {
208+
if let simple_asn1::ASN1Block::OctetString(_, key_value) = inner_entry {
209+
// This should be our 66-byte key
210+
if key_value.len() == 66 {
211+
return Ok(key_value.to_vec());
212+
}
213+
}
214+
}
215+
}
216+
}
217+
}
218+
}
219+
}
220+
}
148221
}
222+
223+
Err(new_error(ErrorKind::InvalidKeyFormat))
149224
}

tests/ecdsa/mod.rs

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

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)