Skip to main content

auths_crypto/
ring_provider.rs

1// INVARIANT: sanctioned crypto boundary — the one place ring is allowed to
2// live. All curve-dispatched verify / sign / keypair-generate paths bottom
3// out here. Permanent allow; do NOT remove in fn-114.40.
4#![allow(clippy::disallowed_methods)]
5
6use async_trait::async_trait;
7
8use crate::provider::{CryptoError, CryptoProvider, ED25519_PUBLIC_KEY_LEN, SecureSeed};
9use ring::rand::SystemRandom;
10use ring::signature::{ED25519, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
11
12/// Native crypto provider powered by `ring` (Ed25519) and `p256` (ECDSA P-256).
13///
14/// Offloads CPU-bound operations to Tokio's blocking pool via
15/// `spawn_blocking` to prevent async reactor starvation under load.
16///
17/// Usage:
18/// ```ignore
19/// use auths_crypto::{CryptoProvider, RingCryptoProvider};
20///
21/// let provider = RingCryptoProvider;
22/// provider.verify_ed25519(&pubkey, &msg, &sig).await.unwrap();
23/// ```
24pub struct RingCryptoProvider;
25
26impl RingCryptoProvider {
27    /// Generate a P-256 keypair. Returns (seed, compressed_public_key).
28    pub fn p256_generate() -> Result<(SecureSeed, Vec<u8>), CryptoError> {
29        use p256::ecdsa::SigningKey;
30        use p256::elliptic_curve::rand_core::OsRng;
31
32        let signing_key = SigningKey::random(&mut OsRng);
33        let verifying_key = p256::ecdsa::VerifyingKey::from(&signing_key);
34
35        // Compressed SEC1 public key (33 bytes: 0x02/0x03 + x-coordinate)
36        let compressed = verifying_key.to_encoded_point(true);
37        let pubkey_bytes = compressed.as_bytes().to_vec();
38
39        // Extract the raw 32-byte scalar as the seed
40        let scalar_bytes = signing_key.to_bytes();
41        let mut seed = [0u8; 32];
42        seed.copy_from_slice(&scalar_bytes);
43
44        Ok((SecureSeed::new(seed), pubkey_bytes))
45    }
46
47    /// Sign with P-256. Returns 64-byte raw r||s.
48    pub fn p256_sign(seed: &[u8; 32], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
49        use p256::ecdsa::{SigningKey, signature::Signer};
50
51        let signing_key = SigningKey::from_slice(seed)
52            .map_err(|e| CryptoError::InvalidPrivateKey(format!("P-256: {e}")))?;
53
54        let signature: p256::ecdsa::Signature = signing_key.sign(message);
55        Ok(signature.to_bytes().to_vec())
56    }
57
58    /// Verify an Ed25519 signature synchronously. Pubkey must be 32 raw bytes.
59    pub fn ed25519_verify(
60        pubkey: &[u8],
61        message: &[u8],
62        signature: &[u8],
63    ) -> Result<(), CryptoError> {
64        // INVARIANT: sanctioned ring usage inside auths-crypto per the crate's
65        // permanent `#![allow(clippy::disallowed_methods)]` carve-out.
66        let pk = ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, pubkey);
67        pk.verify(message, signature)
68            .map_err(|_| CryptoError::InvalidSignature)
69    }
70
71    /// Sign with Ed25519 synchronously. Returns 64-byte raw signature.
72    ///
73    /// Exists so `key_ops::sign` (the sync curve-agnostic dispatcher) can route
74    /// through a single cfg-swappable provider entry point without taking the
75    /// spawn-blocking cost the trait path pays. When FIPS/CNSA swap in, this
76    /// inherent method is the one line that changes.
77    pub fn ed25519_sign(seed: &[u8; 32], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
78        let kp = Ed25519KeyPair::from_seed_unchecked(seed)
79            .map_err(|e| CryptoError::InvalidPrivateKey(format!("Ed25519: {e}")))?;
80        Ok(kp.sign(message).as_ref().to_vec())
81    }
82
83    /// Derive 32-byte Ed25519 public key from a raw seed, synchronously.
84    pub fn ed25519_public_key(seed: &[u8; 32]) -> Result<[u8; 32], CryptoError> {
85        let kp = Ed25519KeyPair::from_seed_unchecked(seed)
86            .map_err(|e| CryptoError::OperationFailed(format!("Ed25519 pubkey: {e}")))?;
87        kp.public_key()
88            .as_ref()
89            .try_into()
90            .map_err(|_| CryptoError::OperationFailed("Ed25519 public key not 32 bytes".into()))
91    }
92
93    /// Verify a P-256 signature. Accepts 33-byte compressed or 65-byte uncompressed pubkey.
94    pub fn p256_verify(pubkey: &[u8], message: &[u8], signature: &[u8]) -> Result<(), CryptoError> {
95        use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
96
97        let vk = VerifyingKey::from_sec1_bytes(pubkey)
98            .map_err(|e| CryptoError::OperationFailed(format!("P-256 key parse: {e}")))?;
99
100        let sig = Signature::from_slice(signature)
101            .map_err(|e| CryptoError::OperationFailed(format!("P-256 sig parse: {e}")))?;
102
103        vk.verify(message, &sig)
104            .map_err(|_| CryptoError::InvalidSignature)
105    }
106
107    /// Derive compressed public key from P-256 seed.
108    /// Generate an Ed25519 keypair. Returns `(seed, public_key)`.
109    pub fn ed25519_generate() -> Result<(SecureSeed, [u8; 32]), CryptoError> {
110        use p256::elliptic_curve::rand_core::{OsRng, RngCore};
111        let mut seed = [0u8; 32];
112        OsRng.fill_bytes(&mut seed);
113        let public_key = Self::ed25519_public_key(&seed)?;
114        Ok((SecureSeed::new(seed), public_key))
115    }
116
117    pub fn p256_public_key_from_seed(seed: &[u8; 32]) -> Result<Vec<u8>, CryptoError> {
118        use p256::ecdsa::SigningKey;
119
120        let signing_key = SigningKey::from_slice(seed)
121            .map_err(|e| CryptoError::InvalidPrivateKey(format!("P-256: {e}")))?;
122        let verifying_key = p256::ecdsa::VerifyingKey::from(&signing_key);
123        let compressed = verifying_key.to_encoded_point(true);
124        Ok(compressed.as_bytes().to_vec())
125    }
126}
127
128#[async_trait]
129impl CryptoProvider for RingCryptoProvider {
130    async fn verify_p256(
131        &self,
132        pubkey: &[u8],
133        message: &[u8],
134        signature: &[u8],
135    ) -> Result<(), CryptoError> {
136        Self::p256_verify(pubkey, message, signature)
137    }
138
139    async fn verify_ed25519(
140        &self,
141        pubkey: &[u8],
142        message: &[u8],
143        signature: &[u8],
144    ) -> Result<(), CryptoError> {
145        if pubkey.len() != ED25519_PUBLIC_KEY_LEN {
146            return Err(CryptoError::InvalidKeyLength {
147                expected: ED25519_PUBLIC_KEY_LEN,
148                actual: pubkey.len(),
149            });
150        }
151
152        let pubkey = pubkey.to_vec();
153        let message = message.to_vec();
154        let signature = signature.to_vec();
155
156        tokio::task::spawn_blocking(move || {
157            let peer_public_key = UnparsedPublicKey::new(&ED25519, &pubkey);
158            peer_public_key
159                .verify(&message, &signature)
160                .map_err(|_| CryptoError::InvalidSignature)
161        })
162        .await
163        .map_err(|_| CryptoError::OperationFailed("Verification task panicked".into()))?
164    }
165
166    async fn sign_ed25519(
167        &self,
168        seed: &SecureSeed,
169        message: &[u8],
170    ) -> Result<Vec<u8>, CryptoError> {
171        let seed_bytes = *seed.as_bytes();
172        let message = message.to_vec();
173
174        // Keypair is re-materialized from the raw seed on each call.
175        // This trades minor CPU overhead for a pure, ring-free domain layer.
176        tokio::task::spawn_blocking(move || {
177            let keypair = Ed25519KeyPair::from_seed_unchecked(&seed_bytes)
178                .map_err(|e| CryptoError::InvalidPrivateKey(format!("{e}")))?;
179            Ok(keypair.sign(&message).as_ref().to_vec())
180        })
181        .await
182        .map_err(|_| CryptoError::OperationFailed("Signing task panicked".into()))?
183    }
184
185    async fn generate_ed25519_keypair(&self) -> Result<(SecureSeed, [u8; 32]), CryptoError> {
186        tokio::task::spawn_blocking(move || {
187            let rng = SystemRandom::new();
188            let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng)
189                .map_err(|_| CryptoError::OperationFailed("Key generation failed".into()))?;
190            let keypair = Ed25519KeyPair::from_pkcs8(pkcs8_doc.as_ref())
191                .map_err(|e| CryptoError::OperationFailed(format!("Parse generated key: {e}")))?;
192
193            let public_key: [u8; 32] = keypair
194                .public_key()
195                .as_ref()
196                .try_into()
197                .map_err(|_| CryptoError::OperationFailed("Public key not 32 bytes".into()))?;
198
199            // Extract the raw 32-byte seed from the PKCS#8 DER encoding.
200            // Ring's Ed25519 PKCS#8 v2 places the seed at bytes [16..48].
201            let pkcs8_bytes = pkcs8_doc.as_ref();
202            let seed: [u8; 32] = pkcs8_bytes[16..48]
203                .try_into()
204                .map_err(|_| CryptoError::OperationFailed("Seed extraction failed".into()))?;
205
206            Ok((SecureSeed::new(seed), public_key))
207        })
208        .await
209        .map_err(|_| CryptoError::OperationFailed("Keygen task panicked".into()))?
210    }
211
212    async fn ed25519_public_key_from_seed(
213        &self,
214        seed: &SecureSeed,
215    ) -> Result<[u8; 32], CryptoError> {
216        let seed_bytes = *seed.as_bytes();
217
218        tokio::task::spawn_blocking(move || {
219            let keypair = Ed25519KeyPair::from_seed_unchecked(&seed_bytes)
220                .map_err(|e| CryptoError::InvalidPrivateKey(format!("{e}")))?;
221            keypair
222                .public_key()
223                .as_ref()
224                .try_into()
225                .map_err(|_| CryptoError::OperationFailed("Public key not 32 bytes".into()))
226        })
227        .await
228        .map_err(|_| CryptoError::OperationFailed("Public key extraction panicked".into()))?
229    }
230
231    async fn sign_p256(&self, seed: &SecureSeed, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
232        // Sync by design: P-256 sign is ~100µs on modern hardware — cheaper than
233        // a spawn_blocking context switch. Matches the inherent `p256_sign`
234        // path at crates/auths-crypto/src/ring_provider.rs:48-56.
235        Self::p256_sign(seed.as_bytes(), message)
236    }
237
238    async fn generate_p256_keypair(&self) -> Result<(SecureSeed, Vec<u8>), CryptoError> {
239        Self::p256_generate()
240    }
241
242    async fn p256_public_key_from_seed(&self, seed: &SecureSeed) -> Result<Vec<u8>, CryptoError> {
243        Self::p256_public_key_from_seed(seed.as_bytes())
244    }
245
246    async fn aead_encrypt(
247        &self,
248        key: &[u8; 32],
249        nonce: &[u8; 12],
250        aad: &[u8],
251        plaintext: &[u8],
252    ) -> Result<Vec<u8>, CryptoError> {
253        use chacha20poly1305::{
254            ChaCha20Poly1305, Nonce,
255            aead::{Aead, KeyInit, Payload},
256        };
257
258        let cipher = ChaCha20Poly1305::new(key.into());
259        let nonce = Nonce::from_slice(nonce);
260        cipher
261            .encrypt(
262                nonce,
263                Payload {
264                    msg: plaintext,
265                    aad,
266                },
267            )
268            .map_err(|e| CryptoError::OperationFailed(format!("AEAD encrypt: {e}")))
269    }
270
271    async fn aead_decrypt(
272        &self,
273        key: &[u8; 32],
274        nonce: &[u8; 12],
275        aad: &[u8],
276        ciphertext: &[u8],
277    ) -> Result<Vec<u8>, CryptoError> {
278        use chacha20poly1305::{
279            ChaCha20Poly1305, Nonce,
280            aead::{Aead, KeyInit, Payload},
281        };
282
283        let cipher = ChaCha20Poly1305::new(key.into());
284        let nonce = Nonce::from_slice(nonce);
285        cipher
286            .decrypt(
287                nonce,
288                Payload {
289                    msg: ciphertext,
290                    aad,
291                },
292            )
293            // Any AEAD failure (tag mismatch, truncation, AAD mismatch) is an
294            // InvalidSignature — the tag IS the signature over (ct||aad).
295            .map_err(|_| CryptoError::InvalidSignature)
296    }
297
298    async fn hkdf_sha256_expand(
299        &self,
300        ikm: &[u8],
301        salt: &[u8],
302        info: &[u8],
303        out_len: usize,
304    ) -> Result<Vec<u8>, CryptoError> {
305        use hkdf::Hkdf;
306        use sha2::Sha256;
307
308        // RFC 5869 max output: 255 × HashLen. For SHA-256 → 8160 bytes.
309        if out_len > 255 * 32 {
310            return Err(CryptoError::OperationFailed(
311                "HKDF-SHA256 output length exceeds 255 * 32 = 8160 bytes".into(),
312            ));
313        }
314
315        let hk = Hkdf::<Sha256>::new(if salt.is_empty() { None } else { Some(salt) }, ikm);
316        let mut out = vec![0u8; out_len];
317        hk.expand(info, &mut out)
318            .map_err(|e| CryptoError::OperationFailed(format!("HKDF-SHA256 expand: {e}")))?;
319        Ok(out)
320    }
321
322    async fn hkdf_sha384_expand(
323        &self,
324        ikm: &[u8],
325        salt: &[u8],
326        info: &[u8],
327        out_len: usize,
328    ) -> Result<Vec<u8>, CryptoError> {
329        use hkdf::Hkdf;
330        use sha2::Sha384;
331
332        if out_len > 255 * 48 {
333            return Err(CryptoError::OperationFailed(
334                "HKDF-SHA384 output length exceeds 255 * 48 = 12240 bytes".into(),
335            ));
336        }
337
338        let hk = Hkdf::<Sha384>::new(if salt.is_empty() { None } else { Some(salt) }, ikm);
339        let mut out = vec![0u8; out_len];
340        hk.expand(info, &mut out)
341            .map_err(|e| CryptoError::OperationFailed(format!("HKDF-SHA384 expand: {e}")))?;
342        Ok(out)
343    }
344
345    async fn hmac_sha256_compute(&self, key: &[u8], msg: &[u8]) -> Result<[u8; 32], CryptoError> {
346        use hmac::{Hmac, Mac};
347        use sha2::Sha256;
348
349        let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key)
350            .map_err(|e| CryptoError::OperationFailed(format!("HMAC-SHA256 key: {e}")))?;
351        mac.update(msg);
352        let tag = mac.finalize().into_bytes();
353        let out: [u8; 32] = tag.into();
354        Ok(out)
355    }
356
357    async fn hmac_sha256_verify(
358        &self,
359        key: &[u8],
360        msg: &[u8],
361        tag: &[u8],
362    ) -> Result<(), CryptoError> {
363        use hmac::{Hmac, Mac};
364        use sha2::Sha256;
365
366        let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key)
367            .map_err(|e| CryptoError::OperationFailed(format!("HMAC-SHA256 key: {e}")))?;
368        mac.update(msg);
369        // `verify_slice` uses a constant-time comparator internally.
370        mac.verify_slice(tag)
371            .map_err(|_| CryptoError::InvalidSignature)
372    }
373
374    async fn hmac_sha384_compute(&self, key: &[u8], msg: &[u8]) -> Result<[u8; 48], CryptoError> {
375        use hmac::{Hmac, Mac};
376        use sha2::Sha384;
377
378        let mut mac = <Hmac<Sha384> as Mac>::new_from_slice(key)
379            .map_err(|e| CryptoError::OperationFailed(format!("HMAC-SHA384 key: {e}")))?;
380        mac.update(msg);
381        let tag = mac.finalize().into_bytes();
382        let out: [u8; 48] = tag.into();
383        Ok(out)
384    }
385
386    async fn hmac_sha384_verify(
387        &self,
388        key: &[u8],
389        msg: &[u8],
390        tag: &[u8],
391    ) -> Result<(), CryptoError> {
392        use hmac::{Hmac, Mac};
393        use sha2::Sha384;
394
395        let mut mac = <Hmac<Sha384> as Mac>::new_from_slice(key)
396            .map_err(|e| CryptoError::OperationFailed(format!("HMAC-SHA384 key: {e}")))?;
397        mac.update(msg);
398        mac.verify_slice(tag)
399            .map_err(|_| CryptoError::InvalidSignature)
400    }
401}