aion_context/crypto.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Cryptographic primitives for AION v2
3//!
4//! This module provides safe, ergonomic wrappers around battle-tested cryptographic
5//! libraries. All operations follow Tiger Style principles: explicit error handling,
6//! constant-time where applicable, and automatic zeroization of sensitive data.
7//!
8//! # Cryptographic Algorithms
9//!
10//! - **Ed25519**: Digital signatures (RFC 8032, 128-bit security level)
11//! - Used for version signing and author authentication
12//! - Deterministic signatures, no nonce generation issues
13//! - Constant-time operations resistant to timing attacks
14//!
15//! - **ChaCha20-Poly1305**: Authenticated encryption (RFC 8439, 256-bit key)
16//! - AEAD cipher for rules encryption
17//! - Prevents tampering via authentication tag
18//! - Used in TLS 1.3 as mandatory cipher suite
19//!
20//! - **BLAKE3**: Cryptographic hashing (256-bit output)
21//! - 5x faster than SHA-256
22//! - Parallelizable for large data
23//! - Used for content hashing and integrity checks
24//!
25//! - **HKDF**: Key derivation function (RFC 5869, NIST approved)
26//! - HMAC-based key derivation with SHA-256
27//! - Derives multiple keys from master secret
28//! - Context separation via info parameter
29//!
30//! # Security Properties
31//!
32//! - **No panics**: All errors explicitly handled
33//! - **Constant-time**: Ed25519 operations resistant to timing attacks
34//! - **Zeroization**: Signing keys automatically cleared on drop
35//! - **Entropy**: OS CSPRNG for key/nonce generation
36//! - **Standards**: RFC-compliant implementations
37//!
38//! # Usage Examples
39//!
40//! ## Digital Signatures
41//!
42//! ```
43//! use aion_context::crypto::SigningKey;
44//!
45//! // Generate a new signing key
46//! let signing_key = SigningKey::generate();
47//! let message = b"Version 1: Updated fraud rules";
48//!
49//! // Sign a message
50//! let signature = signing_key.sign(message);
51//!
52//! // Verify the signature
53//! let verifying_key = signing_key.verifying_key();
54//! assert!(verifying_key.verify(message, &signature).is_ok());
55//! ```
56//!
57//! ## Authenticated Encryption
58//!
59//! ```
60//! use aion_context::crypto::{generate_nonce, encrypt, decrypt};
61//!
62//! let key = [0u8; 32]; // In production, use proper key derivation
63//! let nonce = generate_nonce();
64//! let plaintext = b"sensitive rules data";
65//! let aad = b"version metadata";
66//!
67//! // Encrypt data
68//! let ciphertext = encrypt(&key, &nonce, plaintext, aad).unwrap();
69//!
70//! // Decrypt data
71//! let recovered = decrypt(&key, &nonce, &ciphertext, aad).unwrap();
72//! assert_eq!(recovered, plaintext);
73//! ```
74//!
75//! ## Hashing
76//!
77//! ```
78//! use aion_context::crypto::{hash, keyed_hash};
79//!
80//! // Content hashing
81//! let data = b"file content";
82//! let content_hash = hash(data);
83//!
84//! // Keyed hashing (MAC)
85//! let key = [0u8; 32];
86//! let mac = keyed_hash(&key, data);
87//! ```
88//!
89//! ## Key Derivation
90//!
91//! ```
92//! use aion_context::crypto::derive_key;
93//!
94//! let master_secret = b"high entropy master key";
95//! let salt = b"unique salt value";
96//! let info = b"encryption-key-v1";
97//!
98//! let mut derived_key = [0u8; 32];
99//! derive_key(master_secret, salt, info, &mut derived_key).unwrap();
100//! // derived_key now contains 32 bytes of derived key material
101//! assert_eq!(derived_key.len(), 32);
102//! ```
103
104use crate::{AionError, Result};
105use ed25519_dalek::{Signature as Ed25519Signature, Signer, Verifier};
106use rand::RngCore;
107use zeroize::Zeroizing;
108
109// Re-export commonly used types
110pub use ed25519_dalek::{SigningKey as Ed25519SigningKey, VerifyingKey as Ed25519VerifyingKey};
111
112/// Signing key for Ed25519 signatures
113///
114/// Automatically zeroized on drop to protect key material.
115///
116/// # Security
117///
118/// - 256-bit private key
119/// - Constant-time operations
120/// - Automatically zeroized when dropped
121/// - **Note**: Implements `Clone` for testing convenience, but cloning key material
122/// increases exposure. Use sparingly in production code.
123///
124/// # Examples
125///
126/// ```
127/// use aion_context::crypto::SigningKey;
128///
129/// let key = SigningKey::generate();
130/// let message = b"test message";
131/// let signature = key.sign(message);
132/// ```
133#[derive(Clone)]
134pub struct SigningKey(Zeroizing<[u8; 32]>);
135
136impl SigningKey {
137 /// Generate a new random signing key using OS entropy
138 ///
139 /// Uses the operating system's cryptographically secure random number
140 /// generator (`/dev/urandom` on Unix, `CryptGenRandom` on Windows).
141 #[must_use]
142 pub fn generate() -> Self {
143 let key = Ed25519SigningKey::generate(&mut rand::rngs::OsRng);
144 Self(Zeroizing::new(key.to_bytes()))
145 }
146
147 /// Create a signing key from bytes
148 ///
149 /// # Errors
150 ///
151 /// Returns `AionError::InvalidPrivateKey` if the bytes are not a valid Ed25519 private key
152 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
153 if bytes.len() != 32 {
154 return Err(AionError::InvalidPrivateKey {
155 reason: format!("expected 32 bytes, got {}", bytes.len()),
156 });
157 }
158
159 let mut key_bytes = [0u8; 32];
160 key_bytes.copy_from_slice(bytes);
161
162 // Validate the key by trying to create an Ed25519SigningKey
163 let _validate = Ed25519SigningKey::from_bytes(&key_bytes);
164
165 Ok(Self(Zeroizing::new(key_bytes)))
166 }
167
168 /// Get the bytes of this signing key
169 ///
170 /// Returns a reference to the key bytes
171 #[must_use]
172 pub fn to_bytes(&self) -> &[u8; 32] {
173 &self.0
174 }
175
176 /// Sign a message
177 ///
178 /// Creates an Ed25519 signature over the message using this key.
179 ///
180 /// # Examples
181 ///
182 /// ```
183 /// use aion_context::crypto::SigningKey;
184 ///
185 /// let key = SigningKey::generate();
186 /// let signature = key.sign(b"message");
187 /// assert_eq!(signature.len(), 64);
188 /// ```
189 #[must_use]
190 pub fn sign(&self, message: &[u8]) -> [u8; 64] {
191 let signing_key = Ed25519SigningKey::from_bytes(&self.0);
192 signing_key.sign(message).to_bytes()
193 }
194
195 /// Get the corresponding verifying key
196 #[must_use]
197 pub fn verifying_key(&self) -> VerifyingKey {
198 let signing_key = Ed25519SigningKey::from_bytes(&self.0);
199 VerifyingKey(signing_key.verifying_key())
200 }
201}
202
203/// Verifying key for Ed25519 signatures
204///
205/// Used to verify signatures created by the corresponding `SigningKey`.
206///
207/// # Examples
208///
209/// ```
210/// use aion_context::crypto::{SigningKey, VerifyingKey};
211///
212/// let signing_key = SigningKey::generate();
213/// let verifying_key = signing_key.verifying_key();
214///
215/// let message = b"test";
216/// let signature = signing_key.sign(message);
217///
218/// assert!(verifying_key.verify(message, &signature).is_ok());
219/// ```
220#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub struct VerifyingKey(Ed25519VerifyingKey);
222
223impl VerifyingKey {
224 /// Create a verifying key from bytes
225 ///
226 /// # Errors
227 ///
228 /// Returns `AionError::InvalidPublicKey` if the bytes are not a valid Ed25519 public key
229 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
230 if bytes.len() != 32 {
231 return Err(AionError::InvalidPublicKey {
232 reason: format!("expected 32 bytes, got {}", bytes.len()),
233 });
234 }
235
236 let mut key_bytes = [0u8; 32];
237 key_bytes.copy_from_slice(bytes);
238
239 let key = Ed25519VerifyingKey::from_bytes(&key_bytes).map_err(|e| {
240 AionError::InvalidPublicKey {
241 reason: e.to_string(),
242 }
243 })?;
244
245 Ok(Self(key))
246 }
247
248 /// Get the bytes of this verifying key
249 #[must_use]
250 pub fn to_bytes(&self) -> [u8; 32] {
251 self.0.to_bytes()
252 }
253
254 /// Verify a signature on a message
255 ///
256 /// # Errors
257 ///
258 /// Returns `AionError::InvalidSignature` if the signature is invalid or doesn't match the message
259 pub fn verify(&self, message: &[u8], signature: &[u8; 64]) -> Result<()> {
260 let sig = Ed25519Signature::from_bytes(signature);
261
262 self.0
263 .verify(message, &sig)
264 .map_err(|_| AionError::InvalidSignature {
265 reason: "signature verification failed".to_string(),
266 })
267 }
268}
269
270/// Hash a message using BLAKE3
271///
272/// Returns a 32-byte (256-bit) cryptographic hash.
273///
274/// # Examples
275///
276/// ```
277/// use aion_context::crypto::hash;
278///
279/// let hash = hash(b"Hello, AION!");
280/// assert_eq!(hash.len(), 32);
281/// ```
282#[must_use]
283pub fn hash(data: &[u8]) -> [u8; 32] {
284 blake3::hash(data).into()
285}
286
287/// Keyed hash using BLAKE3
288///
289/// Provides message authentication (MAC) using a secret key.
290///
291/// # Examples
292///
293/// ```
294/// use aion_context::crypto::keyed_hash;
295///
296/// let key = [0u8; 32];
297/// let mac = keyed_hash(&key, b"message");
298/// assert_eq!(mac.len(), 32);
299/// ```
300#[must_use]
301pub fn keyed_hash(key: &[u8; 32], data: &[u8]) -> [u8; 32] {
302 blake3::keyed_hash(key, data).into()
303}
304
305/// Derive a key using HKDF-SHA256
306///
307/// Extracts entropy from input key material and expands it into a derived key.
308///
309/// # Arguments
310///
311/// * `ikm` - Input key material
312/// * `salt` - Optional salt value (use empty slice if none)
313/// * `info` - Context and application-specific information
314/// * `output` - Buffer to fill with derived key material
315///
316/// # Examples
317///
318/// ```
319/// use aion_context::crypto::derive_key;
320///
321/// let ikm = b"input key material";
322/// let salt = b"optional salt";
323/// let info = b"application context";
324/// let mut output = [0u8; 32];
325///
326/// derive_key(ikm, salt, info, &mut output).unwrap();
327/// ```
328///
329/// # Errors
330///
331/// Returns `AionError::InvalidPrivateKey` if key derivation fails (should never happen with valid inputs)
332pub fn derive_key(ikm: &[u8], salt: &[u8], info: &[u8], output: &mut [u8]) -> Result<()> {
333 use hkdf::Hkdf;
334 use sha2::Sha256;
335
336 let hk = Hkdf::<Sha256>::new(Some(salt), ikm);
337
338 hk.expand(info, output)
339 .map_err(|_| AionError::InvalidPrivateKey {
340 reason: "HKDF expand failed".to_string(),
341 })?;
342
343 Ok(())
344}
345
346/// Encrypt data using ChaCha20-Poly1305 (AEAD)
347///
348/// # Arguments
349///
350/// * `key` - 32-byte encryption key
351/// * `nonce` - 12-byte nonce (MUST be unique for each encryption with the same key)
352/// * `plaintext` - Data to encrypt
353/// * `aad` - Additional authenticated data (not encrypted, but authenticated)
354///
355/// # Returns
356///
357/// Ciphertext with authentication tag appended (`plaintext.len()` + 16 bytes)
358///
359/// # Errors
360///
361/// Returns `AionError::EncryptionFailed` if encryption fails
362///
363/// # Security
364///
365/// **CRITICAL**: Never reuse a nonce with the same key. Use `generate_nonce()` for each encryption.
366pub fn encrypt(key: &[u8; 32], nonce: &[u8; 12], plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
367 use chacha20poly1305::{
368 aead::{Aead, KeyInit, Payload},
369 ChaCha20Poly1305,
370 };
371
372 let cipher = ChaCha20Poly1305::new(key.into());
373 let payload = Payload {
374 msg: plaintext,
375 aad,
376 };
377
378 cipher
379 .encrypt(nonce.into(), payload)
380 .map_err(|e| AionError::EncryptionFailed {
381 reason: e.to_string(),
382 })
383}
384
385/// Decrypt data using ChaCha20-Poly1305 (AEAD)
386///
387/// # Arguments
388///
389/// * `key` - 32-byte encryption key (same as used for encryption)
390/// * `nonce` - 12-byte nonce (same as used for encryption)
391/// * `ciphertext` - Encrypted data with authentication tag
392/// * `aad` - Additional authenticated data (must match encryption)
393///
394/// # Returns
395///
396/// Decrypted plaintext
397///
398/// # Errors
399///
400/// Returns `AionError::DecryptionFailed` if:
401/// - Authentication tag is invalid (data was tampered with)
402/// - Wrong key or nonce used
403/// - AAD doesn't match
404pub fn decrypt(key: &[u8; 32], nonce: &[u8; 12], ciphertext: &[u8], aad: &[u8]) -> Result<Vec<u8>> {
405 use chacha20poly1305::{
406 aead::{Aead, KeyInit, Payload},
407 ChaCha20Poly1305,
408 };
409
410 let cipher = ChaCha20Poly1305::new(key.into());
411 let payload = Payload {
412 msg: ciphertext,
413 aad,
414 };
415
416 cipher
417 .decrypt(nonce.into(), payload)
418 .map_err(|e| AionError::DecryptionFailed {
419 reason: e.to_string(),
420 })
421}
422
423/// Generate a random nonce for ChaCha20-Poly1305
424///
425/// Uses OS entropy to generate a cryptographically secure 12-byte nonce.
426///
427/// # Examples
428///
429/// ```
430/// use aion_context::crypto::generate_nonce;
431///
432/// let nonce = generate_nonce();
433/// assert_eq!(nonce.len(), 12);
434/// ```
435#[must_use]
436pub fn generate_nonce() -> [u8; 12] {
437 let mut nonce = [0u8; 12];
438 rand::rngs::OsRng.fill_bytes(&mut nonce);
439 nonce
440}
441
442#[cfg(test)]
443#[allow(clippy::unwrap_used)] // Tests are allowed to panic
444mod tests {
445 use super::*;
446
447 mod signing {
448 use super::*;
449
450 #[test]
451 fn should_generate_signing_key() {
452 let key = SigningKey::generate();
453 let bytes = key.to_bytes();
454 assert_eq!(bytes.len(), 32);
455 }
456
457 #[test]
458 fn should_create_signing_key_from_bytes() {
459 let original = SigningKey::generate();
460 let bytes = *original.to_bytes();
461
462 let restored = SigningKey::from_bytes(&bytes).unwrap();
463 assert_eq!(*original.to_bytes(), *restored.to_bytes());
464 }
465
466 #[test]
467 fn should_reject_invalid_key_length() {
468 let result = SigningKey::from_bytes(&[0u8; 16]);
469 assert!(result.is_err());
470 }
471
472 #[test]
473 fn should_sign_message() {
474 let key = SigningKey::generate();
475 let signature = key.sign(b"test message");
476 assert_eq!(signature.len(), 64);
477 }
478
479 #[test]
480 fn should_verify_valid_signature() {
481 let key = SigningKey::generate();
482 let message = b"test message";
483 let signature = key.sign(message);
484
485 let verifying_key = key.verifying_key();
486 assert!(verifying_key.verify(message, &signature).is_ok());
487 }
488
489 #[test]
490 fn should_reject_invalid_signature() {
491 let key = SigningKey::generate();
492 let message = b"test message";
493 let mut signature = key.sign(message);
494
495 // Tamper with signature
496 signature[0] ^= 1;
497
498 let verifying_key = key.verifying_key();
499 assert!(verifying_key.verify(message, &signature).is_err());
500 }
501
502 #[test]
503 fn should_reject_wrong_message() {
504 let key = SigningKey::generate();
505 let signature = key.sign(b"original message");
506
507 let verifying_key = key.verifying_key();
508 assert!(verifying_key
509 .verify(b"different message", &signature)
510 .is_err());
511 }
512
513 #[test]
514 fn should_serialize_verifying_key() {
515 let key = SigningKey::generate();
516 let verifying_key = key.verifying_key();
517 let bytes = verifying_key.to_bytes();
518 assert_eq!(bytes.len(), 32);
519
520 let restored = VerifyingKey::from_bytes(&bytes).unwrap();
521 assert_eq!(verifying_key.to_bytes(), restored.to_bytes());
522 }
523 }
524
525 mod hashing {
526 use super::*;
527
528 #[test]
529 fn should_hash_data() {
530 let hash1 = hash(b"test data");
531 assert_eq!(hash1.len(), 32);
532
533 // Same input produces same hash
534 let hash2 = hash(b"test data");
535 assert_eq!(hash1, hash2);
536 }
537
538 #[test]
539 fn should_produce_different_hashes_for_different_data() {
540 let hash1 = hash(b"data1");
541 let hash2 = hash(b"data2");
542 assert_ne!(hash1, hash2);
543 }
544
545 #[test]
546 fn should_create_keyed_hash() {
547 let key = [0u8; 32];
548 let mac = keyed_hash(&key, b"message");
549 assert_eq!(mac.len(), 32);
550 }
551
552 #[test]
553 fn should_produce_different_macs_with_different_keys() {
554 let key1 = [0u8; 32];
555 let key2 = [1u8; 32];
556
557 let mac1 = keyed_hash(&key1, b"message");
558 let mac2 = keyed_hash(&key2, b"message");
559 assert_ne!(mac1, mac2);
560 }
561 }
562
563 mod key_derivation {
564 use super::*;
565
566 #[test]
567 fn should_derive_key() {
568 let ikm = b"input key material";
569 let salt = b"salt";
570 let info = b"context";
571 let mut output = [0u8; 32];
572
573 derive_key(ikm, salt, info, &mut output).unwrap();
574
575 // Output should not be all zeros
576 assert_ne!(output, [0u8; 32]);
577 }
578
579 #[test]
580 fn should_produce_deterministic_output() {
581 let ikm = b"input key material";
582 let salt = b"salt";
583 let info = b"context";
584
585 let mut output1 = [0u8; 32];
586 derive_key(ikm, salt, info, &mut output1).unwrap();
587
588 let mut output2 = [0u8; 32];
589 derive_key(ikm, salt, info, &mut output2).unwrap();
590
591 assert_eq!(output1, output2);
592 }
593
594 #[test]
595 fn should_produce_different_keys_for_different_info() {
596 let ikm = b"input key material";
597 let salt = b"salt";
598
599 let mut output1 = [0u8; 32];
600 derive_key(ikm, salt, b"context1", &mut output1).unwrap();
601
602 let mut output2 = [0u8; 32];
603 derive_key(ikm, salt, b"context2", &mut output2).unwrap();
604
605 assert_ne!(output1, output2);
606 }
607 }
608
609 mod encryption {
610 use super::*;
611
612 #[test]
613 fn should_encrypt_and_decrypt() {
614 let key = [0u8; 32];
615 let nonce = generate_nonce();
616 let plaintext = b"secret message";
617 let aad = b"additional data";
618
619 let ciphertext = encrypt(&key, &nonce, plaintext, aad).unwrap();
620 assert_eq!(ciphertext.len(), plaintext.len() + 16); // +16 for auth tag
621
622 let decrypted = decrypt(&key, &nonce, &ciphertext, aad).unwrap();
623 assert_eq!(decrypted, plaintext);
624 }
625
626 #[test]
627 fn should_reject_tampered_ciphertext() {
628 let key = [0u8; 32];
629 let nonce = generate_nonce();
630 let plaintext = b"secret message";
631 let aad = b"additional data";
632
633 let mut ciphertext = encrypt(&key, &nonce, plaintext, aad).unwrap();
634
635 // Tamper with ciphertext
636 if let Some(byte) = ciphertext.get_mut(0) {
637 *byte ^= 1;
638 }
639
640 let result = decrypt(&key, &nonce, &ciphertext, aad);
641 assert!(result.is_err());
642 }
643
644 #[test]
645 fn should_reject_wrong_aad() {
646 let key = [0u8; 32];
647 let nonce = generate_nonce();
648 let plaintext = b"secret message";
649
650 let ciphertext = encrypt(&key, &nonce, plaintext, b"aad1").unwrap();
651
652 let result = decrypt(&key, &nonce, &ciphertext, b"aad2");
653 assert!(result.is_err());
654 }
655
656 #[test]
657 fn should_reject_wrong_key() {
658 let key1 = [0u8; 32];
659 let key2 = [1u8; 32];
660 let nonce = generate_nonce();
661 let plaintext = b"secret message";
662 let aad = b"additional data";
663
664 let ciphertext = encrypt(&key1, &nonce, plaintext, aad).unwrap();
665
666 let result = decrypt(&key2, &nonce, &ciphertext, aad);
667 assert!(result.is_err());
668 }
669
670 #[test]
671 fn should_generate_unique_nonces() {
672 let nonce1 = generate_nonce();
673 let nonce2 = generate_nonce();
674 assert_ne!(nonce1, nonce2);
675 }
676 }
677
678 mod properties {
679 use super::*;
680 use hegel::generators as gs;
681
682 #[hegel::test]
683 fn prop_sign_verify_roundtrip(tc: hegel::TestCase) {
684 let message = tc.draw(gs::binary().max_size(4096));
685 let key = SigningKey::generate();
686 let signature = key.sign(&message);
687 let verifying_key = key.verifying_key();
688 assert!(verifying_key.verify(&message, &signature).is_ok());
689 }
690
691 #[hegel::test]
692 fn prop_verify_rejects_wrong_key(tc: hegel::TestCase) {
693 let message = tc.draw(gs::binary().max_size(4096));
694 let signer = SigningKey::generate();
695 let other = SigningKey::generate();
696 let signature = signer.sign(&message);
697 assert!(other.verifying_key().verify(&message, &signature).is_err());
698 }
699
700 #[hegel::test]
701 fn prop_verify_rejects_tampered_message(tc: hegel::TestCase) {
702 let mut message = tc.draw(gs::binary().min_size(1).max_size(4096));
703 let key = SigningKey::generate();
704 let signature = key.sign(&message);
705 let max = message
706 .len()
707 .checked_sub(1)
708 .unwrap_or_else(|| std::process::abort());
709 let flip_index = tc.draw(gs::integers::<usize>().max_value(max));
710 if let Some(byte) = message.get_mut(flip_index) {
711 *byte ^= 0x01;
712 }
713 assert!(key.verifying_key().verify(&message, &signature).is_err());
714 }
715
716 #[hegel::test]
717 fn prop_hash_is_deterministic(tc: hegel::TestCase) {
718 let data = tc.draw(gs::binary().max_size(8192));
719 assert_eq!(hash(&data), hash(&data));
720 }
721
722 #[hegel::test]
723 fn prop_verifying_key_roundtrip_verifies(tc: hegel::TestCase) {
724 let message = tc.draw(gs::binary().max_size(4096));
725 let signer = SigningKey::generate();
726 let original = signer.verifying_key();
727 let restored = VerifyingKey::from_bytes(&original.to_bytes())
728 .unwrap_or_else(|_| std::process::abort());
729 let signature = signer.sign(&message);
730 assert!(restored.verify(&message, &signature).is_ok());
731 }
732 }
733}