Skip to main content

auths_crypto/
key_ops.rs

1//! Curve-agnostic key operations.
2//!
3//! Single source of truth for parsing, signing, and public key derivation
4//! across Ed25519 and P-256. The [`TypedSeed`] enum carries the curve with
5//! the key material — callers never need to guess which curve a key uses.
6
7// INVARIANT: sanctioned crypto boundary — the only legitimate caller of ring
8// Ed25519 APIs inside the workspace. Every other crate must route through
9// auths_crypto::sign / public_key / TypedSignerKey. Permanent allow; do NOT
10// remove in fn-114.40.
11#![allow(clippy::disallowed_methods)]
12
13use zeroize::{Zeroize, ZeroizeOnDrop};
14
15use crate::provider::{CryptoError, CurveType, SecureSeed};
16
17/// A private key seed that knows its curve.
18///
19/// Adding a new curve means adding a variant here. The compiler then errors
20/// on every `match` that doesn't handle it — no grep needed.
21#[derive(Clone, Zeroize, ZeroizeOnDrop)]
22pub enum TypedSeed {
23    /// Ed25519 private key seed (32 bytes).
24    Ed25519(#[zeroize] [u8; 32]),
25    /// P-256 private scalar (32 bytes).
26    P256(#[zeroize] [u8; 32]),
27}
28
29impl TypedSeed {
30    /// Builds a typed seed from raw 32-byte material on the named curve — the
31    /// one source of truth for `(curve, bytes) -> TypedSeed`. Adding a curve
32    /// makes this `match` non-exhaustive, so every caller is forced to handle it.
33    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    /// Returns the curve this seed belongs to.
41    pub fn curve(&self) -> CurveType {
42        match self {
43            Self::Ed25519(_) => CurveType::Ed25519,
44            Self::P256(_) => CurveType::P256,
45        }
46    }
47
48    /// Returns the raw seed bytes (32 bytes for both Ed25519 and P-256).
49    pub fn as_bytes(&self) -> &[u8; 32] {
50        match self {
51            Self::Ed25519(b) | Self::P256(b) => b,
52        }
53    }
54
55    /// Convert to a legacy `SecureSeed` (loses curve info — use sparingly).
56    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/// Parsed key material with curve baked in.
71#[derive(Debug)]
72pub struct ParsedKey {
73    /// The private key seed, typed to its curve.
74    pub seed: TypedSeed,
75    /// The public key bytes (32 for Ed25519, 33 for P-256 compressed).
76    pub public_key: Vec<u8>,
77}
78
79/// Parse any supported PKCS8 DER (or raw seed) to extract seed + public key + curve.
80///
81/// This is the single source of truth for "what curve is this key?"
82/// The curve is detected here and carried in `TypedSeed` — never re-guessed.
83///
84/// Usage:
85/// ```ignore
86/// let parsed = parse_key_material(&pkcs8_bytes)?;
87/// let sig = sign(&parsed.seed, message)?;
88/// ```
89pub fn parse_key_material(bytes: &[u8]) -> Result<ParsedKey, CryptoError> {
90    // Try Ed25519 first (most common, multiple PKCS8 formats)
91    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                // Derive from seed via ring
96                #[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    // Try P-256 PKCS8
117    #[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// Compile-time provider selection for the sync dispatchers below:
139// - `--features fips` → `AwsLcProvider` (AWS-LC-FIPS 140-3)
140// - `--features cnsa` → `CnsaProvider` (rejects P-256; forwards Ed25519/P-384)
141// - default → `RingCryptoProvider`
142// One cfg block, one import, everything downstream is provider-agnostic.
143#[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/// Sign a message using the seed's curve. No curve parameter needed.
156///
157/// This is the sync curve-agnostic dispatcher. Each arm delegates to a
158/// `SyncProvider` inherent method — the single swap point when FIPS
159/// (fn-128.T3) or CNSA (fn-128.T4) replaces the default provider. Domain code
160/// never matches on curve; this function does it once.
161///
162/// Usage:
163/// ```ignore
164/// let parsed = parse_key_material(&pkcs8)?;
165/// let sig = sign(&parsed.seed, b"hello")?;
166/// ```
167#[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/// Derive the public key from the seed's curve.
176///
177/// Returns 32 bytes for Ed25519, 33 bytes compressed SEC1 for P-256.
178/// Same dispatcher shape as [`sign`] — one `match`, each arm a cfg-swappable
179/// provider inherent method.
180#[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/// Parsed signing key with curve carried explicitly — the authoritative owner
189/// of a private-key + curve pair across sign / verify / PKCS8 export / CESR
190/// encoding flows.
191///
192/// `TypedSignerKey` replaces every `(SecureSeed, CurveType)` pair, every
193/// `[u8; 32]` seed passed alongside an implicit "assume Ed25519", and every
194/// replaces the historic `(SecureSeed, CurveType)` pair pattern throughout the workspace.
195///
196/// Constructed from PKCS8 DER bytes via [`TypedSignerKey::from_pkcs8`], which
197/// delegates to [`parse_key_material`] for curve detection.
198///
199/// Args on construction:
200/// * `bytes`: PKCS8 DER (Ed25519 v1/v2 or P-256).
201///
202/// Usage:
203/// ```ignore
204/// let s = TypedSignerKey::from_pkcs8(&pkcs8)?;
205/// let sig = s.sign(b"payload bytes")?;
206/// let cesr = s.cesr_encoded_pubkey(); // "D..." for Ed25519, "1AAJ..." for P-256 (transferable verkey)
207/// let pkcs8 = s.to_pkcs8()?;          // curve-aware encode (replaces build_ed25519_pkcs8_v2)
208/// ```
209#[derive(Debug)]
210pub struct TypedSignerKey {
211    /// The private seed, tagged with its curve. Private — access via [`TypedSignerKey::curve`]
212    /// or the typed sign/to_pkcs8 methods. Prevents callers from grabbing raw bytes and
213    /// re-introducing curve-less dispatch.
214    seed: TypedSeed,
215    /// The public key bytes (32 Ed25519, 33 P-256 compressed). Private — access via
216    /// [`TypedSignerKey::public_key`].
217    public_key: Vec<u8>,
218}
219
220// Marker impl — the inner `TypedSeed` is itself `ZeroizeOnDrop`, so dropping
221// a `TypedSignerKey` transitively zeroes the private key. The public key
222// field intentionally does not need to be scrubbed. This empty impl formally
223// declares the invariant so fn-128.T5's `Secret` trait bound is satisfiable.
224impl zeroize::ZeroizeOnDrop for TypedSignerKey {}
225
226impl TypedSignerKey {
227    /// Parse a PKCS8 DER blob into a curve-tagged signer.
228    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    /// Construct directly from a typed seed and its derived public key.
237    /// Caller must ensure the public key matches the seed's curve; if the
238    /// lengths disagree with the curve, returns `InvalidPrivateKey`.
239    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    /// Derive from a typed seed by recomputing the public key.
253    #[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    /// CESR-encoded public key string.
263    ///
264    /// Uses the spec-correct derivation codes:
265    /// - `D` + base64url(32 bytes) for Ed25519 (transferable verkey)
266    /// - `1AAJ` + base64url(33 bytes compressed SEC1) for P-256 (transferable verkey)
267    ///
268    /// Per the CESR master code table, `1AAJ` (`ECDSA_256r1`) is the
269    /// transferable secp256r1 verkey code — the P-256 analogue of Ed25519 `D`.
270    /// (`1AAI` / `ECDSA_256r1N` is the non-transferable variant.) Auths
271    /// identities rotate, so signers emit the transferable code.
272    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        // INVARIANT: every constructor validates public_key length against the curve, so cesride encode under the matching fixed-size code cannot fail
280        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    /// Legacy alias; callers should prefer [`cesr_encoded_pubkey`].
286    pub fn cesr_encoded(&self) -> String {
287        self.cesr_encoded_pubkey()
288    }
289
290    /// Curve-aware PKCS8 DER encode — replaces `build_ed25519_pkcs8_v2` and
291    /// `encode_seed_as_pkcs8`. Dispatches on the seed's curve so a P-256 seed
292    /// never silently wraps as an Ed25519 PKCS8 blob (hazard S3/S4).
293    #[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    /// Sign bytes using the signer's curve.
321    #[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    /// Returns the curve this signer uses.
327    pub fn curve(&self) -> CurveType {
328        self.seed.curve()
329    }
330
331    /// Returns the public key bytes (32 for Ed25519, 33 for P-256 compressed).
332    pub fn public_key(&self) -> &[u8] {
333        &self.public_key
334    }
335
336    /// Returns a reference to the typed seed. Scoped access for signing paths that
337    /// need the `TypedSeed` directly (e.g. `auths_crypto::sign(&seed, msg)`) without
338    /// exposing the raw bytes.
339    pub fn seed(&self) -> &TypedSeed {
340        &self.seed
341    }
342}
343
344/// Normalize raw public key bytes to the canonical verkey form for `curve`.
345///
346/// Hardware backends and foreign encodings may hand back non-canonical forms
347/// (e.g. an uncompressed SEC1 point); wire formats carry exactly one canonical
348/// shape per curve — 32 raw bytes for Ed25519, the 33-byte compressed SEC1
349/// point for P-256. Rejects bytes that are not a valid key on `curve`.
350///
351/// Args:
352/// * `bytes`: Raw public key bytes in any encoding the curve accepts.
353/// * `curve`: The curve the key belongs to (never inferred from length).
354///
355/// Usage:
356/// ```ignore
357/// let verkey = normalize_verkey(&hardware_pubkey, CurveType::P256)?;
358/// assert_eq!(verkey.len(), 33);
359/// ```
360pub 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            // Generate via ring, parse back
427            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            // Same raw bytes, different curves, different outputs
498            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            // Different lengths (32 vs 33) and different values
506            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            // Both 64 bytes but different values
513            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            // Via the transitional alias
612            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]; // 33 bytes, expected 32 for Ed25519
620            let err = TypedSignerKey::from_parts(seed, wrong_len_pk).unwrap_err();
621            assert!(matches!(err, CryptoError::InvalidPrivateKey(_)));
622        }
623
624        /// Regression — ECDSA-P256 must be deterministic (RFC 6979). If this
625        /// breaks, something has routed signing through a randomized-nonce
626        /// path, which is class-breaking (Sony PS3 failure mode).
627        ///
628        /// NOTE: The FIPS provider (aws-lc-rs) adds randomized blinding to
629        /// nonce computation for side-channel resistance. Signatures are still
630        /// RFC 6979 compliant (verifiable) but not byte-deterministic. This
631        /// test is gated to the default (ring) provider only.
632        #[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        /// Regression — Ed25519 is deterministic by construction (RFC 8032).
646        /// Pairs with the P-256 test to assert the whole sign surface is
647        /// deterministic.
648        #[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        /// Regression — the sync `sign` free function and the async trait
659        /// `sign_typed` method must produce byte-identical output. Proves the
660        /// T2 refactor did not fork signing behavior across the two entry
661        /// points; a FIPS/CNSA swap that changes one must change the other
662        /// and this test will catch a drift.
663        ///
664        /// Gated to non-FIPS: under `--features fips`, the sync provider is
665        /// aws-lc-rs (randomized blinding) while this test compares against
666        /// Ring (deterministic). The two produce valid but non-identical sigs.
667        #[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        /// Regression — the sync `public_key` free function and the async
697        /// trait `typed_public_key_from_seed` method must produce byte-identical
698        /// output across curves.
699        #[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        /// Regression — the async trait `sign_typed` round-trips through
718        /// `verify_typed` with the public key derived from the same seed.
719        /// Covers the curve-agnostic path end-to-end.
720        #[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}