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}