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/// Verify a signature under the named curve — the sync twin of [`sign`], for
189/// callers outside async provider contexts. Same dispatcher shape: one `match`,
190/// each arm a cfg-swappable provider inherent method.
191///
192/// Args:
193/// * `curve`: the signature's curve, carried explicitly (never inferred from length).
194/// * `public_key`: 32 raw bytes (Ed25519) or SEC1 bytes (P-256).
195/// * `message`: the signed bytes.
196/// * `signature`: the raw signature.
197///
198/// Usage:
199/// ```ignore
200/// verify(CurveType::P256, &pubkey, msg, &sig)?;
201/// ```
202#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
203pub fn verify(
204    curve: CurveType,
205    public_key: &[u8],
206    message: &[u8],
207    signature: &[u8],
208) -> Result<(), CryptoError> {
209    match curve {
210        CurveType::Ed25519 => SyncProvider::ed25519_verify(public_key, message, signature),
211        CurveType::P256 => SyncProvider::p256_verify(public_key, message, signature),
212    }
213}
214
215/// Generate a fresh keypair on the named curve.
216///
217/// Returns the curve-tagged seed plus the public key ([`public_key`]'s
218/// encoding). Same dispatcher shape as [`sign`].
219///
220/// Args:
221/// * `curve`: the curve to generate on.
222///
223/// Usage:
224/// ```ignore
225/// let (seed, public_key) = generate(CurveType::P256)?;
226/// ```
227#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
228pub fn generate(curve: CurveType) -> Result<(TypedSeed, Vec<u8>), CryptoError> {
229    match curve {
230        CurveType::Ed25519 => {
231            let (seed, public_key) = SyncProvider::ed25519_generate()?;
232            Ok((TypedSeed::Ed25519(*seed.as_bytes()), public_key.to_vec()))
233        }
234        CurveType::P256 => {
235            let (seed, public_key) = SyncProvider::p256_generate()?;
236            Ok((TypedSeed::P256(*seed.as_bytes()), public_key))
237        }
238    }
239}
240
241/// Parsed signing key with curve carried explicitly — the authoritative owner
242/// of a private-key + curve pair across sign / verify / PKCS8 export / CESR
243/// encoding flows.
244///
245/// `TypedSignerKey` replaces every `(SecureSeed, CurveType)` pair, every
246/// `[u8; 32]` seed passed alongside an implicit "assume Ed25519", and every
247/// replaces the historic `(SecureSeed, CurveType)` pair pattern throughout the workspace.
248///
249/// Constructed from PKCS8 DER bytes via [`TypedSignerKey::from_pkcs8`], which
250/// delegates to [`parse_key_material`] for curve detection.
251///
252/// Args on construction:
253/// * `bytes`: PKCS8 DER (Ed25519 v1/v2 or P-256).
254///
255/// Usage:
256/// ```ignore
257/// let s = TypedSignerKey::from_pkcs8(&pkcs8)?;
258/// let sig = s.sign(b"payload bytes")?;
259/// let cesr = s.cesr_encoded_pubkey(); // "D..." for Ed25519, "1AAJ..." for P-256 (transferable verkey)
260/// let pkcs8 = s.to_pkcs8()?;          // curve-aware encode (replaces build_ed25519_pkcs8_v2)
261/// ```
262#[derive(Debug)]
263pub struct TypedSignerKey {
264    /// The private seed, tagged with its curve. Private — access via [`TypedSignerKey::curve`]
265    /// or the typed sign/to_pkcs8 methods. Prevents callers from grabbing raw bytes and
266    /// re-introducing curve-less dispatch.
267    seed: TypedSeed,
268    /// The public key bytes (32 Ed25519, 33 P-256 compressed). Private — access via
269    /// [`TypedSignerKey::public_key`].
270    public_key: Vec<u8>,
271}
272
273// Marker impl — the inner `TypedSeed` is itself `ZeroizeOnDrop`, so dropping
274// a `TypedSignerKey` transitively zeroes the private key. The public key
275// field intentionally does not need to be scrubbed. This empty impl formally
276// declares the invariant so fn-128.T5's `Secret` trait bound is satisfiable.
277impl zeroize::ZeroizeOnDrop for TypedSignerKey {}
278
279impl TypedSignerKey {
280    /// Parse a PKCS8 DER blob into a curve-tagged signer.
281    pub fn from_pkcs8(bytes: &[u8]) -> Result<Self, CryptoError> {
282        let parsed = parse_key_material(bytes)?;
283        Ok(Self {
284            seed: parsed.seed,
285            public_key: parsed.public_key,
286        })
287    }
288
289    /// Construct directly from a typed seed and its derived public key.
290    /// Caller must ensure the public key matches the seed's curve; if the
291    /// lengths disagree with the curve, returns `InvalidPrivateKey`.
292    pub fn from_parts(seed: TypedSeed, public_key: Vec<u8>) -> Result<Self, CryptoError> {
293        let expected = seed.curve().public_key_len();
294        if public_key.len() != expected {
295            return Err(CryptoError::InvalidPrivateKey(format!(
296                "public key length {} does not match {} expected {} bytes",
297                public_key.len(),
298                seed.curve(),
299                expected
300            )));
301        }
302        Ok(Self { seed, public_key })
303    }
304
305    /// Derive from a typed seed by recomputing the public key.
306    #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
307    pub fn from_seed(seed: TypedSeed) -> Result<Self, CryptoError> {
308        let pk = public_key(&seed)?;
309        Ok(Self {
310            seed,
311            public_key: pk,
312        })
313    }
314
315    /// CESR-encoded public key string.
316    ///
317    /// Uses the spec-correct derivation codes:
318    /// - `D` + base64url(32 bytes) for Ed25519 (transferable verkey)
319    /// - `1AAJ` + base64url(33 bytes compressed SEC1) for P-256 (transferable verkey)
320    ///
321    /// Per the CESR master code table, `1AAJ` (`ECDSA_256r1`) is the
322    /// transferable secp256r1 verkey code — the P-256 analogue of Ed25519 `D`.
323    /// (`1AAI` / `ECDSA_256r1N` is the non-transferable variant.) Auths
324    /// identities rotate, so signers emit the transferable code.
325    pub fn cesr_encoded_pubkey(&self) -> String {
326        use cesride::Matter;
327        let code = match self.seed.curve() {
328            CurveType::Ed25519 => cesride::matter::Codex::Ed25519,
329            CurveType::P256 => cesride::matter::Codex::ECDSA_256r1,
330        };
331        #[allow(clippy::expect_used)]
332        // INVARIANT: every constructor validates public_key length against the curve, so cesride encode under the matching fixed-size code cannot fail
333        cesride::Verfer::new(Some(code), Some(&self.public_key), None, None, None)
334            .and_then(|v| v.qb64())
335            .expect("cesride verkey encode is infallible for a validated key")
336    }
337
338    /// Legacy alias; callers should prefer [`cesr_encoded_pubkey`].
339    pub fn cesr_encoded(&self) -> String {
340        self.cesr_encoded_pubkey()
341    }
342
343    /// Curve-aware PKCS8 DER encode — replaces `build_ed25519_pkcs8_v2` and
344    /// `encode_seed_as_pkcs8`. Dispatches on the seed's curve so a P-256 seed
345    /// never silently wraps as an Ed25519 PKCS8 blob (hazard S3/S4).
346    #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
347    pub fn to_pkcs8(&self) -> Result<crate::pkcs8::Pkcs8Der, CryptoError> {
348        match &self.seed {
349            TypedSeed::Ed25519(seed_bytes) => {
350                if self.public_key.len() != crate::provider::ED25519_PUBLIC_KEY_LEN {
351                    return Err(CryptoError::InvalidPrivateKey(
352                        "Ed25519 public key must be 32 bytes".to_string(),
353                    ));
354                }
355                let mut pk = [0u8; 32];
356                pk.copy_from_slice(&self.public_key);
357                let bytes = crate::key_material::build_ed25519_pkcs8_v2(seed_bytes, &pk);
358                Ok(crate::pkcs8::Pkcs8Der::new(bytes.to_vec()))
359            }
360            TypedSeed::P256(scalar) => {
361                use p256::ecdsa::SigningKey;
362                use p256::pkcs8::EncodePrivateKey;
363                let sk = SigningKey::from_slice(scalar)
364                    .map_err(|e| CryptoError::InvalidPrivateKey(format!("P-256 scalar: {e}")))?;
365                let doc = sk
366                    .to_pkcs8_der()
367                    .map_err(|e| CryptoError::OperationFailed(format!("P-256 PKCS8: {e}")))?;
368                Ok(crate::pkcs8::Pkcs8Der::new(doc.as_bytes().to_vec()))
369            }
370        }
371    }
372
373    /// Sign bytes using the signer's curve.
374    #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
375    pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
376        sign(&self.seed, message)
377    }
378
379    /// Returns the curve this signer uses.
380    pub fn curve(&self) -> CurveType {
381        self.seed.curve()
382    }
383
384    /// Returns the public key bytes (32 for Ed25519, 33 for P-256 compressed).
385    pub fn public_key(&self) -> &[u8] {
386        &self.public_key
387    }
388
389    /// Returns a reference to the typed seed. Scoped access for signing paths that
390    /// need the `TypedSeed` directly (e.g. `auths_crypto::sign(&seed, msg)`) without
391    /// exposing the raw bytes.
392    pub fn seed(&self) -> &TypedSeed {
393        &self.seed
394    }
395}
396
397/// Normalize raw public key bytes to the canonical verkey form for `curve`.
398///
399/// Hardware backends and foreign encodings may hand back non-canonical forms
400/// (e.g. an uncompressed SEC1 point); wire formats carry exactly one canonical
401/// shape per curve — 32 raw bytes for Ed25519, the 33-byte compressed SEC1
402/// point for P-256. Rejects bytes that are not a valid key on `curve`.
403///
404/// Args:
405/// * `bytes`: Raw public key bytes in any encoding the curve accepts.
406/// * `curve`: The curve the key belongs to (never inferred from length).
407///
408/// Usage:
409/// ```ignore
410/// let verkey = normalize_verkey(&hardware_pubkey, CurveType::P256)?;
411/// assert_eq!(verkey.len(), 33);
412/// ```
413pub fn normalize_verkey(bytes: &[u8], curve: CurveType) -> Result<Vec<u8>, CryptoError> {
414    match curve {
415        CurveType::Ed25519 => {
416            if bytes.len() != 32 {
417                return Err(CryptoError::OperationFailed(format!(
418                    "Ed25519 verkey must be 32 bytes, got {}",
419                    bytes.len()
420                )));
421            }
422            Ok(bytes.to_vec())
423        }
424        CurveType::P256 => {
425            #[cfg(feature = "native")]
426            {
427                use p256::elliptic_curve::sec1::ToEncodedPoint;
428                let pk = p256::PublicKey::from_sec1_bytes(bytes).map_err(|e| {
429                    CryptoError::OperationFailed(format!("invalid P-256 public key: {e}"))
430                })?;
431                Ok(pk.to_encoded_point(true).as_bytes().to_vec())
432            }
433            #[cfg(not(feature = "native"))]
434            {
435                let _ = bytes;
436                Err(CryptoError::UnsupportedTarget)
437            }
438        }
439        #[allow(unreachable_patterns)]
440        other => Err(CryptoError::OperationFailed(format!(
441            "normalize_verkey: unsupported curve {other:?}"
442        ))),
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    #[test]
451    fn typed_seed_curve_identification() {
452        let ed = TypedSeed::Ed25519([1u8; 32]);
453        assert_eq!(ed.curve(), CurveType::Ed25519);
454
455        let p = TypedSeed::P256([2u8; 32]);
456        assert_eq!(p.curve(), CurveType::P256);
457    }
458
459    #[test]
460    fn typed_seed_as_bytes() {
461        let seed = TypedSeed::Ed25519([42u8; 32]);
462        assert_eq!(seed.as_bytes(), &[42u8; 32]);
463    }
464
465    #[test]
466    fn typed_seed_debug_redacts() {
467        let seed = TypedSeed::P256([0u8; 32]);
468        let debug = format!("{:?}", seed);
469        assert!(debug.contains("REDACTED"));
470        assert!(!debug.contains("0, 0, 0"));
471    }
472
473    #[cfg(all(feature = "native", not(target_arch = "wasm32")))]
474    mod native {
475        use super::*;
476
477        #[test]
478        fn parse_ed25519_pkcs8_v2() {
479            // Generate via ring, parse back
480            use ring::rand::SystemRandom;
481            use ring::signature::Ed25519KeyPair;
482            let rng = SystemRandom::new();
483            let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
484            let parsed = parse_key_material(pkcs8.as_ref()).unwrap();
485            assert_eq!(parsed.seed.curve(), CurveType::Ed25519);
486            assert_eq!(parsed.public_key.len(), 32);
487        }
488
489        #[test]
490        fn parse_p256_pkcs8() {
491            use p256::ecdsa::SigningKey;
492            use p256::elliptic_curve::rand_core::OsRng;
493            use p256::pkcs8::EncodePrivateKey;
494            let sk = SigningKey::random(&mut OsRng);
495            let pkcs8 = sk.to_pkcs8_der().unwrap();
496            let parsed = parse_key_material(pkcs8.as_bytes()).unwrap();
497            assert_eq!(parsed.seed.curve(), CurveType::P256);
498            assert_eq!(parsed.public_key.len(), 33);
499        }
500
501        #[test]
502        fn parse_raw_32_bytes_is_ed25519() {
503            let raw = [7u8; 32];
504            let parsed = parse_key_material(&raw).unwrap();
505            assert_eq!(parsed.seed.curve(), CurveType::Ed25519);
506        }
507
508        #[test]
509        fn parse_garbage_fails() {
510            let garbage = [0xFFu8; 50];
511            assert!(parse_key_material(&garbage).is_err());
512        }
513
514        #[test]
515        fn parse_empty_fails() {
516            assert!(parse_key_material(&[]).is_err());
517        }
518
519        #[test]
520        fn sign_ed25519_roundtrip() {
521            use ring::signature::{ED25519, UnparsedPublicKey};
522            let seed = TypedSeed::Ed25519([1u8; 32]);
523            let msg = b"hello world";
524            let sig = sign(&seed, msg).unwrap();
525            assert_eq!(sig.len(), 64);
526
527            let pk = public_key(&seed).unwrap();
528            let verifier = UnparsedPublicKey::new(&ED25519, &pk);
529            assert!(verifier.verify(msg, &sig).is_ok());
530        }
531
532        #[test]
533        fn sign_p256_roundtrip() {
534            use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
535            let seed = TypedSeed::P256([3u8; 32]);
536            let msg = b"hello p256";
537            let sig_bytes = sign(&seed, msg).unwrap();
538            assert_eq!(sig_bytes.len(), 64);
539
540            let pk_bytes = public_key(&seed).unwrap();
541            assert_eq!(pk_bytes.len(), 33);
542
543            let vk = VerifyingKey::from_sec1_bytes(&pk_bytes).unwrap();
544            let sig = Signature::from_slice(&sig_bytes).unwrap();
545            assert!(vk.verify(msg, &sig).is_ok());
546        }
547
548        #[test]
549        fn cross_curve_isolation() {
550            // Same raw bytes, different curves, different outputs
551            let bytes = [5u8; 32];
552            let ed_seed = TypedSeed::Ed25519(bytes);
553            let p256_seed = TypedSeed::P256(bytes);
554
555            let ed_pk = public_key(&ed_seed).unwrap();
556            let p256_pk = public_key(&p256_seed).unwrap();
557
558            // Different lengths (32 vs 33) and different values
559            assert_ne!(ed_pk.len(), p256_pk.len());
560
561            let msg = b"test";
562            let ed_sig = sign(&ed_seed, msg).unwrap();
563            let p256_sig = sign(&p256_seed, msg).unwrap();
564
565            // Both 64 bytes but different values
566            assert_eq!(ed_sig.len(), 64);
567            assert_eq!(p256_sig.len(), 64);
568            assert_ne!(ed_sig, p256_sig);
569        }
570
571        #[test]
572        fn parse_then_sign_ed25519() {
573            use ring::rand::SystemRandom;
574            use ring::signature::{ED25519, Ed25519KeyPair, UnparsedPublicKey};
575            let rng = SystemRandom::new();
576            let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
577            let parsed = parse_key_material(pkcs8.as_ref()).unwrap();
578
579            let msg = b"end to end";
580            let sig = sign(&parsed.seed, msg).unwrap();
581            let verifier = UnparsedPublicKey::new(&ED25519, &parsed.public_key);
582            assert!(verifier.verify(msg, &sig).is_ok());
583        }
584
585        #[test]
586        fn parse_then_sign_p256() {
587            use p256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::Verifier};
588            use p256::elliptic_curve::rand_core::OsRng;
589            use p256::pkcs8::EncodePrivateKey;
590
591            let sk = SigningKey::random(&mut OsRng);
592            let pkcs8 = sk.to_pkcs8_der().unwrap();
593            let parsed = parse_key_material(pkcs8.as_bytes()).unwrap();
594
595            let msg = b"end to end p256";
596            let sig_bytes = sign(&parsed.seed, msg).unwrap();
597
598            let vk = VerifyingKey::from_sec1_bytes(&parsed.public_key).unwrap();
599            let sig = Signature::from_slice(&sig_bytes).unwrap();
600            assert!(vk.verify(msg, &sig).is_ok());
601        }
602
603        #[test]
604        fn typed_signer_key_ed25519_roundtrip() {
605            use ring::rand::SystemRandom;
606            use ring::signature::Ed25519KeyPair;
607            let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
608            let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
609            assert_eq!(s.curve(), CurveType::Ed25519);
610            assert!(s.cesr_encoded_pubkey().starts_with('D'));
611            assert_eq!(s.public_key().len(), 32);
612            let sig = s.sign(b"msg").unwrap();
613            assert_eq!(sig.len(), 64);
614        }
615
616        #[test]
617        fn typed_signer_key_p256_roundtrip() {
618            use p256::ecdsa::SigningKey;
619            use p256::elliptic_curve::rand_core::OsRng;
620            use p256::pkcs8::EncodePrivateKey;
621            let sk = SigningKey::random(&mut OsRng);
622            let pkcs8 = sk.to_pkcs8_der().unwrap();
623            let s = TypedSignerKey::from_pkcs8(pkcs8.as_bytes()).unwrap();
624            assert_eq!(s.curve(), CurveType::P256);
625            assert!(s.cesr_encoded_pubkey().starts_with("1AAJ"));
626            assert_eq!(s.public_key().len(), 33);
627            let sig = s.sign(b"msg").unwrap();
628            assert_eq!(sig.len(), 64);
629        }
630
631        #[test]
632        fn typed_signer_key_to_pkcs8_ed25519_roundtrip() {
633            use ring::rand::SystemRandom;
634            use ring::signature::Ed25519KeyPair;
635            let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
636            let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
637            let encoded = s.to_pkcs8().unwrap();
638            let reparsed = TypedSignerKey::from_pkcs8(encoded.as_ref()).unwrap();
639            assert_eq!(reparsed.curve(), CurveType::Ed25519);
640            assert_eq!(reparsed.public_key(), s.public_key());
641            assert_eq!(reparsed.seed.as_bytes(), s.seed.as_bytes());
642        }
643
644        #[test]
645        fn typed_signer_key_to_pkcs8_p256_roundtrip() {
646            let seed = TypedSeed::P256({
647                let mut scalar = [9u8; 32];
648                scalar[0] |= 1;
649                scalar
650            });
651            let s = TypedSignerKey::from_seed(seed).unwrap();
652            let encoded = s.to_pkcs8().unwrap();
653            let reparsed = TypedSignerKey::from_pkcs8(encoded.as_ref()).unwrap();
654            assert_eq!(reparsed.curve(), CurveType::P256);
655            assert_eq!(reparsed.public_key(), s.public_key());
656            assert_eq!(reparsed.seed.as_bytes(), s.seed.as_bytes());
657        }
658
659        #[test]
660        fn rotation_signer_alias_still_works() {
661            use ring::rand::SystemRandom;
662            use ring::signature::Ed25519KeyPair;
663            let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
664            // Via the transitional alias
665            let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
666            assert_eq!(s.curve(), CurveType::Ed25519);
667        }
668
669        #[test]
670        fn typed_signer_key_from_parts_rejects_mismatched_pubkey_length() {
671            let seed = TypedSeed::Ed25519([1u8; 32]);
672            let wrong_len_pk = vec![0u8; 33]; // 33 bytes, expected 32 for Ed25519
673            let err = TypedSignerKey::from_parts(seed, wrong_len_pk).unwrap_err();
674            assert!(matches!(err, CryptoError::InvalidPrivateKey(_)));
675        }
676
677        /// Regression — ECDSA-P256 must be deterministic (RFC 6979). If this
678        /// breaks, something has routed signing through a randomized-nonce
679        /// path, which is class-breaking (Sony PS3 failure mode).
680        ///
681        /// NOTE: The FIPS provider (aws-lc-rs) adds randomized blinding to
682        /// nonce computation for side-channel resistance. Signatures are still
683        /// RFC 6979 compliant (verifiable) but not byte-deterministic. This
684        /// test is gated to the default (ring) provider only.
685        #[test]
686        #[cfg(not(feature = "fips"))]
687        fn sign_p256_is_rfc6979_deterministic() {
688            let seed = TypedSeed::P256([7u8; 32]);
689            let msg = b"fn-128.T2 determinism";
690            let a = sign(&seed, msg).unwrap();
691            let b = sign(&seed, msg).unwrap();
692            let c = sign(&seed, msg).unwrap();
693            assert_eq!(a, b);
694            assert_eq!(b, c);
695            assert_eq!(a.len(), 64);
696        }
697
698        /// Regression — Ed25519 is deterministic by construction (RFC 8032).
699        /// Pairs with the P-256 test to assert the whole sign surface is
700        /// deterministic.
701        #[test]
702        fn sign_ed25519_is_deterministic() {
703            let seed = TypedSeed::Ed25519([11u8; 32]);
704            let msg = b"fn-128.T2 determinism";
705            let a = sign(&seed, msg).unwrap();
706            let b = sign(&seed, msg).unwrap();
707            assert_eq!(a, b);
708            assert_eq!(a.len(), 64);
709        }
710
711        /// Regression — the sync `sign` free function and the async trait
712        /// `sign_typed` method must produce byte-identical output. Proves the
713        /// T2 refactor did not fork signing behavior across the two entry
714        /// points; a FIPS/CNSA swap that changes one must change the other
715        /// and this test will catch a drift.
716        ///
717        /// Gated to non-FIPS: under `--features fips`, the sync provider is
718        /// aws-lc-rs (randomized blinding) while this test compares against
719        /// Ring (deterministic). The two produce valid but non-identical sigs.
720        #[tokio::test]
721        #[cfg(not(feature = "fips"))]
722        async fn sync_sign_matches_async_sign_typed_p256() {
723            use crate::provider::CryptoProvider;
724            use crate::ring_provider::RingCryptoProvider;
725
726            let seed = TypedSeed::P256([42u8; 32]);
727            let msg = b"parity check";
728
729            let sync_sig = sign(&seed, msg).unwrap();
730            let async_sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
731
732            assert_eq!(sync_sig, async_sig);
733        }
734
735        #[tokio::test]
736        async fn sync_sign_matches_async_sign_typed_ed25519() {
737            use crate::provider::CryptoProvider;
738            use crate::ring_provider::RingCryptoProvider;
739
740            let seed = TypedSeed::Ed25519([19u8; 32]);
741            let msg = b"parity check";
742
743            let sync_sig = sign(&seed, msg).unwrap();
744            let async_sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
745
746            assert_eq!(sync_sig, async_sig);
747        }
748
749        /// Regression — the sync `public_key` free function and the async
750        /// trait `typed_public_key_from_seed` method must produce byte-identical
751        /// output across curves.
752        #[tokio::test]
753        async fn sync_public_key_matches_async_typed_public_key() {
754            use crate::provider::CryptoProvider;
755            use crate::ring_provider::RingCryptoProvider;
756
757            for (name, seed) in [
758                ("ed25519", TypedSeed::Ed25519([23u8; 32])),
759                ("p256", TypedSeed::P256([29u8; 32])),
760            ] {
761                let sync_pk = public_key(&seed).unwrap();
762                let async_pk = RingCryptoProvider
763                    .typed_public_key_from_seed(&seed)
764                    .await
765                    .unwrap();
766                assert_eq!(sync_pk, async_pk, "pub key drift on {name}");
767            }
768        }
769
770        /// Regression — the async trait `sign_typed` round-trips through
771        /// `verify_typed` with the public key derived from the same seed.
772        /// Covers the curve-agnostic path end-to-end.
773        #[tokio::test]
774        async fn sign_typed_and_verify_typed_round_trip() {
775            use crate::provider::CryptoProvider;
776            use crate::ring_provider::RingCryptoProvider;
777
778            for seed in [TypedSeed::Ed25519([31u8; 32]), TypedSeed::P256([37u8; 32])] {
779                let msg = b"curve-agnostic round trip";
780                let pk = RingCryptoProvider
781                    .typed_public_key_from_seed(&seed)
782                    .await
783                    .unwrap();
784                let sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
785                RingCryptoProvider
786                    .verify_typed(seed.curve(), &pk, msg, &sig)
787                    .await
788                    .expect("verify_typed should accept matching signature");
789            }
790        }
791    }
792}