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}