1#![allow(clippy::disallowed_methods)]
12
13use zeroize::{Zeroize, ZeroizeOnDrop};
14
15use crate::provider::{CryptoError, CurveType, SecureSeed};
16
17#[derive(Clone, Zeroize, ZeroizeOnDrop)]
22pub enum TypedSeed {
23 Ed25519(#[zeroize] [u8; 32]),
25 P256(#[zeroize] [u8; 32]),
27}
28
29impl TypedSeed {
30 pub fn curve(&self) -> CurveType {
32 match self {
33 Self::Ed25519(_) => CurveType::Ed25519,
34 Self::P256(_) => CurveType::P256,
35 }
36 }
37
38 pub fn as_bytes(&self) -> &[u8; 32] {
40 match self {
41 Self::Ed25519(b) | Self::P256(b) => b,
42 }
43 }
44
45 pub fn to_secure_seed(&self) -> SecureSeed {
47 SecureSeed::new(*self.as_bytes())
48 }
49}
50
51impl std::fmt::Debug for TypedSeed {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 Self::Ed25519(_) => f.write_str("TypedSeed::Ed25519([REDACTED])"),
55 Self::P256(_) => f.write_str("TypedSeed::P256([REDACTED])"),
56 }
57 }
58}
59
60#[derive(Debug)]
62pub struct ParsedKey {
63 pub seed: TypedSeed,
65 pub public_key: Vec<u8>,
67}
68
69pub fn parse_key_material(bytes: &[u8]) -> Result<ParsedKey, CryptoError> {
80 if let Ok((seed, maybe_pk)) = crate::key_material::parse_ed25519_key_material(bytes) {
82 let public_key = match maybe_pk {
83 Some(pk) => pk.to_vec(),
84 None => {
85 #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
87 {
88 use ring::signature::{Ed25519KeyPair, KeyPair};
89 let kp = Ed25519KeyPair::from_seed_unchecked(seed.as_bytes()).map_err(|e| {
90 CryptoError::OperationFailed(format!("Ed25519 pubkey: {e}"))
91 })?;
92 kp.public_key().as_ref().to_vec()
93 }
94 #[cfg(not(all(feature = "native", not(target_arch = "wasm32"))))]
95 {
96 return Err(CryptoError::UnsupportedTarget);
97 }
98 }
99 };
100 return Ok(ParsedKey {
101 seed: TypedSeed::Ed25519(*seed.as_bytes()),
102 public_key,
103 });
104 }
105
106 #[cfg(feature = "native")]
108 {
109 use p256::pkcs8::DecodePrivateKey;
110 if let Ok(sk) = p256::ecdsa::SigningKey::from_pkcs8_der(bytes) {
111 let vk = p256::ecdsa::VerifyingKey::from(&sk);
112 let compressed = vk.to_encoded_point(true);
113 let mut scalar = [0u8; 32];
114 scalar.copy_from_slice(&sk.to_bytes());
115 return Ok(ParsedKey {
116 seed: TypedSeed::P256(scalar),
117 public_key: compressed.as_bytes().to_vec(),
118 });
119 }
120 }
121
122 Err(CryptoError::InvalidPrivateKey(format!(
123 "Unrecognized key format ({} bytes)",
124 bytes.len()
125 )))
126}
127
128#[cfg(all(feature = "fips", not(target_arch = "wasm32")))]
134use crate::aws_lc_provider::AwsLcProvider as SyncProvider;
135#[cfg(all(feature = "cnsa", not(feature = "fips"), not(target_arch = "wasm32")))]
136use crate::cnsa_provider::CnsaProvider as SyncProvider;
137#[cfg(all(
138 feature = "native",
139 not(feature = "fips"),
140 not(feature = "cnsa"),
141 not(target_arch = "wasm32")
142))]
143use crate::ring_provider::RingCryptoProvider as SyncProvider;
144
145#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
158pub fn sign(seed: &TypedSeed, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
159 match seed {
160 TypedSeed::Ed25519(s) => SyncProvider::ed25519_sign(s, message),
161 TypedSeed::P256(s) => SyncProvider::p256_sign(s, message),
162 }
163}
164
165#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
171pub fn public_key(seed: &TypedSeed) -> Result<Vec<u8>, CryptoError> {
172 match seed {
173 TypedSeed::Ed25519(s) => Ok(SyncProvider::ed25519_public_key(s)?.to_vec()),
174 TypedSeed::P256(s) => SyncProvider::p256_public_key_from_seed(s),
175 }
176}
177
178#[derive(Debug)]
200pub struct TypedSignerKey {
201 seed: TypedSeed,
205 public_key: Vec<u8>,
208}
209
210impl zeroize::ZeroizeOnDrop for TypedSignerKey {}
215
216impl TypedSignerKey {
217 pub fn from_pkcs8(bytes: &[u8]) -> Result<Self, CryptoError> {
219 let parsed = parse_key_material(bytes)?;
220 Ok(Self {
221 seed: parsed.seed,
222 public_key: parsed.public_key,
223 })
224 }
225
226 pub fn from_parts(seed: TypedSeed, public_key: Vec<u8>) -> Result<Self, CryptoError> {
230 let expected = seed.curve().public_key_len();
231 if public_key.len() != expected {
232 return Err(CryptoError::InvalidPrivateKey(format!(
233 "public key length {} does not match {} expected {} bytes",
234 public_key.len(),
235 seed.curve(),
236 expected
237 )));
238 }
239 Ok(Self { seed, public_key })
240 }
241
242 #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
244 pub fn from_seed(seed: TypedSeed) -> Result<Self, CryptoError> {
245 let pk = public_key(&seed)?;
246 Ok(Self {
247 seed,
248 public_key: pk,
249 })
250 }
251
252 pub fn cesr_encoded_pubkey(&self) -> String {
263 use cesride::Matter;
264 let code = match self.seed.curve() {
265 CurveType::Ed25519 => cesride::matter::Codex::Ed25519,
266 CurveType::P256 => cesride::matter::Codex::ECDSA_256r1,
267 };
268 #[allow(clippy::expect_used)]
269 cesride::Verfer::new(Some(code), Some(&self.public_key), None, None, None)
271 .and_then(|v| v.qb64())
272 .expect("cesride verkey encode is infallible for a validated key")
273 }
274
275 pub fn cesr_encoded(&self) -> String {
277 self.cesr_encoded_pubkey()
278 }
279
280 #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
284 pub fn to_pkcs8(&self) -> Result<crate::pkcs8::Pkcs8Der, CryptoError> {
285 match &self.seed {
286 TypedSeed::Ed25519(seed_bytes) => {
287 if self.public_key.len() != crate::provider::ED25519_PUBLIC_KEY_LEN {
288 return Err(CryptoError::InvalidPrivateKey(
289 "Ed25519 public key must be 32 bytes".to_string(),
290 ));
291 }
292 let mut pk = [0u8; 32];
293 pk.copy_from_slice(&self.public_key);
294 let bytes = crate::key_material::build_ed25519_pkcs8_v2(seed_bytes, &pk);
295 Ok(crate::pkcs8::Pkcs8Der::new(bytes))
296 }
297 TypedSeed::P256(scalar) => {
298 use p256::ecdsa::SigningKey;
299 use p256::pkcs8::EncodePrivateKey;
300 let sk = SigningKey::from_slice(scalar)
301 .map_err(|e| CryptoError::InvalidPrivateKey(format!("P-256 scalar: {e}")))?;
302 let doc = sk
303 .to_pkcs8_der()
304 .map_err(|e| CryptoError::OperationFailed(format!("P-256 PKCS8: {e}")))?;
305 Ok(crate::pkcs8::Pkcs8Der::new(doc.as_bytes().to_vec()))
306 }
307 }
308 }
309
310 #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
312 pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
313 sign(&self.seed, message)
314 }
315
316 pub fn curve(&self) -> CurveType {
318 self.seed.curve()
319 }
320
321 pub fn public_key(&self) -> &[u8] {
323 &self.public_key
324 }
325
326 pub fn seed(&self) -> &TypedSeed {
330 &self.seed
331 }
332}
333
334pub fn normalize_verkey(bytes: &[u8], curve: CurveType) -> Result<Vec<u8>, CryptoError> {
351 match curve {
352 CurveType::Ed25519 => {
353 if bytes.len() != 32 {
354 return Err(CryptoError::OperationFailed(format!(
355 "Ed25519 verkey must be 32 bytes, got {}",
356 bytes.len()
357 )));
358 }
359 Ok(bytes.to_vec())
360 }
361 CurveType::P256 => {
362 #[cfg(feature = "native")]
363 {
364 use p256::elliptic_curve::sec1::ToEncodedPoint;
365 let pk = p256::PublicKey::from_sec1_bytes(bytes).map_err(|e| {
366 CryptoError::OperationFailed(format!("invalid P-256 public key: {e}"))
367 })?;
368 Ok(pk.to_encoded_point(true).as_bytes().to_vec())
369 }
370 #[cfg(not(feature = "native"))]
371 {
372 let _ = bytes;
373 Err(CryptoError::UnsupportedTarget)
374 }
375 }
376 #[allow(unreachable_patterns)]
377 other => Err(CryptoError::OperationFailed(format!(
378 "normalize_verkey: unsupported curve {other:?}"
379 ))),
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn typed_seed_curve_identification() {
389 let ed = TypedSeed::Ed25519([1u8; 32]);
390 assert_eq!(ed.curve(), CurveType::Ed25519);
391
392 let p = TypedSeed::P256([2u8; 32]);
393 assert_eq!(p.curve(), CurveType::P256);
394 }
395
396 #[test]
397 fn typed_seed_as_bytes() {
398 let seed = TypedSeed::Ed25519([42u8; 32]);
399 assert_eq!(seed.as_bytes(), &[42u8; 32]);
400 }
401
402 #[test]
403 fn typed_seed_debug_redacts() {
404 let seed = TypedSeed::P256([0u8; 32]);
405 let debug = format!("{:?}", seed);
406 assert!(debug.contains("REDACTED"));
407 assert!(!debug.contains("0, 0, 0"));
408 }
409
410 #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
411 mod native {
412 use super::*;
413
414 #[test]
415 fn parse_ed25519_pkcs8_v2() {
416 use ring::rand::SystemRandom;
418 use ring::signature::Ed25519KeyPair;
419 let rng = SystemRandom::new();
420 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
421 let parsed = parse_key_material(pkcs8.as_ref()).unwrap();
422 assert_eq!(parsed.seed.curve(), CurveType::Ed25519);
423 assert_eq!(parsed.public_key.len(), 32);
424 }
425
426 #[test]
427 fn parse_p256_pkcs8() {
428 use p256::ecdsa::SigningKey;
429 use p256::elliptic_curve::rand_core::OsRng;
430 use p256::pkcs8::EncodePrivateKey;
431 let sk = SigningKey::random(&mut OsRng);
432 let pkcs8 = sk.to_pkcs8_der().unwrap();
433 let parsed = parse_key_material(pkcs8.as_bytes()).unwrap();
434 assert_eq!(parsed.seed.curve(), CurveType::P256);
435 assert_eq!(parsed.public_key.len(), 33);
436 }
437
438 #[test]
439 fn parse_raw_32_bytes_is_ed25519() {
440 let raw = [7u8; 32];
441 let parsed = parse_key_material(&raw).unwrap();
442 assert_eq!(parsed.seed.curve(), CurveType::Ed25519);
443 }
444
445 #[test]
446 fn parse_garbage_fails() {
447 let garbage = [0xFFu8; 50];
448 assert!(parse_key_material(&garbage).is_err());
449 }
450
451 #[test]
452 fn parse_empty_fails() {
453 assert!(parse_key_material(&[]).is_err());
454 }
455
456 #[test]
457 fn sign_ed25519_roundtrip() {
458 use ring::signature::{ED25519, UnparsedPublicKey};
459 let seed = TypedSeed::Ed25519([1u8; 32]);
460 let msg = b"hello world";
461 let sig = sign(&seed, msg).unwrap();
462 assert_eq!(sig.len(), 64);
463
464 let pk = public_key(&seed).unwrap();
465 let verifier = UnparsedPublicKey::new(&ED25519, &pk);
466 assert!(verifier.verify(msg, &sig).is_ok());
467 }
468
469 #[test]
470 fn sign_p256_roundtrip() {
471 use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
472 let seed = TypedSeed::P256([3u8; 32]);
473 let msg = b"hello p256";
474 let sig_bytes = sign(&seed, msg).unwrap();
475 assert_eq!(sig_bytes.len(), 64);
476
477 let pk_bytes = public_key(&seed).unwrap();
478 assert_eq!(pk_bytes.len(), 33);
479
480 let vk = VerifyingKey::from_sec1_bytes(&pk_bytes).unwrap();
481 let sig = Signature::from_slice(&sig_bytes).unwrap();
482 assert!(vk.verify(msg, &sig).is_ok());
483 }
484
485 #[test]
486 fn cross_curve_isolation() {
487 let bytes = [5u8; 32];
489 let ed_seed = TypedSeed::Ed25519(bytes);
490 let p256_seed = TypedSeed::P256(bytes);
491
492 let ed_pk = public_key(&ed_seed).unwrap();
493 let p256_pk = public_key(&p256_seed).unwrap();
494
495 assert_ne!(ed_pk.len(), p256_pk.len());
497
498 let msg = b"test";
499 let ed_sig = sign(&ed_seed, msg).unwrap();
500 let p256_sig = sign(&p256_seed, msg).unwrap();
501
502 assert_eq!(ed_sig.len(), 64);
504 assert_eq!(p256_sig.len(), 64);
505 assert_ne!(ed_sig, p256_sig);
506 }
507
508 #[test]
509 fn parse_then_sign_ed25519() {
510 use ring::rand::SystemRandom;
511 use ring::signature::{ED25519, Ed25519KeyPair, UnparsedPublicKey};
512 let rng = SystemRandom::new();
513 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
514 let parsed = parse_key_material(pkcs8.as_ref()).unwrap();
515
516 let msg = b"end to end";
517 let sig = sign(&parsed.seed, msg).unwrap();
518 let verifier = UnparsedPublicKey::new(&ED25519, &parsed.public_key);
519 assert!(verifier.verify(msg, &sig).is_ok());
520 }
521
522 #[test]
523 fn parse_then_sign_p256() {
524 use p256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::Verifier};
525 use p256::elliptic_curve::rand_core::OsRng;
526 use p256::pkcs8::EncodePrivateKey;
527
528 let sk = SigningKey::random(&mut OsRng);
529 let pkcs8 = sk.to_pkcs8_der().unwrap();
530 let parsed = parse_key_material(pkcs8.as_bytes()).unwrap();
531
532 let msg = b"end to end p256";
533 let sig_bytes = sign(&parsed.seed, msg).unwrap();
534
535 let vk = VerifyingKey::from_sec1_bytes(&parsed.public_key).unwrap();
536 let sig = Signature::from_slice(&sig_bytes).unwrap();
537 assert!(vk.verify(msg, &sig).is_ok());
538 }
539
540 #[test]
541 fn typed_signer_key_ed25519_roundtrip() {
542 use ring::rand::SystemRandom;
543 use ring::signature::Ed25519KeyPair;
544 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
545 let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
546 assert_eq!(s.curve(), CurveType::Ed25519);
547 assert!(s.cesr_encoded_pubkey().starts_with('D'));
548 assert_eq!(s.public_key().len(), 32);
549 let sig = s.sign(b"msg").unwrap();
550 assert_eq!(sig.len(), 64);
551 }
552
553 #[test]
554 fn typed_signer_key_p256_roundtrip() {
555 use p256::ecdsa::SigningKey;
556 use p256::elliptic_curve::rand_core::OsRng;
557 use p256::pkcs8::EncodePrivateKey;
558 let sk = SigningKey::random(&mut OsRng);
559 let pkcs8 = sk.to_pkcs8_der().unwrap();
560 let s = TypedSignerKey::from_pkcs8(pkcs8.as_bytes()).unwrap();
561 assert_eq!(s.curve(), CurveType::P256);
562 assert!(s.cesr_encoded_pubkey().starts_with("1AAJ"));
563 assert_eq!(s.public_key().len(), 33);
564 let sig = s.sign(b"msg").unwrap();
565 assert_eq!(sig.len(), 64);
566 }
567
568 #[test]
569 fn typed_signer_key_to_pkcs8_ed25519_roundtrip() {
570 use ring::rand::SystemRandom;
571 use ring::signature::Ed25519KeyPair;
572 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
573 let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
574 let encoded = s.to_pkcs8().unwrap();
575 let reparsed = TypedSignerKey::from_pkcs8(encoded.as_ref()).unwrap();
576 assert_eq!(reparsed.curve(), CurveType::Ed25519);
577 assert_eq!(reparsed.public_key(), s.public_key());
578 assert_eq!(reparsed.seed.as_bytes(), s.seed.as_bytes());
579 }
580
581 #[test]
582 fn typed_signer_key_to_pkcs8_p256_roundtrip() {
583 let seed = TypedSeed::P256({
584 let mut scalar = [9u8; 32];
585 scalar[0] |= 1;
586 scalar
587 });
588 let s = TypedSignerKey::from_seed(seed).unwrap();
589 let encoded = s.to_pkcs8().unwrap();
590 let reparsed = TypedSignerKey::from_pkcs8(encoded.as_ref()).unwrap();
591 assert_eq!(reparsed.curve(), CurveType::P256);
592 assert_eq!(reparsed.public_key(), s.public_key());
593 assert_eq!(reparsed.seed.as_bytes(), s.seed.as_bytes());
594 }
595
596 #[test]
597 fn rotation_signer_alias_still_works() {
598 use ring::rand::SystemRandom;
599 use ring::signature::Ed25519KeyPair;
600 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
601 let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
603 assert_eq!(s.curve(), CurveType::Ed25519);
604 }
605
606 #[test]
607 fn typed_signer_key_from_parts_rejects_mismatched_pubkey_length() {
608 let seed = TypedSeed::Ed25519([1u8; 32]);
609 let wrong_len_pk = vec![0u8; 33]; let err = TypedSignerKey::from_parts(seed, wrong_len_pk).unwrap_err();
611 assert!(matches!(err, CryptoError::InvalidPrivateKey(_)));
612 }
613
614 #[test]
623 #[cfg(not(feature = "fips"))]
624 fn sign_p256_is_rfc6979_deterministic() {
625 let seed = TypedSeed::P256([7u8; 32]);
626 let msg = b"fn-128.T2 determinism";
627 let a = sign(&seed, msg).unwrap();
628 let b = sign(&seed, msg).unwrap();
629 let c = sign(&seed, msg).unwrap();
630 assert_eq!(a, b);
631 assert_eq!(b, c);
632 assert_eq!(a.len(), 64);
633 }
634
635 #[test]
639 fn sign_ed25519_is_deterministic() {
640 let seed = TypedSeed::Ed25519([11u8; 32]);
641 let msg = b"fn-128.T2 determinism";
642 let a = sign(&seed, msg).unwrap();
643 let b = sign(&seed, msg).unwrap();
644 assert_eq!(a, b);
645 assert_eq!(a.len(), 64);
646 }
647
648 #[tokio::test]
658 #[cfg(not(feature = "fips"))]
659 async fn sync_sign_matches_async_sign_typed_p256() {
660 use crate::provider::CryptoProvider;
661 use crate::ring_provider::RingCryptoProvider;
662
663 let seed = TypedSeed::P256([42u8; 32]);
664 let msg = b"parity check";
665
666 let sync_sig = sign(&seed, msg).unwrap();
667 let async_sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
668
669 assert_eq!(sync_sig, async_sig);
670 }
671
672 #[tokio::test]
673 async fn sync_sign_matches_async_sign_typed_ed25519() {
674 use crate::provider::CryptoProvider;
675 use crate::ring_provider::RingCryptoProvider;
676
677 let seed = TypedSeed::Ed25519([19u8; 32]);
678 let msg = b"parity check";
679
680 let sync_sig = sign(&seed, msg).unwrap();
681 let async_sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
682
683 assert_eq!(sync_sig, async_sig);
684 }
685
686 #[tokio::test]
690 async fn sync_public_key_matches_async_typed_public_key() {
691 use crate::provider::CryptoProvider;
692 use crate::ring_provider::RingCryptoProvider;
693
694 for (name, seed) in [
695 ("ed25519", TypedSeed::Ed25519([23u8; 32])),
696 ("p256", TypedSeed::P256([29u8; 32])),
697 ] {
698 let sync_pk = public_key(&seed).unwrap();
699 let async_pk = RingCryptoProvider
700 .typed_public_key_from_seed(&seed)
701 .await
702 .unwrap();
703 assert_eq!(sync_pk, async_pk, "pub key drift on {name}");
704 }
705 }
706
707 #[tokio::test]
711 async fn sign_typed_and_verify_typed_round_trip() {
712 use crate::provider::CryptoProvider;
713 use crate::ring_provider::RingCryptoProvider;
714
715 for seed in [TypedSeed::Ed25519([31u8; 32]), TypedSeed::P256([37u8; 32])] {
716 let msg = b"curve-agnostic round trip";
717 let pk = RingCryptoProvider
718 .typed_public_key_from_seed(&seed)
719 .await
720 .unwrap();
721 let sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
722 RingCryptoProvider
723 .verify_typed(seed.curve(), &pk, msg, &sig)
724 .await
725 .expect("verify_typed should accept matching signature");
726 }
727 }
728 }
729}