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