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 from_curve(curve: CurveType, bytes: [u8; 32]) -> Self {
34 match curve {
35 CurveType::Ed25519 => Self::Ed25519(bytes),
36 CurveType::P256 => Self::P256(bytes),
37 }
38 }
39
40 pub fn curve(&self) -> CurveType {
42 match self {
43 Self::Ed25519(_) => CurveType::Ed25519,
44 Self::P256(_) => CurveType::P256,
45 }
46 }
47
48 pub fn as_bytes(&self) -> &[u8; 32] {
50 match self {
51 Self::Ed25519(b) | Self::P256(b) => b,
52 }
53 }
54
55 pub fn to_secure_seed(&self) -> SecureSeed {
57 SecureSeed::new(*self.as_bytes())
58 }
59}
60
61impl std::fmt::Debug for TypedSeed {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 Self::Ed25519(_) => f.write_str("TypedSeed::Ed25519([REDACTED])"),
65 Self::P256(_) => f.write_str("TypedSeed::P256([REDACTED])"),
66 }
67 }
68}
69
70#[derive(Debug)]
72pub struct ParsedKey {
73 pub seed: TypedSeed,
75 pub public_key: Vec<u8>,
77}
78
79pub fn parse_key_material(bytes: &[u8]) -> Result<ParsedKey, CryptoError> {
90 if let Ok((seed, maybe_pk)) = crate::key_material::parse_ed25519_key_material(bytes) {
92 let public_key = match maybe_pk {
93 Some(pk) => pk.to_vec(),
94 None => {
95 #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
97 {
98 use ring::signature::{Ed25519KeyPair, KeyPair};
99 let kp = Ed25519KeyPair::from_seed_unchecked(seed.as_bytes()).map_err(|e| {
100 CryptoError::OperationFailed(format!("Ed25519 pubkey: {e}"))
101 })?;
102 kp.public_key().as_ref().to_vec()
103 }
104 #[cfg(not(all(feature = "native", not(target_arch = "wasm32"))))]
105 {
106 return Err(CryptoError::UnsupportedTarget);
107 }
108 }
109 };
110 return Ok(ParsedKey {
111 seed: TypedSeed::Ed25519(*seed.as_bytes()),
112 public_key,
113 });
114 }
115
116 #[cfg(feature = "native")]
118 {
119 use p256::pkcs8::DecodePrivateKey;
120 if let Ok(sk) = p256::ecdsa::SigningKey::from_pkcs8_der(bytes) {
121 let vk = p256::ecdsa::VerifyingKey::from(&sk);
122 let compressed = vk.to_encoded_point(true);
123 let mut scalar = [0u8; 32];
124 scalar.copy_from_slice(&sk.to_bytes());
125 return Ok(ParsedKey {
126 seed: TypedSeed::P256(scalar),
127 public_key: compressed.as_bytes().to_vec(),
128 });
129 }
130 }
131
132 Err(CryptoError::InvalidPrivateKey(format!(
133 "Unrecognized key format ({} bytes)",
134 bytes.len()
135 )))
136}
137
138#[cfg(all(feature = "fips", not(target_arch = "wasm32")))]
144use crate::aws_lc_provider::AwsLcProvider as SyncProvider;
145#[cfg(all(feature = "cnsa", not(feature = "fips"), not(target_arch = "wasm32")))]
146use crate::cnsa_provider::CnsaProvider as SyncProvider;
147#[cfg(all(
148 feature = "native",
149 not(feature = "fips"),
150 not(feature = "cnsa"),
151 not(target_arch = "wasm32")
152))]
153use crate::ring_provider::RingCryptoProvider as SyncProvider;
154
155#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
168pub fn sign(seed: &TypedSeed, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
169 match seed {
170 TypedSeed::Ed25519(s) => SyncProvider::ed25519_sign(s, message),
171 TypedSeed::P256(s) => SyncProvider::p256_sign(s, message),
172 }
173}
174
175#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
181pub fn public_key(seed: &TypedSeed) -> Result<Vec<u8>, CryptoError> {
182 match seed {
183 TypedSeed::Ed25519(s) => Ok(SyncProvider::ed25519_public_key(s)?.to_vec()),
184 TypedSeed::P256(s) => SyncProvider::p256_public_key_from_seed(s),
185 }
186}
187
188#[derive(Debug)]
210pub struct TypedSignerKey {
211 seed: TypedSeed,
215 public_key: Vec<u8>,
218}
219
220impl zeroize::ZeroizeOnDrop for TypedSignerKey {}
225
226impl TypedSignerKey {
227 pub fn from_pkcs8(bytes: &[u8]) -> Result<Self, CryptoError> {
229 let parsed = parse_key_material(bytes)?;
230 Ok(Self {
231 seed: parsed.seed,
232 public_key: parsed.public_key,
233 })
234 }
235
236 pub fn from_parts(seed: TypedSeed, public_key: Vec<u8>) -> Result<Self, CryptoError> {
240 let expected = seed.curve().public_key_len();
241 if public_key.len() != expected {
242 return Err(CryptoError::InvalidPrivateKey(format!(
243 "public key length {} does not match {} expected {} bytes",
244 public_key.len(),
245 seed.curve(),
246 expected
247 )));
248 }
249 Ok(Self { seed, public_key })
250 }
251
252 #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
254 pub fn from_seed(seed: TypedSeed) -> Result<Self, CryptoError> {
255 let pk = public_key(&seed)?;
256 Ok(Self {
257 seed,
258 public_key: pk,
259 })
260 }
261
262 pub fn cesr_encoded_pubkey(&self) -> String {
273 use cesride::Matter;
274 let code = match self.seed.curve() {
275 CurveType::Ed25519 => cesride::matter::Codex::Ed25519,
276 CurveType::P256 => cesride::matter::Codex::ECDSA_256r1,
277 };
278 #[allow(clippy::expect_used)]
279 cesride::Verfer::new(Some(code), Some(&self.public_key), None, None, None)
281 .and_then(|v| v.qb64())
282 .expect("cesride verkey encode is infallible for a validated key")
283 }
284
285 pub fn cesr_encoded(&self) -> String {
287 self.cesr_encoded_pubkey()
288 }
289
290 #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
294 pub fn to_pkcs8(&self) -> Result<crate::pkcs8::Pkcs8Der, CryptoError> {
295 match &self.seed {
296 TypedSeed::Ed25519(seed_bytes) => {
297 if self.public_key.len() != crate::provider::ED25519_PUBLIC_KEY_LEN {
298 return Err(CryptoError::InvalidPrivateKey(
299 "Ed25519 public key must be 32 bytes".to_string(),
300 ));
301 }
302 let mut pk = [0u8; 32];
303 pk.copy_from_slice(&self.public_key);
304 let bytes = crate::key_material::build_ed25519_pkcs8_v2(seed_bytes, &pk);
305 Ok(crate::pkcs8::Pkcs8Der::new(bytes.to_vec()))
306 }
307 TypedSeed::P256(scalar) => {
308 use p256::ecdsa::SigningKey;
309 use p256::pkcs8::EncodePrivateKey;
310 let sk = SigningKey::from_slice(scalar)
311 .map_err(|e| CryptoError::InvalidPrivateKey(format!("P-256 scalar: {e}")))?;
312 let doc = sk
313 .to_pkcs8_der()
314 .map_err(|e| CryptoError::OperationFailed(format!("P-256 PKCS8: {e}")))?;
315 Ok(crate::pkcs8::Pkcs8Der::new(doc.as_bytes().to_vec()))
316 }
317 }
318 }
319
320 #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
322 pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
323 sign(&self.seed, message)
324 }
325
326 pub fn curve(&self) -> CurveType {
328 self.seed.curve()
329 }
330
331 pub fn public_key(&self) -> &[u8] {
333 &self.public_key
334 }
335
336 pub fn seed(&self) -> &TypedSeed {
340 &self.seed
341 }
342}
343
344pub fn normalize_verkey(bytes: &[u8], curve: CurveType) -> Result<Vec<u8>, CryptoError> {
361 match curve {
362 CurveType::Ed25519 => {
363 if bytes.len() != 32 {
364 return Err(CryptoError::OperationFailed(format!(
365 "Ed25519 verkey must be 32 bytes, got {}",
366 bytes.len()
367 )));
368 }
369 Ok(bytes.to_vec())
370 }
371 CurveType::P256 => {
372 #[cfg(feature = "native")]
373 {
374 use p256::elliptic_curve::sec1::ToEncodedPoint;
375 let pk = p256::PublicKey::from_sec1_bytes(bytes).map_err(|e| {
376 CryptoError::OperationFailed(format!("invalid P-256 public key: {e}"))
377 })?;
378 Ok(pk.to_encoded_point(true).as_bytes().to_vec())
379 }
380 #[cfg(not(feature = "native"))]
381 {
382 let _ = bytes;
383 Err(CryptoError::UnsupportedTarget)
384 }
385 }
386 #[allow(unreachable_patterns)]
387 other => Err(CryptoError::OperationFailed(format!(
388 "normalize_verkey: unsupported curve {other:?}"
389 ))),
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn typed_seed_curve_identification() {
399 let ed = TypedSeed::Ed25519([1u8; 32]);
400 assert_eq!(ed.curve(), CurveType::Ed25519);
401
402 let p = TypedSeed::P256([2u8; 32]);
403 assert_eq!(p.curve(), CurveType::P256);
404 }
405
406 #[test]
407 fn typed_seed_as_bytes() {
408 let seed = TypedSeed::Ed25519([42u8; 32]);
409 assert_eq!(seed.as_bytes(), &[42u8; 32]);
410 }
411
412 #[test]
413 fn typed_seed_debug_redacts() {
414 let seed = TypedSeed::P256([0u8; 32]);
415 let debug = format!("{:?}", seed);
416 assert!(debug.contains("REDACTED"));
417 assert!(!debug.contains("0, 0, 0"));
418 }
419
420 #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
421 mod native {
422 use super::*;
423
424 #[test]
425 fn parse_ed25519_pkcs8_v2() {
426 use ring::rand::SystemRandom;
428 use ring::signature::Ed25519KeyPair;
429 let rng = SystemRandom::new();
430 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
431 let parsed = parse_key_material(pkcs8.as_ref()).unwrap();
432 assert_eq!(parsed.seed.curve(), CurveType::Ed25519);
433 assert_eq!(parsed.public_key.len(), 32);
434 }
435
436 #[test]
437 fn parse_p256_pkcs8() {
438 use p256::ecdsa::SigningKey;
439 use p256::elliptic_curve::rand_core::OsRng;
440 use p256::pkcs8::EncodePrivateKey;
441 let sk = SigningKey::random(&mut OsRng);
442 let pkcs8 = sk.to_pkcs8_der().unwrap();
443 let parsed = parse_key_material(pkcs8.as_bytes()).unwrap();
444 assert_eq!(parsed.seed.curve(), CurveType::P256);
445 assert_eq!(parsed.public_key.len(), 33);
446 }
447
448 #[test]
449 fn parse_raw_32_bytes_is_ed25519() {
450 let raw = [7u8; 32];
451 let parsed = parse_key_material(&raw).unwrap();
452 assert_eq!(parsed.seed.curve(), CurveType::Ed25519);
453 }
454
455 #[test]
456 fn parse_garbage_fails() {
457 let garbage = [0xFFu8; 50];
458 assert!(parse_key_material(&garbage).is_err());
459 }
460
461 #[test]
462 fn parse_empty_fails() {
463 assert!(parse_key_material(&[]).is_err());
464 }
465
466 #[test]
467 fn sign_ed25519_roundtrip() {
468 use ring::signature::{ED25519, UnparsedPublicKey};
469 let seed = TypedSeed::Ed25519([1u8; 32]);
470 let msg = b"hello world";
471 let sig = sign(&seed, msg).unwrap();
472 assert_eq!(sig.len(), 64);
473
474 let pk = public_key(&seed).unwrap();
475 let verifier = UnparsedPublicKey::new(&ED25519, &pk);
476 assert!(verifier.verify(msg, &sig).is_ok());
477 }
478
479 #[test]
480 fn sign_p256_roundtrip() {
481 use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
482 let seed = TypedSeed::P256([3u8; 32]);
483 let msg = b"hello p256";
484 let sig_bytes = sign(&seed, msg).unwrap();
485 assert_eq!(sig_bytes.len(), 64);
486
487 let pk_bytes = public_key(&seed).unwrap();
488 assert_eq!(pk_bytes.len(), 33);
489
490 let vk = VerifyingKey::from_sec1_bytes(&pk_bytes).unwrap();
491 let sig = Signature::from_slice(&sig_bytes).unwrap();
492 assert!(vk.verify(msg, &sig).is_ok());
493 }
494
495 #[test]
496 fn cross_curve_isolation() {
497 let bytes = [5u8; 32];
499 let ed_seed = TypedSeed::Ed25519(bytes);
500 let p256_seed = TypedSeed::P256(bytes);
501
502 let ed_pk = public_key(&ed_seed).unwrap();
503 let p256_pk = public_key(&p256_seed).unwrap();
504
505 assert_ne!(ed_pk.len(), p256_pk.len());
507
508 let msg = b"test";
509 let ed_sig = sign(&ed_seed, msg).unwrap();
510 let p256_sig = sign(&p256_seed, msg).unwrap();
511
512 assert_eq!(ed_sig.len(), 64);
514 assert_eq!(p256_sig.len(), 64);
515 assert_ne!(ed_sig, p256_sig);
516 }
517
518 #[test]
519 fn parse_then_sign_ed25519() {
520 use ring::rand::SystemRandom;
521 use ring::signature::{ED25519, Ed25519KeyPair, UnparsedPublicKey};
522 let rng = SystemRandom::new();
523 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
524 let parsed = parse_key_material(pkcs8.as_ref()).unwrap();
525
526 let msg = b"end to end";
527 let sig = sign(&parsed.seed, msg).unwrap();
528 let verifier = UnparsedPublicKey::new(&ED25519, &parsed.public_key);
529 assert!(verifier.verify(msg, &sig).is_ok());
530 }
531
532 #[test]
533 fn parse_then_sign_p256() {
534 use p256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::Verifier};
535 use p256::elliptic_curve::rand_core::OsRng;
536 use p256::pkcs8::EncodePrivateKey;
537
538 let sk = SigningKey::random(&mut OsRng);
539 let pkcs8 = sk.to_pkcs8_der().unwrap();
540 let parsed = parse_key_material(pkcs8.as_bytes()).unwrap();
541
542 let msg = b"end to end p256";
543 let sig_bytes = sign(&parsed.seed, msg).unwrap();
544
545 let vk = VerifyingKey::from_sec1_bytes(&parsed.public_key).unwrap();
546 let sig = Signature::from_slice(&sig_bytes).unwrap();
547 assert!(vk.verify(msg, &sig).is_ok());
548 }
549
550 #[test]
551 fn typed_signer_key_ed25519_roundtrip() {
552 use ring::rand::SystemRandom;
553 use ring::signature::Ed25519KeyPair;
554 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
555 let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
556 assert_eq!(s.curve(), CurveType::Ed25519);
557 assert!(s.cesr_encoded_pubkey().starts_with('D'));
558 assert_eq!(s.public_key().len(), 32);
559 let sig = s.sign(b"msg").unwrap();
560 assert_eq!(sig.len(), 64);
561 }
562
563 #[test]
564 fn typed_signer_key_p256_roundtrip() {
565 use p256::ecdsa::SigningKey;
566 use p256::elliptic_curve::rand_core::OsRng;
567 use p256::pkcs8::EncodePrivateKey;
568 let sk = SigningKey::random(&mut OsRng);
569 let pkcs8 = sk.to_pkcs8_der().unwrap();
570 let s = TypedSignerKey::from_pkcs8(pkcs8.as_bytes()).unwrap();
571 assert_eq!(s.curve(), CurveType::P256);
572 assert!(s.cesr_encoded_pubkey().starts_with("1AAJ"));
573 assert_eq!(s.public_key().len(), 33);
574 let sig = s.sign(b"msg").unwrap();
575 assert_eq!(sig.len(), 64);
576 }
577
578 #[test]
579 fn typed_signer_key_to_pkcs8_ed25519_roundtrip() {
580 use ring::rand::SystemRandom;
581 use ring::signature::Ed25519KeyPair;
582 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
583 let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
584 let encoded = s.to_pkcs8().unwrap();
585 let reparsed = TypedSignerKey::from_pkcs8(encoded.as_ref()).unwrap();
586 assert_eq!(reparsed.curve(), CurveType::Ed25519);
587 assert_eq!(reparsed.public_key(), s.public_key());
588 assert_eq!(reparsed.seed.as_bytes(), s.seed.as_bytes());
589 }
590
591 #[test]
592 fn typed_signer_key_to_pkcs8_p256_roundtrip() {
593 let seed = TypedSeed::P256({
594 let mut scalar = [9u8; 32];
595 scalar[0] |= 1;
596 scalar
597 });
598 let s = TypedSignerKey::from_seed(seed).unwrap();
599 let encoded = s.to_pkcs8().unwrap();
600 let reparsed = TypedSignerKey::from_pkcs8(encoded.as_ref()).unwrap();
601 assert_eq!(reparsed.curve(), CurveType::P256);
602 assert_eq!(reparsed.public_key(), s.public_key());
603 assert_eq!(reparsed.seed.as_bytes(), s.seed.as_bytes());
604 }
605
606 #[test]
607 fn rotation_signer_alias_still_works() {
608 use ring::rand::SystemRandom;
609 use ring::signature::Ed25519KeyPair;
610 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
611 let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
613 assert_eq!(s.curve(), CurveType::Ed25519);
614 }
615
616 #[test]
617 fn typed_signer_key_from_parts_rejects_mismatched_pubkey_length() {
618 let seed = TypedSeed::Ed25519([1u8; 32]);
619 let wrong_len_pk = vec![0u8; 33]; let err = TypedSignerKey::from_parts(seed, wrong_len_pk).unwrap_err();
621 assert!(matches!(err, CryptoError::InvalidPrivateKey(_)));
622 }
623
624 #[test]
633 #[cfg(not(feature = "fips"))]
634 fn sign_p256_is_rfc6979_deterministic() {
635 let seed = TypedSeed::P256([7u8; 32]);
636 let msg = b"fn-128.T2 determinism";
637 let a = sign(&seed, msg).unwrap();
638 let b = sign(&seed, msg).unwrap();
639 let c = sign(&seed, msg).unwrap();
640 assert_eq!(a, b);
641 assert_eq!(b, c);
642 assert_eq!(a.len(), 64);
643 }
644
645 #[test]
649 fn sign_ed25519_is_deterministic() {
650 let seed = TypedSeed::Ed25519([11u8; 32]);
651 let msg = b"fn-128.T2 determinism";
652 let a = sign(&seed, msg).unwrap();
653 let b = sign(&seed, msg).unwrap();
654 assert_eq!(a, b);
655 assert_eq!(a.len(), 64);
656 }
657
658 #[tokio::test]
668 #[cfg(not(feature = "fips"))]
669 async fn sync_sign_matches_async_sign_typed_p256() {
670 use crate::provider::CryptoProvider;
671 use crate::ring_provider::RingCryptoProvider;
672
673 let seed = TypedSeed::P256([42u8; 32]);
674 let msg = b"parity check";
675
676 let sync_sig = sign(&seed, msg).unwrap();
677 let async_sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
678
679 assert_eq!(sync_sig, async_sig);
680 }
681
682 #[tokio::test]
683 async fn sync_sign_matches_async_sign_typed_ed25519() {
684 use crate::provider::CryptoProvider;
685 use crate::ring_provider::RingCryptoProvider;
686
687 let seed = TypedSeed::Ed25519([19u8; 32]);
688 let msg = b"parity check";
689
690 let sync_sig = sign(&seed, msg).unwrap();
691 let async_sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
692
693 assert_eq!(sync_sig, async_sig);
694 }
695
696 #[tokio::test]
700 async fn sync_public_key_matches_async_typed_public_key() {
701 use crate::provider::CryptoProvider;
702 use crate::ring_provider::RingCryptoProvider;
703
704 for (name, seed) in [
705 ("ed25519", TypedSeed::Ed25519([23u8; 32])),
706 ("p256", TypedSeed::P256([29u8; 32])),
707 ] {
708 let sync_pk = public_key(&seed).unwrap();
709 let async_pk = RingCryptoProvider
710 .typed_public_key_from_seed(&seed)
711 .await
712 .unwrap();
713 assert_eq!(sync_pk, async_pk, "pub key drift on {name}");
714 }
715 }
716
717 #[tokio::test]
721 async fn sign_typed_and_verify_typed_round_trip() {
722 use crate::provider::CryptoProvider;
723 use crate::ring_provider::RingCryptoProvider;
724
725 for seed in [TypedSeed::Ed25519([31u8; 32]), TypedSeed::P256([37u8; 32])] {
726 let msg = b"curve-agnostic round trip";
727 let pk = RingCryptoProvider
728 .typed_public_key_from_seed(&seed)
729 .await
730 .unwrap();
731 let sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
732 RingCryptoProvider
733 .verify_typed(seed.curve(), &pk, msg, &sig)
734 .await
735 .expect("verify_typed should accept matching signature");
736 }
737 }
738 }
739}