1use aws_lc_rs::signature::{KeyPair as _, ParsedPublicKey};
33use aws_lc_rs::unstable::signature::{ML_DSA_65, ML_DSA_65_SIGNING, PqdsaKeyPair};
34use base64ct::{Base64UrlUnpadded, Encoding as _};
35use sha2::{Digest as _, Sha256, Sha512};
36
37use crate::misc::error::{OPAQUE, Opaque};
38use crate::misc::jwt;
39use crate::misc::serde_ext::bytes_wrapper::B64;
40
41pub const ALG: &str = "ph-ML-DSA-65-Ed25519";
44
45const PREFIX: &[u8] = b"CompositeAlgorithmSignatures2025";
48
49const LABEL: &[u8] = b"COMPSIG-MLDSA65-Ed25519-SHA512";
55
56const CTX: &[u8] = b"";
59
60const _: () = assert!(CTX.len() <= 255);
62
63const ML_DSA_CTX: &[u8] = b"";
69
70const _: () = assert!(ML_DSA_CTX.is_empty());
75
76const ED25519_SIG_LEN: usize = ed25519_dalek::SIGNATURE_LENGTH;
78
79const M_PRIME_LEN: usize = PREFIX.len() + LABEL.len() + 1 + CTX.len() + 64;
82
83#[derive(Debug)]
85pub struct SigningKey {
86 ed: ed25519_dalek::SigningKey,
87 ml: PqdsaKeyPair,
88
89 ml_seed: zeroize::Zeroizing<[u8; 32]>,
92
93 vk: VerifyingKey,
96}
97
98#[derive(Clone, Debug)]
100pub struct VerifyingKey {
101 ed: ed25519_dalek::VerifyingKey,
102
103 ml: ParsedPublicKey,
105}
106
107#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, zeroize::ZeroizeOnDrop)]
109pub struct SigningKeyBytes {
110 ed: B64,
112
113 ml: B64,
115}
116
117#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
119pub struct VerifyingKeyBytes {
120 pub ed: B64,
122
123 pub ml: B64,
125}
126
127impl SigningKey {
128 pub fn generate() -> Result<Self, Opaque> {
130 Self::from_parts(
131 crate::misc::crypto::random_32_bytes(),
132 crate::misc::crypto::random_32_bytes(),
133 )
134 }
135
136 pub fn verifying_key(&self) -> &VerifyingKey {
141 &self.vk
142 }
143
144 pub fn encode(&self) -> SigningKeyBytes {
146 SigningKeyBytes {
147 ed: B64::from_bytes(self.ed.to_bytes()),
148 ml: B64::from_bytes(*self.ml_seed),
149 }
150 }
151
152 pub fn ed25519_signing_key(&self) -> &ed25519_dalek::SigningKey {
158 &self.ed
159 }
160
161 fn from_parts(ed_seed: [u8; 32], ml_seed: [u8; 32]) -> Result<Self, Opaque> {
166 let ed = ed25519_dalek::SigningKey::from_bytes(&ed_seed);
167 let ml = PqdsaKeyPair::from_seed(&ML_DSA_65_SIGNING, &ml_seed).map_err(|_| OPAQUE)?;
168 let vk = VerifyingKey {
169 ed: ed.verifying_key(),
170 ml: ParsedPublicKey::new(&ML_DSA_65, ml.public_key().as_ref()).map_err(|_| OPAQUE)?,
171 };
172 Ok(Self {
173 ed,
174 ml,
175 ml_seed: zeroize::Zeroizing::new(ml_seed),
176 vk,
177 })
178 }
179}
180
181impl SigningKeyBytes {
182 pub fn decode(&self) -> Result<SigningKey, Opaque> {
184 let ed_seed: [u8; 32] = (&self.ed[..]).try_into()?;
185 let ml_seed: [u8; 32] = (&self.ml[..]).try_into()?;
186 SigningKey::from_parts(ed_seed, ml_seed)
187 }
188}
189
190impl VerifyingKey {
191 pub fn encode(&self) -> VerifyingKeyBytes {
193 VerifyingKeyBytes {
194 ed: B64::from_bytes(self.ed.to_bytes()),
195 ml: B64::from_bytes(self.ml.as_ref()),
196 }
197 }
198
199 pub fn ed25519_bytes(&self) -> [u8; 32] {
202 self.ed.to_bytes()
203 }
204}
205
206impl VerifyingKeyBytes {
207 pub fn decode(&self) -> Result<VerifyingKey, Opaque> {
209 let ed_bytes: [u8; 32] = (&self.ed[..]).try_into()?;
210 Ok(VerifyingKey {
211 ed: ed25519_dalek::VerifyingKey::from_bytes(&ed_bytes)?,
212 ml: ParsedPublicKey::new(&ML_DSA_65, &self.ml[..]).map_err(|_| OPAQUE)?,
213 })
214 }
215}
216
217impl PartialEq for VerifyingKey {
220 fn eq(&self, other: &Self) -> bool {
221 self.ed == other.ed && self.ml.as_ref() == other.ml.as_ref()
222 }
223}
224
225impl Eq for VerifyingKey {}
226
227impl jwt::Key for SigningKey {
228 const ALG: &'static str = ALG;
229}
230
231impl jwt::Key for VerifyingKey {
232 const ALG: &'static str = ALG;
233}
234
235impl jwt::SigningKey for SigningKey {
236 type Signature = Vec<u8>;
237
238 fn sign(&self, message: &[u8]) -> anyhow::Result<Vec<u8>> {
239 let m_prime = message_representative(message);
240
241 let ml_sig_len = ML_DSA_65_SIGNING.signature_len();
243 let mut signature = vec![0u8; ml_sig_len + ED25519_SIG_LEN];
244 ml_dsa_sign(&self.ml, &m_prime, &mut signature[..ml_sig_len])
245 .map_err(|_| anyhow::anyhow!("ML-DSA signing failed"))?;
246 signature[ml_sig_len..]
247 .copy_from_slice(&ed25519_dalek::Signer::sign(&self.ed, &m_prime).to_bytes());
248 Ok(signature)
249 }
250
251 fn jwk(&self) -> serde_json::Value {
252 let mut pk = self.ml.public_key().as_ref().to_vec();
254 pk.extend_from_slice(self.ed.verifying_key().as_bytes());
255
256 serde_json::json!({
257 "kty": "AKP",
258 "alg": ALG,
259 "pub": Base64UrlUnpadded::encode_string(&pk),
260 "kid": jwk_thumbprint(ALG, &pk),
261 "use": "sig",
262 })
263 }
264}
265
266impl jwt::VerifyingKey for VerifyingKey {
267 fn is_valid_signature(&self, message: &[u8], signature: Vec<u8>) -> bool {
268 let ml_sig_len = ML_DSA_65_SIGNING.signature_len();
269 if signature.len() != ml_sig_len + ED25519_SIG_LEN {
270 return false;
271 }
272 let (ml_sig, ed_sig) = signature.split_at(ml_sig_len);
273
274 let m_prime = message_representative(message);
275
276 if !ml_dsa_verify(&self.ml, &m_prime, ml_sig) {
278 return false;
279 }
280 let Ok(ed_sig) = ed25519_dalek::Signature::from_slice(ed_sig) else {
281 return false;
282 };
283 ed25519_dalek::Verifier::verify(&self.ed, &m_prime, &ed_sig).is_ok()
284 }
285
286 fn describe(&self) -> String {
287 let mut pubkey = self.ml.as_ref().to_vec();
290 pubkey.extend_from_slice(self.ed.as_bytes());
291 format!("{ALG} key #{}", jwk_thumbprint(ALG, &pubkey))
292 }
293}
294
295fn jwk_thumbprint(alg: &str, pub_bytes: &[u8]) -> String {
302 let canonical = format!(
303 r#"{{"alg":"{alg}","kty":"AKP","pub":"{}"}}"#,
304 Base64UrlUnpadded::encode_string(pub_bytes)
305 );
306 Base64UrlUnpadded::encode_string(Sha256::digest(canonical.as_bytes()).as_slice())
307}
308
309fn message_representative(message: &[u8]) -> [u8; M_PRIME_LEN] {
315 let mut m_prime = [0u8; M_PRIME_LEN];
316 let mut at = 0;
317 m_prime[at..at + PREFIX.len()].copy_from_slice(PREFIX);
318 at += PREFIX.len();
319 m_prime[at..at + LABEL.len()].copy_from_slice(LABEL);
320 at += LABEL.len();
321 m_prime[at] = CTX.len() as u8; at += 1;
323 m_prime[at..at + CTX.len()].copy_from_slice(CTX);
324 at += CTX.len();
325 m_prime[at..].copy_from_slice(Sha512::digest(message).as_slice());
326 m_prime
327}
328
329fn ml_dsa_sign(keypair: &PqdsaKeyPair, m_prime: &[u8], out: &mut [u8]) -> Result<(), Opaque> {
336 keypair.sign(m_prime, out).map_err(|_| OPAQUE)?;
338 Ok(())
339}
340
341fn ml_dsa_verify(public_key: &ParsedPublicKey, m_prime: &[u8], signature: &[u8]) -> bool {
343 public_key.verify_sig(m_prime, signature).is_ok()
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use crate::misc::jwt::{self, Claims, SigningKey as _, VerifyingKey as _};
350 use base64ct::Base64UrlUnpadded;
351
352 #[test]
357 fn jwk_thumbprint_matches_rfc9964() {
358 const PUB: &str = "unH59k4RuutY-pxvu24U5h8YZD2rSVtHU5qRZsoBmBMcRPgmu9VuNOVdteXi1zNIXjnqJg_GAAxepLqA00Vc3lO0bzRIKu39VFD8Lhuk8l0V-cFEJC-zm7UihxiQMMUEmOFxe3x1ixkKZ0jqmqP3rKryx8tSbtcXyfea64QhT6XNje2SoMP6FViBDxLHBQo2dwjRls0k5a-XSQSu2OTOiHLoaWsLe8pQ5FLNfTDqmkrawDEdZyxr3oSWJAsHQxRjcIiVzZuvwxYy1zl2STiP2vy_fTBaPemkleynQzqPg7oPCyXEE8bjnJbrfWkbNNN8438e6tHPIX4l7zTuzz98YPhLjt_d6EBdT4MldsYe-Y4KLyjaGHcAlTkk9oa5RhRwW89T0z_t1DSO3dvfKLUGXh8gd1BD6Fz5MfgpF5NjoafnQEqDjsAAhrCXY4b-Y3yYJEdX4_dp3dRGdHG_rWcPmgX4JG7lCnser4f8QGnDriqiAzJYEXeS8LzUngg_0bx0lqv_KcyU5IaLISFO0xZSU5mmEPvdSoDnyAcV8pV44qhLtAvd29n0ehG259oRihtljTWeiu9V60a1N2tbZVl5mEqSK-6_xZvNYA1TCdzNctvweH24unV7U3wer9XA9Q6kvJWDVJ4oKaQsKMrCSMlteBJMRxWbGK7ddUq6F7GdQw-3j2M-qdJvVKm9UPjY9rc1lPgol25-oJxTu7nxGlbJUH-4m5pevAN6NyZ6lfhbjWTKlxkrEKZvQXs_Yf6cpXEwpI_ZJeriq1UC1XHIpRkDwdOY9MH3an4RdDl2r9vGl_IwlKPNdh_5aF3jLgn7PCit1FNJAwC8fIncAXgAlgcXIpRXdfJk4bBiO89GGccSyDh2EgXYdpG3XvNgGWy7npuSoNTE7WIyblAk13UQuO4sdCbMIuriCdyfE73mvwj15xgb07RZRQtFGlFTmnFcIdZ90zDrWXDbANntv7KCKwNvoTuv64bY3HiGbj-NQ-U9eMylWVpvr4hrXcES8c9K3PqHWADZC0iIOvlzFv4VBoc_wVflcOrL_SIoaNFCNBAZZq-2v5lAgpJTqVOtqJ_HVraoSfcKy5g45p-qULunXj6Jwq21fobQiKubBKKOZwcJFyJD7F4ACKXOrz-HIvSHMCWW_9dVrRuCpJw0s0aVFbRqopDNhu446nqb4_EDYQM1tTHMozPd_jKxRRD0sH75X8ZoToxFSpLBDbtdWcenxj-zBf6IGWfZnmaetjKEBYJWC7QDQx1A91pJVJCEgieCkoIfTqkeQuePpIyu48g2FG3P1zjRF-kumhUTfSjo5qS0YiZQy0E1BMs6M11EvuxXRsHClLHoy5nLYI2Sj4zjVjYyxSHyPRPGGo9hwB34yWxzYNtPPGiqXS_dNCpi_zRZwRY4lCGrQ-hYTEWIK1Dm5OlttvC4_eiQ1dv63NiGkLRJ5kJA3bICN0fzCDY-MBqnd1cWn8YVBijVkgtaoascjL9EywDgJdeHnXK0eeOvUxHHhXJVkNqcibn8O4RQdpVU60TSA-uiu675ytIjcBHC6kTv8A8pmkj_4oypPd-F92YIJC741swkYQoeIHj8rE-ThcMUkF7KqC5VORbZTRp8HsZSqgiJcIPaouuxd1-8Rxrid3fXkE6p8bkrysPYoxWEJgh7ZFsRCPDWX-yTeJwFN0PKFP1j0F6YtlLfK5wv-c4F8ZQHA_-yc_gODicy7KmWDZgbTP07e7gEWzw4MFRrndjbDQ";
359 const KID: &str = "T4xl70S7MT6Zeq6r9V9fPJGVn76wfnXJ21-gyo0Gu6o";
360
361 let pub_bytes = Base64UrlUnpadded::decode_vec(PUB).unwrap();
362 assert_eq!(jwk_thumbprint("ML-DSA-44", &pub_bytes), KID);
363 }
364
365 #[test]
372 fn jwk_kid_matches_describe() {
373 let sk = SigningKey::generate().unwrap();
374 let vk = sk.verifying_key();
375
376 let jwk_kid = sk.jwk()["kid"].as_str().unwrap().to_string();
377 let describe_kid = vk.describe().rsplit_once('#').unwrap().1.to_string();
379
380 assert_eq!(jwk_kid, describe_kid);
381 }
382
383 #[test]
384 fn sign_verify_and_tamper() {
385 let sk = SigningKey::generate().unwrap();
386 let vk = sk.verifying_key();
387 let message = b"the message to be signed";
388
389 let signature = sk.sign(message).unwrap();
390 assert!(vk.is_valid_signature(message, signature.clone()));
391 assert!(!vk.is_valid_signature(b"other message", signature.clone()));
393
394 let mut ml_tampered = signature.clone();
396 ml_tampered[0] ^= 1;
397 assert!(!vk.is_valid_signature(message, ml_tampered));
398
399 let mut ed_tampered = signature;
401 *ed_tampered.last_mut().unwrap() ^= 1;
402 assert!(!vk.is_valid_signature(message, ed_tampered));
403 }
404
405 #[test]
406 fn malformed_signature_rejected() {
407 let sk = SigningKey::generate().unwrap();
408 let vk = sk.verifying_key();
409 let message = b"msg";
410 let valid = sk.sign(message).unwrap();
411
412 assert!(!vk.is_valid_signature(message, vec![]));
414 assert!(!vk.is_valid_signature(message, valid[..valid.len() - 1].to_vec()));
415 let mut too_long = valid.clone();
416 too_long.push(0);
417 assert!(!vk.is_valid_signature(message, too_long));
418 }
419
420 #[test]
421 fn jwt_roundtrip() {
422 let sk = SigningKey::generate().unwrap();
423 let vk = sk.verifying_key();
424
425 let token = Claims::new()
426 .claim("foo", "bar")
427 .unwrap()
428 .sign(&sk)
429 .unwrap();
430 let mut claims = token.open(vk).unwrap();
431 assert_eq!(
432 claims.extract::<String>("foo").unwrap(),
433 Some("bar".to_string())
434 );
435
436 let hs_token = Claims::new().sign(&jwt::HS256(vec![0u8; 32])).unwrap();
438 assert!(matches!(
439 hs_token.open(vk),
440 Err(jwt::Error::UnexpectedAlgorithm { .. })
441 ));
442 }
443
444 #[test]
445 fn signed_roundtrip() {
446 #[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)]
447 struct TestMsg {
448 hello: String,
449 }
450 crate::api::having_message_code! { TestMsg, Example }
451
452 let sk = SigningKey::generate().unwrap();
453 let vk = sk.verifying_key();
454 let message = TestMsg {
455 hello: "world".to_string(),
456 };
457
458 let signed =
459 crate::api::Signed::<TestMsg>::new(&sk, &message, std::time::Duration::from_secs(60))
460 .unwrap();
461 assert_eq!(signed.open(vk, None).unwrap(), message);
462 }
463
464 #[test]
465 fn encode_decode_roundtrip() {
466 let sk = SigningKey::generate().unwrap();
467 let vk = sk.verifying_key();
468
469 let skb: SigningKeyBytes =
471 serde_json::from_str(&serde_json::to_string(&sk.encode()).unwrap()).unwrap();
472 let sk2 = skb.decode().unwrap();
473 assert_eq!(sk2.verifying_key(), vk);
474
475 let vkb: VerifyingKeyBytes =
477 serde_json::from_str(&serde_json::to_string(&vk.encode()).unwrap()).unwrap();
478 assert_eq!(vkb, vk.encode());
479 let vk2 = vkb.decode().unwrap();
480 assert_eq!(&vk2, vk);
481 }
482
483 #[test]
491 #[ignore = "regenerates the Python hub fixture; see the doc comment"]
492 fn emit_bespoke_test_vector() {
493 let sk = SigningKey::from_parts([0x11u8; 32], [0x22u8; 32]).unwrap();
494 let vk = sk.verifying_key();
495
496 let jws: String = Claims::new()
497 .claim("msg", "bespoke composite test vector")
498 .unwrap()
499 .sign(&sk)
500 .unwrap()
501 .into();
502
503 let vector = serde_json::json!({
504 "alg": ALG,
505 "verifying_key": serde_json::to_value(vk.encode()).unwrap(),
506 "jws": jws,
507 });
508 println!(
509 "BESPOKE_VECTOR_BEGIN\n{}\nBESPOKE_VECTOR_END",
510 serde_json::to_string_pretty(&vector).unwrap()
511 );
512 }
513}