Skip to main content

auths_crypto/
provider.rs

1//! Curve-agnostic cryptographic abstraction supporting Ed25519 and ECDSA P-256.
2//!
3//! Defines the [`CryptoProvider`] trait for signature verification, signing, and
4//! key generation — enabling `ring`/`p256` on native targets and `WebCrypto` on WASM.
5//! P-256 is the workspace default curve; Ed25519 is supported as a peer alternative
6//! (SSH/Radicle/legacy KERI compat). See `docs/architecture/cryptography.md`.
7
8use async_trait::async_trait;
9use zeroize::{Zeroize, ZeroizeOnDrop};
10
11/// Error type for cryptographic operations.
12///
13/// Usage:
14/// ```ignore
15/// match result {
16///     Err(CryptoError::InvalidSignature) => { /* signature did not verify */ }
17///     Err(CryptoError::UnsupportedTarget) => { /* not available on this platform */ }
18///     Err(CryptoError::OperationFailed(msg)) => { /* backend error */ }
19///     Ok(()) => { /* success */ }
20/// }
21/// ```
22#[derive(Debug, Clone, thiserror::Error)]
23#[non_exhaustive]
24pub enum CryptoError {
25    #[error("Invalid signature")]
26    InvalidSignature,
27
28    #[error("Invalid public key length: expected {expected}, got {actual}")]
29    InvalidKeyLength { expected: usize, actual: usize },
30
31    #[error("Invalid private key: {0}")]
32    InvalidPrivateKey(String),
33
34    #[error("Crypto operation failed: {0}")]
35    OperationFailed(String),
36
37    #[error("Operation not supported on current compilation target")]
38    UnsupportedTarget,
39}
40
41impl crate::AuthsErrorInfo for CryptoError {
42    fn error_code(&self) -> &'static str {
43        match self {
44            Self::InvalidSignature => "AUTHS-E1001",
45            Self::InvalidKeyLength { .. } => "AUTHS-E1002",
46            Self::InvalidPrivateKey(_) => "AUTHS-E1003",
47            Self::OperationFailed(_) => "AUTHS-E1004",
48            Self::UnsupportedTarget => "AUTHS-E1005",
49        }
50    }
51
52    fn suggestion(&self) -> Option<&'static str> {
53        match self {
54            Self::InvalidSignature => Some("The signature does not match the data or public key"),
55            Self::InvalidKeyLength { .. } => Some(
56                "Ensure the key length matches the declared curve (32 bytes Ed25519, 33 bytes P-256 compressed SEC1)",
57            ),
58            Self::UnsupportedTarget => {
59                Some("This operation is not available on the current platform")
60            }
61            _ => None,
62        }
63    }
64}
65
66/// Zeroize-on-drop wrapper for a raw 32-byte signing seed.
67///
68/// Curve-untyped — both Ed25519 and P-256 use 32-byte scalars, so the curve
69/// must be carried separately (e.g. via [`crate::TypedSeed`]). For curve-aware
70/// flows prefer `TypedSeed`; `SecureSeed` exists for the curve-agnostic
71/// trait surface on [`CryptoProvider`].
72///
73/// This is the portable key representation that crosses the [`CryptoProvider`]
74/// boundary. No ring types leak through the trait — only this raw seed.
75/// The provider materializes the internal keypair from the seed on each call.
76///
77/// Usage:
78/// ```ignore
79/// let (seed, pubkey) = provider.generate_ed25519_keypair().await?;
80/// let sig = provider.sign_ed25519(&seed, b"hello").await?;
81/// // seed is securely zeroed when dropped
82/// ```
83#[derive(Clone, Zeroize, ZeroizeOnDrop)]
84pub struct SecureSeed([u8; 32]);
85
86impl SecureSeed {
87    pub fn new(bytes: [u8; 32]) -> Self {
88        Self(bytes)
89    }
90
91    pub fn as_bytes(&self) -> &[u8; 32] {
92        &self.0
93    }
94}
95
96impl std::fmt::Debug for SecureSeed {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.write_str("SecureSeed([REDACTED])")
99    }
100}
101
102/// Curve-agnostic abstraction for cryptographic operations across target architectures.
103///
104/// All method signatures use primitive Rust types or [`SecureSeed`] — no
105/// ring-specific types. This ensures domain crates (`auths-core`, `auths-sdk`)
106/// compile without any ring dependency.
107///
108/// Usage:
109/// ```ignore
110/// use auths_crypto::CryptoProvider;
111///
112/// async fn roundtrip(provider: &dyn CryptoProvider) {
113///     let (seed, pk) = provider.generate_ed25519_keypair().await.unwrap();
114///     let sig = provider.sign_ed25519(&seed, b"msg").await.unwrap();
115///     provider.verify_ed25519(&pk, b"msg", &sig).await.unwrap();
116/// }
117/// ```
118#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
119#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
120pub trait CryptoProvider: Send + Sync {
121    /// Verify an Ed25519 signature against a public key and message.
122    async fn verify_ed25519(
123        &self,
124        pubkey: &[u8],
125        message: &[u8],
126        signature: &[u8],
127    ) -> Result<(), CryptoError>;
128
129    /// Verify an ECDSA P-256 signature (r||s, 64 bytes) against a public key
130    /// (33-byte compressed or 65-byte uncompressed SEC1) and message.
131    ///
132    /// Default impl returns `UnsupportedTarget`; override in providers that
133    /// support P-256 (`RingCryptoProvider` via `p256` crate on native,
134    /// `WebCryptoProvider` via `SubtleCrypto.verify("ECDSA", …)` on WASM).
135    async fn verify_p256(
136        &self,
137        _pubkey: &[u8],
138        _message: &[u8],
139        _signature: &[u8],
140    ) -> Result<(), CryptoError> {
141        Err(CryptoError::UnsupportedTarget)
142    }
143
144    /// Sign a message using a raw 32-byte Ed25519 seed.
145    ///
146    /// The provider materializes the internal keypair from the seed on each
147    /// call. This trades minor CPU overhead for a pure, ring-free domain layer.
148    ///
149    /// Args:
150    /// * `seed`: Raw 32-byte Ed25519 private key seed.
151    /// * `message`: The data to sign.
152    ///
153    /// Usage:
154    /// ```ignore
155    /// let sig = provider.sign_ed25519(&seed, b"hello").await?;
156    /// assert_eq!(sig.len(), 64);
157    /// ```
158    async fn sign_ed25519(&self, seed: &SecureSeed, message: &[u8])
159    -> Result<Vec<u8>, CryptoError>;
160
161    /// Generate a fresh Ed25519 keypair.
162    ///
163    /// Returns the raw 32-byte seed and 32-byte public key.
164    ///
165    /// Usage:
166    /// ```ignore
167    /// let (seed, pubkey) = provider.generate_ed25519_keypair().await?;
168    /// assert_eq!(pubkey.len(), 32);
169    /// ```
170    async fn generate_ed25519_keypair(&self) -> Result<(SecureSeed, [u8; 32]), CryptoError>;
171
172    /// Derive the 32-byte public key from a raw seed.
173    ///
174    /// Args:
175    /// * `seed`: Raw 32-byte Ed25519 private key seed.
176    ///
177    /// Usage:
178    /// ```ignore
179    /// let pk = provider.ed25519_public_key_from_seed(&seed).await?;
180    /// assert_eq!(pk.len(), 32);
181    /// ```
182    async fn ed25519_public_key_from_seed(
183        &self,
184        seed: &SecureSeed,
185    ) -> Result<[u8; 32], CryptoError>;
186
187    /// Sign a message using a raw 32-byte P-256 scalar seed. Deterministic
188    /// ECDSA per RFC 6979. Returns a 64-byte compact r||s signature.
189    ///
190    /// Args:
191    /// * `seed`: Raw 32-byte P-256 private scalar.
192    /// * `message`: The data to sign.
193    ///
194    /// Usage:
195    /// ```ignore
196    /// let sig = provider.sign_p256(&seed, b"hello").await?;
197    /// assert_eq!(sig.len(), P256_SIGNATURE_LEN);
198    /// ```
199    async fn sign_p256(&self, _seed: &SecureSeed, _message: &[u8]) -> Result<Vec<u8>, CryptoError> {
200        Err(CryptoError::UnsupportedTarget)
201    }
202
203    /// Generate a fresh P-256 keypair. Returns the raw 32-byte scalar seed
204    /// and the 33-byte SEC1 compressed public key.
205    ///
206    /// Usage:
207    /// ```ignore
208    /// let (seed, pubkey) = provider.generate_p256_keypair().await?;
209    /// assert_eq!(pubkey.len(), P256_PUBLIC_KEY_LEN);
210    /// ```
211    async fn generate_p256_keypair(&self) -> Result<(SecureSeed, Vec<u8>), CryptoError> {
212        Err(CryptoError::UnsupportedTarget)
213    }
214
215    /// Derive the 33-byte SEC1 compressed P-256 public key from a raw seed.
216    ///
217    /// Args:
218    /// * `seed`: Raw 32-byte P-256 private scalar.
219    ///
220    /// Usage:
221    /// ```ignore
222    /// let pk = provider.p256_public_key_from_seed(&seed).await?;
223    /// assert_eq!(pk.len(), P256_PUBLIC_KEY_LEN);
224    /// ```
225    async fn p256_public_key_from_seed(&self, _seed: &SecureSeed) -> Result<Vec<u8>, CryptoError> {
226        Err(CryptoError::UnsupportedTarget)
227    }
228
229    /// Encrypt `plaintext` under a 256-bit symmetric `key` with a 96-bit
230    /// `nonce` and Additional Authenticated Data (AAD). Output is
231    /// `ciphertext || tag` (tag is 16 bytes for both ChaCha20-Poly1305 and
232    /// AES-256-GCM).
233    ///
234    /// Algorithm selection is compile-time via Cargo feature:
235    /// - default build: ChaCha20-Poly1305
236    /// - `cnsa` feature (fn-128.T4): AES-256-GCM
237    ///
238    /// Args:
239    /// * `key`: 32-byte symmetric key.
240    /// * `nonce`: 12-byte per-message nonce; MUST NOT repeat under the same key.
241    /// * `aad`: Additional authenticated data (authenticated but not encrypted).
242    /// * `plaintext`: Bytes to encrypt.
243    ///
244    /// Usage:
245    /// ```ignore
246    /// let ct = provider.aead_encrypt(&key, &nonce, b"session:1", b"secret").await?;
247    /// ```
248    async fn aead_encrypt(
249        &self,
250        _key: &[u8; 32],
251        _nonce: &[u8; 12],
252        _aad: &[u8],
253        _plaintext: &[u8],
254    ) -> Result<Vec<u8>, CryptoError> {
255        Err(CryptoError::UnsupportedTarget)
256    }
257
258    /// Decrypt `ciphertext` (`ct || tag`) under a 256-bit symmetric `key`,
259    /// 96-bit `nonce`, and matching AAD. Returns the plaintext on success;
260    /// `InvalidSignature` on tag mismatch.
261    ///
262    /// Args:
263    /// * `key`: 32-byte symmetric key.
264    /// * `nonce`: 12-byte nonce (must match the one used at encryption).
265    /// * `aad`: Additional authenticated data (must match encryption AAD byte-for-byte).
266    /// * `ciphertext`: `encrypt` output (`ct || tag`).
267    ///
268    /// Usage:
269    /// ```ignore
270    /// let pt = provider.aead_decrypt(&key, &nonce, b"session:1", &ct).await?;
271    /// ```
272    async fn aead_decrypt(
273        &self,
274        _key: &[u8; 32],
275        _nonce: &[u8; 12],
276        _aad: &[u8],
277        _ciphertext: &[u8],
278    ) -> Result<Vec<u8>, CryptoError> {
279        Err(CryptoError::UnsupportedTarget)
280    }
281
282    /// One-shot HKDF-SHA256 extract-then-expand (RFC 5869).
283    ///
284    /// Args:
285    /// * `ikm`: Input keying material (secret).
286    /// * `salt`: Non-secret salt (empty slice is acceptable).
287    /// * `info`: Domain-separating context tag.
288    /// * `out_len`: Desired output length in bytes (max 255 × 32 = 8160).
289    ///
290    /// Usage:
291    /// ```ignore
292    /// let okm = provider.hkdf_sha256_expand(&ikm, &salt, b"my-proto-v1", 32).await?;
293    /// ```
294    async fn hkdf_sha256_expand(
295        &self,
296        _ikm: &[u8],
297        _salt: &[u8],
298        _info: &[u8],
299        _out_len: usize,
300    ) -> Result<Vec<u8>, CryptoError> {
301        Err(CryptoError::UnsupportedTarget)
302    }
303
304    /// One-shot HKDF-SHA384 extract-then-expand. Same shape as
305    /// [`hkdf_sha256_expand`] but with SHA-384 as the underlying hash. CNSA 2.0
306    /// requires SHA-384 for NSS workloads; default builds may return
307    /// [`CryptoError::UnsupportedTarget`] if SHA-384 is not available.
308    ///
309    /// Max output length is 255 × 48 = 12240 bytes.
310    async fn hkdf_sha384_expand(
311        &self,
312        _ikm: &[u8],
313        _salt: &[u8],
314        _info: &[u8],
315        _out_len: usize,
316    ) -> Result<Vec<u8>, CryptoError> {
317        Err(CryptoError::UnsupportedTarget)
318    }
319
320    /// Compute HMAC-SHA256 over `msg` under `key`. Returns a 32-byte tag.
321    ///
322    /// Args:
323    /// * `key`: MAC key (any length; HMAC hashes over-long keys internally).
324    /// * `msg`: Bytes to authenticate.
325    ///
326    /// Usage:
327    /// ```ignore
328    /// let tag = provider.hmac_sha256_compute(&key, b"GET\n/path\n...").await?;
329    /// ```
330    async fn hmac_sha256_compute(&self, _key: &[u8], _msg: &[u8]) -> Result<[u8; 32], CryptoError> {
331        Err(CryptoError::UnsupportedTarget)
332    }
333
334    /// Verify an HMAC-SHA256 `tag` over `msg` under `key`, constant-time.
335    ///
336    /// Returns `Ok(())` on match, `Err(CryptoError::InvalidSignature)` on
337    /// mismatch. Implementations MUST use a constant-time comparator
338    /// (`subtle::ct_eq`, `ring::constant_time`, or equivalent).
339    ///
340    /// Args:
341    /// * `key`: MAC key used at compute time.
342    /// * `msg`: Bytes whose authenticity is being checked.
343    /// * `tag`: Claimed 32-byte tag.
344    ///
345    /// Usage:
346    /// ```ignore
347    /// provider.hmac_sha256_verify(&key, &msg, &claimed_tag).await?;
348    /// ```
349    async fn hmac_sha256_verify(
350        &self,
351        _key: &[u8],
352        _msg: &[u8],
353        _tag: &[u8],
354    ) -> Result<(), CryptoError> {
355        Err(CryptoError::UnsupportedTarget)
356    }
357
358    /// Compute HMAC-SHA384 over `msg` under `key`. Returns a 48-byte tag.
359    /// CNSA counterpart of [`hmac_sha256_compute`].
360    async fn hmac_sha384_compute(&self, _key: &[u8], _msg: &[u8]) -> Result<[u8; 48], CryptoError> {
361        Err(CryptoError::UnsupportedTarget)
362    }
363
364    /// Verify an HMAC-SHA384 `tag` over `msg` under `key`, constant-time.
365    /// CNSA counterpart of [`hmac_sha256_verify`].
366    async fn hmac_sha384_verify(
367        &self,
368        _key: &[u8],
369        _msg: &[u8],
370        _tag: &[u8],
371    ) -> Result<(), CryptoError> {
372        Err(CryptoError::UnsupportedTarget)
373    }
374
375    // -------------------------------------------------------------------------
376    // Curve-agnostic entry points (default-impl'd; domain code SHOULD use these)
377    //
378    // These route through a single `match` on the caller-supplied curve, then
379    // dispatch to the primitive methods above. Domain code never needs to
380    // `match CurveType` again — that's fn-121's ethos at the provider boundary.
381    // -------------------------------------------------------------------------
382
383    /// Sign a message using a curve-carrying seed. Dispatches internally based
384    /// on `seed.curve()`; the caller never sees a curve-specific method.
385    ///
386    /// Args:
387    /// * `seed`: Typed seed carrying its curve tag.
388    /// * `message`: Bytes to sign.
389    ///
390    /// Usage:
391    /// ```ignore
392    /// let sig = provider.sign_typed(&typed_seed, message).await?;
393    /// ```
394    async fn sign_typed(
395        &self,
396        seed: &crate::key_ops::TypedSeed,
397        message: &[u8],
398    ) -> Result<Vec<u8>, CryptoError> {
399        let secure = SecureSeed::new(*seed.as_bytes());
400        match seed.curve() {
401            CurveType::Ed25519 => self.sign_ed25519(&secure, message).await,
402            CurveType::P256 => self.sign_p256(&secure, message).await,
403        }
404    }
405
406    /// Verify a signature using a curve and raw public-key bytes. Dispatches
407    /// internally; callers never see a curve-specific method.
408    ///
409    /// Args:
410    /// * `curve`: The curve the public key belongs to.
411    /// * `pubkey`: Raw public-key bytes (length validated by the curve-specific primitive).
412    /// * `message`: Bytes the signature is over.
413    /// * `signature`: Signature bytes to verify.
414    ///
415    /// Usage:
416    /// ```ignore
417    /// provider.verify_typed(curve, &pk_bytes, msg, &sig).await?;
418    /// ```
419    async fn verify_typed(
420        &self,
421        curve: CurveType,
422        pubkey: &[u8],
423        message: &[u8],
424        signature: &[u8],
425    ) -> Result<(), CryptoError> {
426        match curve {
427            CurveType::Ed25519 => self.verify_ed25519(pubkey, message, signature).await,
428            CurveType::P256 => self.verify_p256(pubkey, message, signature).await,
429        }
430    }
431
432    /// Generate a keypair for the requested curve. Returns a curve-tagged
433    /// [`crate::key_ops::TypedSeed`] plus raw public-key bytes.
434    ///
435    /// Args:
436    /// * `curve`: Which curve to generate for.
437    ///
438    /// Usage:
439    /// ```ignore
440    /// let (typed_seed, pubkey_bytes) = provider.generate_typed_keypair(curve).await?;
441    /// ```
442    async fn generate_typed_keypair(
443        &self,
444        curve: CurveType,
445    ) -> Result<(crate::key_ops::TypedSeed, Vec<u8>), CryptoError> {
446        match curve {
447            CurveType::Ed25519 => {
448                let (seed, pk) = self.generate_ed25519_keypair().await?;
449                Ok((
450                    crate::key_ops::TypedSeed::Ed25519(*seed.as_bytes()),
451                    pk.to_vec(),
452                ))
453            }
454            CurveType::P256 => {
455                let (seed, pk) = self.generate_p256_keypair().await?;
456                Ok((crate::key_ops::TypedSeed::P256(*seed.as_bytes()), pk))
457            }
458        }
459    }
460
461    /// Derive the public key from a curve-tagged seed.
462    ///
463    /// Args:
464    /// * `seed`: Typed seed.
465    ///
466    /// Usage:
467    /// ```ignore
468    /// let pk_bytes = provider.typed_public_key_from_seed(&typed_seed).await?;
469    /// ```
470    async fn typed_public_key_from_seed(
471        &self,
472        seed: &crate::key_ops::TypedSeed,
473    ) -> Result<Vec<u8>, CryptoError> {
474        let secure = SecureSeed::new(*seed.as_bytes());
475        match seed.curve() {
476            CurveType::Ed25519 => {
477                let pk = self.ed25519_public_key_from_seed(&secure).await?;
478                Ok(pk.to_vec())
479            }
480            CurveType::P256 => self.p256_public_key_from_seed(&secure).await,
481        }
482    }
483}
484
485// ---------------------------------------------------------------------------
486// Feature-combination guards (compile-time enforcement of provider invariants)
487// ---------------------------------------------------------------------------
488
489// `fips` and `cnsa` both replace the default provider with incompatible
490// primitive sets (FIPS uses aws-lc-rs's ChaCha20-Poly1305; CNSA forbids
491// ChaCha and mandates AES-256-GCM + SHA-384 + P-384). When a build enables
492// both — commonly `cargo {build,check,clippy} --all-features` — the
493// downstream `cfg(all(feature = "cnsa", not(feature = "fips"), …))` gates
494// on the CNSA provider silently win, so FIPS takes precedence and the
495// CNSA-specific provider is excluded. Consumers who want CNSA MUST NOT
496// enable `fips` in their dependency edge.
497//
498// We intentionally do NOT emit `compile_error!` here: `--all-features` is
499// a first-class developer ergonomic (CI hooks, `cargo hack`, etc.) and
500// failing that build surface-level cost exceeds the safety benefit — the
501// cfg gates below already produce a deterministic, single-provider
502// binary. A lint scanner in `xtask` (see `check-feature-sanity`) enforces
503// "never enable both in a deployed profile" at the supply-chain layer.
504
505/// `fips` requires CMake + Go + C toolchain and targets native platforms only.
506/// aws-lc-rs has no supported WASM target.
507#[cfg(all(feature = "fips", target_arch = "wasm32"))]
508compile_error!(
509    "auths-crypto feature `fips` is incompatible with `target_arch = wasm32`. \
510     Build the verifier with default features (RustCrypto + ring)."
511);
512
513/// Returns the workspace-configured default crypto provider.
514///
515/// Selection is compile-time:
516/// - default build → [`crate::ring_provider::RingCryptoProvider`]
517/// - `--features fips` → [`crate::aws_lc_provider::AwsLcProvider`] (AWS-LC-FIPS)
518/// - `--features cnsa` → (fn-128.T4; TODO: returns Ring until CnsaProvider lands)
519///
520/// Domain code SHOULD route cryptographic operations through this function
521/// rather than constructing `p256::ecdsa::SigningKey` (or equivalent) directly —
522/// that way the provider swap is mechanical.
523///
524/// Usage:
525/// ```ignore
526/// use auths_crypto::{default_provider, CryptoProvider};
527///
528/// let provider = default_provider();
529/// let sig = provider.sign_p256(&seed, msg).await?;
530/// ```
531#[cfg(all(feature = "fips", not(target_arch = "wasm32")))]
532pub fn default_provider() -> &'static dyn CryptoProvider {
533    &crate::aws_lc_provider::AwsLcProvider
534}
535
536#[cfg(all(feature = "cnsa", not(feature = "fips"), not(target_arch = "wasm32")))]
537pub fn default_provider() -> &'static dyn CryptoProvider {
538    &crate::cnsa_provider::CnsaProvider
539}
540
541#[cfg(all(
542    feature = "native",
543    not(feature = "fips"),
544    not(feature = "cnsa"),
545    not(target_arch = "wasm32")
546))]
547pub fn default_provider() -> &'static dyn CryptoProvider {
548    &crate::ring_provider::RingCryptoProvider
549}
550
551/// Errors from hex seed decoding.
552///
553/// Usage:
554/// ```ignore
555/// match decode_seed_hex("bad") {
556///     Err(SeedDecodeError::InvalidHex(_)) => { /* not valid hex */ }
557///     Err(SeedDecodeError::WrongLength { .. }) => { /* not 32 bytes */ }
558///     Ok(seed) => { /* use seed */ }
559/// }
560/// ```
561#[derive(Debug, thiserror::Error)]
562pub enum SeedDecodeError {
563    /// The input string is not valid hexadecimal.
564    #[error("invalid hex encoding: {0}")]
565    InvalidHex(hex::FromHexError),
566
567    /// The decoded bytes are not exactly 32 bytes.
568    #[error("expected {expected} bytes, got {got}")]
569    WrongLength {
570        /// Expected byte count (always 32).
571        expected: usize,
572        /// Actual byte count after decoding.
573        got: usize,
574    },
575}
576
577/// Decodes a hex-encoded Ed25519 seed (64 hex chars = 32 bytes) into a [`SecureSeed`].
578///
579/// Args:
580/// * `hex_str`: Hex-encoded seed string (must be exactly 64 characters).
581///
582/// Usage:
583/// ```ignore
584/// let seed = decode_seed_hex("abcdef01...")?;
585/// ```
586pub fn decode_seed_hex(hex_str: &str) -> Result<SecureSeed, SeedDecodeError> {
587    let bytes = hex::decode(hex_str).map_err(SeedDecodeError::InvalidHex)?;
588    let arr: [u8; 32] = bytes
589        .try_into()
590        .map_err(|v: Vec<u8>| SeedDecodeError::WrongLength {
591            expected: 32,
592            got: v.len(),
593        })?;
594    Ok(SecureSeed::new(arr))
595}
596
597/// Ed25519 public key length in bytes.
598pub const ED25519_PUBLIC_KEY_LEN: usize = 32;
599
600/// Ed25519 signature length in bytes.
601pub const ED25519_SIGNATURE_LEN: usize = 64;
602
603/// ECDSA P-256 compressed public key length in bytes (SEC1: 0x02/0x03 + 32-byte x).
604pub const P256_PUBLIC_KEY_LEN: usize = 33;
605
606/// ECDSA P-256 raw r||s signature length in bytes (32 + 32).
607pub const P256_SIGNATURE_LEN: usize = 64;
608
609/// Supported elliptic curve types for identity and signing operations.
610///
611/// P-256 is the default for all operations (identity keys, ephemeral CI keys).
612/// Ed25519 is available for compatibility with existing KERI deployments.
613///
614/// Usage:
615/// ```ignore
616/// let curve = CurveType::P256; // default
617/// let (seed, pubkey) = provider.generate_keypair(curve).await?;
618/// ```
619#[derive(
620    Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
621)]
622#[serde(rename_all = "lowercase")]
623pub enum CurveType {
624    /// Ed25519 (RFC 8032). 32-byte keys, 64-byte signatures.
625    Ed25519,
626    /// ECDSA P-256 / secp256r1. 33-byte compressed keys, 64-byte r||s signatures.
627    /// Default for all operations.
628    #[default]
629    P256,
630}
631
632impl std::fmt::Display for CurveType {
633    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
634        match self {
635            Self::Ed25519 => f.write_str("ed25519"),
636            Self::P256 => f.write_str("p256"),
637        }
638    }
639}
640
641impl CurveType {
642    /// Returns the expected public key length for this curve.
643    pub fn public_key_len(&self) -> usize {
644        match self {
645            Self::Ed25519 => ED25519_PUBLIC_KEY_LEN,
646            Self::P256 => P256_PUBLIC_KEY_LEN,
647        }
648    }
649
650    /// Returns the expected signature length for this curve.
651    pub fn signature_len(&self) -> usize {
652        match self {
653            Self::Ed25519 => ED25519_SIGNATURE_LEN,
654            Self::P256 => P256_SIGNATURE_LEN,
655        }
656    }
657}