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    /// Returns the curve this seed belongs to.
31    pub fn curve(&self) -> CurveType {
32        match self {
33            Self::Ed25519(_) => CurveType::Ed25519,
34            Self::P256(_) => CurveType::P256,
35        }
36    }
37
38    /// Returns the raw seed bytes (32 bytes for both Ed25519 and P-256).
39    pub fn as_bytes(&self) -> &[u8; 32] {
40        match self {
41            Self::Ed25519(b) | Self::P256(b) => b,
42        }
43    }
44
45    /// Convert to a legacy `SecureSeed` (loses curve info — use sparingly).
46    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/// Parsed key material with curve baked in.
61#[derive(Debug)]
62pub struct ParsedKey {
63    /// The private key seed, typed to its curve.
64    pub seed: TypedSeed,
65    /// The public key bytes (32 for Ed25519, 33 for P-256 compressed).
66    pub public_key: Vec<u8>,
67}
68
69/// Parse any supported PKCS8 DER (or raw seed) to extract seed + public key + curve.
70///
71/// This is the single source of truth for "what curve is this key?"
72/// The curve is detected here and carried in `TypedSeed` — never re-guessed.
73///
74/// Usage:
75/// ```ignore
76/// let parsed = parse_key_material(&pkcs8_bytes)?;
77/// let sig = sign(&parsed.seed, message)?;
78/// ```
79pub fn parse_key_material(bytes: &[u8]) -> Result<ParsedKey, CryptoError> {
80    // Try Ed25519 first (most common, multiple PKCS8 formats)
81    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                // Derive from seed via ring
86                #[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    // Try P-256 PKCS8
107    #[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// Compile-time provider selection for the sync dispatchers below:
129// - `--features fips` → `AwsLcProvider` (AWS-LC-FIPS 140-3)
130// - `--features cnsa` → `CnsaProvider` (rejects P-256; forwards Ed25519/P-384)
131// - default → `RingCryptoProvider`
132// One cfg block, one import, everything downstream is provider-agnostic.
133#[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/// Sign a message using the seed's curve. No curve parameter needed.
146///
147/// This is the sync curve-agnostic dispatcher. Each arm delegates to a
148/// `SyncProvider` inherent method — the single swap point when FIPS
149/// (fn-128.T3) or CNSA (fn-128.T4) replaces the default provider. Domain code
150/// never matches on curve; this function does it once.
151///
152/// Usage:
153/// ```ignore
154/// let parsed = parse_key_material(&pkcs8)?;
155/// let sig = sign(&parsed.seed, b"hello")?;
156/// ```
157#[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/// Derive the public key from the seed's curve.
166///
167/// Returns 32 bytes for Ed25519, 33 bytes compressed SEC1 for P-256.
168/// Same dispatcher shape as [`sign`] — one `match`, each arm a cfg-swappable
169/// provider inherent method.
170#[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/// Parsed signing key with curve carried explicitly — the authoritative owner
179/// of a private-key + curve pair across sign / verify / PKCS8 export / CESR
180/// encoding flows.
181///
182/// `TypedSignerKey` replaces every `(SecureSeed, CurveType)` pair, every
183/// `[u8; 32]` seed passed alongside an implicit "assume Ed25519", and every
184/// replaces the historic `(SecureSeed, CurveType)` pair pattern throughout the workspace.
185///
186/// Constructed from PKCS8 DER bytes via [`TypedSignerKey::from_pkcs8`], which
187/// delegates to [`parse_key_material`] for curve detection.
188///
189/// Args on construction:
190/// * `bytes`: PKCS8 DER (Ed25519 v1/v2 or P-256).
191///
192/// Usage:
193/// ```ignore
194/// let s = TypedSignerKey::from_pkcs8(&pkcs8)?;
195/// let sig = s.sign(b"payload bytes")?;
196/// let cesr = s.cesr_encoded_pubkey(); // "D..." for Ed25519, "1AAJ..." for P-256 (transferable verkey)
197/// let pkcs8 = s.to_pkcs8()?;          // curve-aware encode (replaces build_ed25519_pkcs8_v2)
198/// ```
199#[derive(Debug)]
200pub struct TypedSignerKey {
201    /// The private seed, tagged with its curve. Private — access via [`TypedSignerKey::curve`]
202    /// or the typed sign/to_pkcs8 methods. Prevents callers from grabbing raw bytes and
203    /// re-introducing curve-less dispatch.
204    seed: TypedSeed,
205    /// The public key bytes (32 Ed25519, 33 P-256 compressed). Private — access via
206    /// [`TypedSignerKey::public_key`].
207    public_key: Vec<u8>,
208}
209
210// Marker impl — the inner `TypedSeed` is itself `ZeroizeOnDrop`, so dropping
211// a `TypedSignerKey` transitively zeroes the private key. The public key
212// field intentionally does not need to be scrubbed. This empty impl formally
213// declares the invariant so fn-128.T5's `Secret` trait bound is satisfiable.
214impl zeroize::ZeroizeOnDrop for TypedSignerKey {}
215
216impl TypedSignerKey {
217    /// Parse a PKCS8 DER blob into a curve-tagged signer.
218    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    /// Construct directly from a typed seed and its derived public key.
227    /// Caller must ensure the public key matches the seed's curve; if the
228    /// lengths disagree with the curve, returns `InvalidPrivateKey`.
229    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    /// Derive from a typed seed by recomputing the public key.
243    #[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    /// CESR-encoded public key string.
253    ///
254    /// Uses the spec-correct derivation codes:
255    /// - `D` + base64url(32 bytes) for Ed25519 (transferable verkey)
256    /// - `1AAJ` + base64url(33 bytes compressed SEC1) for P-256 (transferable verkey)
257    ///
258    /// Per the CESR master code table, `1AAJ` (`ECDSA_256r1`) is the
259    /// transferable secp256r1 verkey code — the P-256 analogue of Ed25519 `D`.
260    /// (`1AAI` / `ECDSA_256r1N` is the non-transferable variant.) Auths
261    /// identities rotate, so signers emit the transferable code.
262    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        // INVARIANT: every constructor validates public_key length against the curve, so cesride encode under the matching fixed-size code cannot fail
270        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    /// Legacy alias; callers should prefer [`cesr_encoded_pubkey`].
276    pub fn cesr_encoded(&self) -> String {
277        self.cesr_encoded_pubkey()
278    }
279
280    /// Curve-aware PKCS8 DER encode — replaces `build_ed25519_pkcs8_v2` and
281    /// `encode_seed_as_pkcs8`. Dispatches on the seed's curve so a P-256 seed
282    /// never silently wraps as an Ed25519 PKCS8 blob (hazard S3/S4).
283    #[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    /// Sign bytes using the signer's curve.
311    #[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    /// Returns the curve this signer uses.
317    pub fn curve(&self) -> CurveType {
318        self.seed.curve()
319    }
320
321    /// Returns the public key bytes (32 for Ed25519, 33 for P-256 compressed).
322    pub fn public_key(&self) -> &[u8] {
323        &self.public_key
324    }
325
326    /// Returns a reference to the typed seed. Scoped access for signing paths that
327    /// need the `TypedSeed` directly (e.g. `auths_crypto::sign(&seed, msg)`) without
328    /// exposing the raw bytes.
329    pub fn seed(&self) -> &TypedSeed {
330        &self.seed
331    }
332}
333
334/// Normalize raw public key bytes to the canonical verkey form for `curve`.
335///
336/// Hardware backends and foreign encodings may hand back non-canonical forms
337/// (e.g. an uncompressed SEC1 point); wire formats carry exactly one canonical
338/// shape per curve — 32 raw bytes for Ed25519, the 33-byte compressed SEC1
339/// point for P-256. Rejects bytes that are not a valid key on `curve`.
340///
341/// Args:
342/// * `bytes`: Raw public key bytes in any encoding the curve accepts.
343/// * `curve`: The curve the key belongs to (never inferred from length).
344///
345/// Usage:
346/// ```ignore
347/// let verkey = normalize_verkey(&hardware_pubkey, CurveType::P256)?;
348/// assert_eq!(verkey.len(), 33);
349/// ```
350pub 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            // Generate via ring, parse back
417            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            // Same raw bytes, different curves, different outputs
488            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            // Different lengths (32 vs 33) and different values
496            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            // Both 64 bytes but different values
503            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            // Via the transitional alias
602            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]; // 33 bytes, expected 32 for Ed25519
610            let err = TypedSignerKey::from_parts(seed, wrong_len_pk).unwrap_err();
611            assert!(matches!(err, CryptoError::InvalidPrivateKey(_)));
612        }
613
614        /// Regression — ECDSA-P256 must be deterministic (RFC 6979). If this
615        /// breaks, something has routed signing through a randomized-nonce
616        /// path, which is class-breaking (Sony PS3 failure mode).
617        ///
618        /// NOTE: The FIPS provider (aws-lc-rs) adds randomized blinding to
619        /// nonce computation for side-channel resistance. Signatures are still
620        /// RFC 6979 compliant (verifiable) but not byte-deterministic. This
621        /// test is gated to the default (ring) provider only.
622        #[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        /// Regression — Ed25519 is deterministic by construction (RFC 8032).
636        /// Pairs with the P-256 test to assert the whole sign surface is
637        /// deterministic.
638        #[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        /// Regression — the sync `sign` free function and the async trait
649        /// `sign_typed` method must produce byte-identical output. Proves the
650        /// T2 refactor did not fork signing behavior across the two entry
651        /// points; a FIPS/CNSA swap that changes one must change the other
652        /// and this test will catch a drift.
653        ///
654        /// Gated to non-FIPS: under `--features fips`, the sync provider is
655        /// aws-lc-rs (randomized blinding) while this test compares against
656        /// Ring (deterministic). The two produce valid but non-identical sigs.
657        #[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        /// Regression — the sync `public_key` free function and the async
687        /// trait `typed_public_key_from_seed` method must produce byte-identical
688        /// output across curves.
689        #[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        /// Regression — the async trait `sign_typed` round-trips through
708        /// `verify_typed` with the public key derived from the same seed.
709        /// Covers the curve-agnostic path end-to-end.
710        #[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}