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    pub fn p256_public_key_from_seed(seed: &[u8; 32]) -> Result<Vec<u8>, CryptoError> {
109        use p256::ecdsa::SigningKey;
110
111        let signing_key = SigningKey::from_slice(seed)
112            .map_err(|e| CryptoError::InvalidPrivateKey(format!("P-256: {e}")))?;
113        let verifying_key = p256::ecdsa::VerifyingKey::from(&signing_key);
114        let compressed = verifying_key.to_encoded_point(true);
115        Ok(compressed.as_bytes().to_vec())
116    }
117}
118
119#[async_trait]
120impl CryptoProvider for RingCryptoProvider {
121    async fn verify_p256(
122        &self,
123        pubkey: &[u8],
124        message: &[u8],
125        signature: &[u8],
126    ) -> Result<(), CryptoError> {
127        Self::p256_verify(pubkey, message, signature)
128    }
129
130    async fn verify_ed25519(
131        &self,
132        pubkey: &[u8],
133        message: &[u8],
134        signature: &[u8],
135    ) -> Result<(), CryptoError> {
136        if pubkey.len() != ED25519_PUBLIC_KEY_LEN {
137            return Err(CryptoError::InvalidKeyLength {
138                expected: ED25519_PUBLIC_KEY_LEN,
139                actual: pubkey.len(),
140            });
141        }
142
143        let pubkey = pubkey.to_vec();
144        let message = message.to_vec();
145        let signature = signature.to_vec();
146
147        tokio::task::spawn_blocking(move || {
148            let peer_public_key = UnparsedPublicKey::new(&ED25519, &pubkey);
149            peer_public_key
150                .verify(&message, &signature)
151                .map_err(|_| CryptoError::InvalidSignature)
152        })
153        .await
154        .map_err(|_| CryptoError::OperationFailed("Verification task panicked".into()))?
155    }
156
157    async fn sign_ed25519(
158        &self,
159        seed: &SecureSeed,
160        message: &[u8],
161    ) -> Result<Vec<u8>, CryptoError> {
162        let seed_bytes = *seed.as_bytes();
163        let message = message.to_vec();
164
165        // Keypair is re-materialized from the raw seed on each call.
166        // This trades minor CPU overhead for a pure, ring-free domain layer.
167        tokio::task::spawn_blocking(move || {
168            let keypair = Ed25519KeyPair::from_seed_unchecked(&seed_bytes)
169                .map_err(|e| CryptoError::InvalidPrivateKey(format!("{e}")))?;
170            Ok(keypair.sign(&message).as_ref().to_vec())
171        })
172        .await
173        .map_err(|_| CryptoError::OperationFailed("Signing task panicked".into()))?
174    }
175
176    async fn generate_ed25519_keypair(&self) -> Result<(SecureSeed, [u8; 32]), CryptoError> {
177        tokio::task::spawn_blocking(move || {
178            let rng = SystemRandom::new();
179            let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng)
180                .map_err(|_| CryptoError::OperationFailed("Key generation failed".into()))?;
181            let keypair = Ed25519KeyPair::from_pkcs8(pkcs8_doc.as_ref())
182                .map_err(|e| CryptoError::OperationFailed(format!("Parse generated key: {e}")))?;
183
184            let public_key: [u8; 32] = keypair
185                .public_key()
186                .as_ref()
187                .try_into()
188                .map_err(|_| CryptoError::OperationFailed("Public key not 32 bytes".into()))?;
189
190            // Extract the raw 32-byte seed from the PKCS#8 DER encoding.
191            // Ring's Ed25519 PKCS#8 v2 places the seed at bytes [16..48].
192            let pkcs8_bytes = pkcs8_doc.as_ref();
193            let seed: [u8; 32] = pkcs8_bytes[16..48]
194                .try_into()
195                .map_err(|_| CryptoError::OperationFailed("Seed extraction failed".into()))?;
196
197            Ok((SecureSeed::new(seed), public_key))
198        })
199        .await
200        .map_err(|_| CryptoError::OperationFailed("Keygen task panicked".into()))?
201    }
202
203    async fn ed25519_public_key_from_seed(
204        &self,
205        seed: &SecureSeed,
206    ) -> Result<[u8; 32], CryptoError> {
207        let seed_bytes = *seed.as_bytes();
208
209        tokio::task::spawn_blocking(move || {
210            let keypair = Ed25519KeyPair::from_seed_unchecked(&seed_bytes)
211                .map_err(|e| CryptoError::InvalidPrivateKey(format!("{e}")))?;
212            keypair
213                .public_key()
214                .as_ref()
215                .try_into()
216                .map_err(|_| CryptoError::OperationFailed("Public key not 32 bytes".into()))
217        })
218        .await
219        .map_err(|_| CryptoError::OperationFailed("Public key extraction panicked".into()))?
220    }
221
222    async fn sign_p256(&self, seed: &SecureSeed, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
223        // Sync by design: P-256 sign is ~100µs on modern hardware — cheaper than
224        // a spawn_blocking context switch. Matches the inherent `p256_sign`
225        // path at crates/auths-crypto/src/ring_provider.rs:48-56.
226        Self::p256_sign(seed.as_bytes(), message)
227    }
228
229    async fn generate_p256_keypair(&self) -> Result<(SecureSeed, Vec<u8>), CryptoError> {
230        Self::p256_generate()
231    }
232
233    async fn p256_public_key_from_seed(&self, seed: &SecureSeed) -> Result<Vec<u8>, CryptoError> {
234        Self::p256_public_key_from_seed(seed.as_bytes())
235    }
236
237    async fn aead_encrypt(
238        &self,
239        key: &[u8; 32],
240        nonce: &[u8; 12],
241        aad: &[u8],
242        plaintext: &[u8],
243    ) -> Result<Vec<u8>, CryptoError> {
244        use chacha20poly1305::{
245            ChaCha20Poly1305, Nonce,
246            aead::{Aead, KeyInit, Payload},
247        };
248
249        let cipher = ChaCha20Poly1305::new(key.into());
250        let nonce = Nonce::from_slice(nonce);
251        cipher
252            .encrypt(
253                nonce,
254                Payload {
255                    msg: plaintext,
256                    aad,
257                },
258            )
259            .map_err(|e| CryptoError::OperationFailed(format!("AEAD encrypt: {e}")))
260    }
261
262    async fn aead_decrypt(
263        &self,
264        key: &[u8; 32],
265        nonce: &[u8; 12],
266        aad: &[u8],
267        ciphertext: &[u8],
268    ) -> Result<Vec<u8>, CryptoError> {
269        use chacha20poly1305::{
270            ChaCha20Poly1305, Nonce,
271            aead::{Aead, KeyInit, Payload},
272        };
273
274        let cipher = ChaCha20Poly1305::new(key.into());
275        let nonce = Nonce::from_slice(nonce);
276        cipher
277            .decrypt(
278                nonce,
279                Payload {
280                    msg: ciphertext,
281                    aad,
282                },
283            )
284            // Any AEAD failure (tag mismatch, truncation, AAD mismatch) is an
285            // InvalidSignature — the tag IS the signature over (ct||aad).
286            .map_err(|_| CryptoError::InvalidSignature)
287    }
288
289    async fn hkdf_sha256_expand(
290        &self,
291        ikm: &[u8],
292        salt: &[u8],
293        info: &[u8],
294        out_len: usize,
295    ) -> Result<Vec<u8>, CryptoError> {
296        use hkdf::Hkdf;
297        use sha2::Sha256;
298
299        // RFC 5869 max output: 255 × HashLen. For SHA-256 → 8160 bytes.
300        if out_len > 255 * 32 {
301            return Err(CryptoError::OperationFailed(
302                "HKDF-SHA256 output length exceeds 255 * 32 = 8160 bytes".into(),
303            ));
304        }
305
306        let hk = Hkdf::<Sha256>::new(if salt.is_empty() { None } else { Some(salt) }, ikm);
307        let mut out = vec![0u8; out_len];
308        hk.expand(info, &mut out)
309            .map_err(|e| CryptoError::OperationFailed(format!("HKDF-SHA256 expand: {e}")))?;
310        Ok(out)
311    }
312
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        use hkdf::Hkdf;
321        use sha2::Sha384;
322
323        if out_len > 255 * 48 {
324            return Err(CryptoError::OperationFailed(
325                "HKDF-SHA384 output length exceeds 255 * 48 = 12240 bytes".into(),
326            ));
327        }
328
329        let hk = Hkdf::<Sha384>::new(if salt.is_empty() { None } else { Some(salt) }, ikm);
330        let mut out = vec![0u8; out_len];
331        hk.expand(info, &mut out)
332            .map_err(|e| CryptoError::OperationFailed(format!("HKDF-SHA384 expand: {e}")))?;
333        Ok(out)
334    }
335
336    async fn hmac_sha256_compute(&self, key: &[u8], msg: &[u8]) -> Result<[u8; 32], CryptoError> {
337        use hmac::{Hmac, Mac};
338        use sha2::Sha256;
339
340        let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key)
341            .map_err(|e| CryptoError::OperationFailed(format!("HMAC-SHA256 key: {e}")))?;
342        mac.update(msg);
343        let tag = mac.finalize().into_bytes();
344        let out: [u8; 32] = tag.into();
345        Ok(out)
346    }
347
348    async fn hmac_sha256_verify(
349        &self,
350        key: &[u8],
351        msg: &[u8],
352        tag: &[u8],
353    ) -> Result<(), CryptoError> {
354        use hmac::{Hmac, Mac};
355        use sha2::Sha256;
356
357        let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key)
358            .map_err(|e| CryptoError::OperationFailed(format!("HMAC-SHA256 key: {e}")))?;
359        mac.update(msg);
360        // `verify_slice` uses a constant-time comparator internally.
361        mac.verify_slice(tag)
362            .map_err(|_| CryptoError::InvalidSignature)
363    }
364
365    async fn hmac_sha384_compute(&self, key: &[u8], msg: &[u8]) -> Result<[u8; 48], CryptoError> {
366        use hmac::{Hmac, Mac};
367        use sha2::Sha384;
368
369        let mut mac = <Hmac<Sha384> as Mac>::new_from_slice(key)
370            .map_err(|e| CryptoError::OperationFailed(format!("HMAC-SHA384 key: {e}")))?;
371        mac.update(msg);
372        let tag = mac.finalize().into_bytes();
373        let out: [u8; 48] = tag.into();
374        Ok(out)
375    }
376
377    async fn hmac_sha384_verify(
378        &self,
379        key: &[u8],
380        msg: &[u8],
381        tag: &[u8],
382    ) -> Result<(), CryptoError> {
383        use hmac::{Hmac, Mac};
384        use sha2::Sha384;
385
386        let mut mac = <Hmac<Sha384> as Mac>::new_from_slice(key)
387            .map_err(|e| CryptoError::OperationFailed(format!("HMAC-SHA384 key: {e}")))?;
388        mac.update(msg);
389        mac.verify_slice(tag)
390            .map_err(|_| CryptoError::InvalidSignature)
391    }
392}