chains_sdk/security.rs
1//! Security utilities for enclave / confidential computing environments.
2//!
3//! Provides constant-time hex encoding, memory guarding, pluggable RNG,
4//! and secure comparison primitives for use in TEE (SGX, Nitro, TDX, SEV)
5//! environments.
6
7use zeroize::Zeroizing;
8
9// ─── Constant-Time Hex ─────────────────────────────────────────────
10
11/// Constant-time hex encoding for secret material.
12///
13/// Unlike `hex::encode()`, this implementation processes all bytes
14/// uniformly regardless of value, preventing timing side-channels.
15#[must_use]
16pub fn ct_hex_encode(data: &[u8]) -> String {
17 const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
18 let mut result = String::with_capacity(data.len() * 2);
19 for &byte in data {
20 result.push(HEX_CHARS[(byte >> 4) as usize] as char);
21 result.push(HEX_CHARS[(byte & 0x0F) as usize] as char);
22 }
23 result
24}
25
26/// Constant-time hex decoding for secret material.
27///
28/// Returns `None` if the input contains non-hex characters or has odd length.
29/// Processes the full input regardless of validity to avoid timing leaks.
30#[must_use]
31pub fn ct_hex_decode(hex: &str) -> Option<Vec<u8>> {
32 let bytes = hex.as_bytes();
33 // Process all bytes even if odd-length — avoid early return timing leak
34 let odd = bytes.len() % 2;
35 let pair_count = bytes.len() / 2;
36 let mut result = Vec::with_capacity(pair_count);
37 let mut all_valid: u8 = 0xFF;
38
39 for chunk in bytes.chunks(2) {
40 if chunk.len() == 2 {
41 let (high, h_ok) = ct_hex_val(chunk[0]);
42 let (low, l_ok) = ct_hex_val(chunk[1]);
43 all_valid &= h_ok & l_ok;
44 result.push((high << 4) | low);
45 }
46 }
47
48 // Reject odd-length inputs via the flag, not an early return
49 if odd != 0 {
50 all_valid = 0;
51 }
52
53 if all_valid != 0 {
54 Some(result)
55 } else {
56 None
57 }
58}
59
60/// Fully constant-time hex character to value (branchless).
61///
62/// Returns `(value, validity_mask)` where `validity_mask` is `0xFF` if the
63/// character is valid hex and `0x00` otherwise. No branches on data.
64fn ct_hex_val(c: u8) -> (u8, u8) {
65 // Compute all three candidate values unconditionally
66 let digit = c.wrapping_sub(b'0');
67 let upper = c.wrapping_sub(b'A').wrapping_add(10);
68 let lower = c.wrapping_sub(b'a').wrapping_add(10);
69
70 // Create validity masks (0xFF if valid, 0x00 if not) — branchless
71 let digit_valid = ((digit as i8).wrapping_sub(10) >> 7) as u8; // 0xFF if digit < 10
72 let upper_valid = ((upper.wrapping_sub(10) as i8).wrapping_sub(6) >> 7) as u8 & !digit_valid;
73 let lower_valid =
74 ((lower.wrapping_sub(10) as i8).wrapping_sub(6) >> 7) as u8 & !digit_valid & !upper_valid;
75
76 // Select result using masks — no branches on data
77 let result = (digit & digit_valid) | (upper & upper_valid) | (lower & lower_valid);
78 let any_valid = digit_valid | upper_valid | lower_valid;
79
80 (result, any_valid)
81}
82
83// ─── Secure Zero ───────────────────────────────────────────────────
84
85/// Securely zeroize a mutable byte slice using volatile writes.
86///
87/// This ensures the compiler cannot optimize away the zeroization.
88pub fn secure_zero(data: &mut [u8]) {
89 use zeroize::Zeroize;
90 data.zeroize();
91}
92
93/// A guarded memory region that zeroizes on drop.
94///
95/// For enclave environments where sensitive data must be:
96/// 1. Zeroized when no longer needed
97/// 2. Tracked for lifetime management
98/// 3. Protected from accidental copies
99/// 4. Locked in RAM (when `mlock` feature is enabled)
100///
101/// Uses `Box<[u8]>` internally (not `Vec<u8>`) to guarantee the backing
102/// pointer is never invalidated by reallocation — critical for `mlock` safety.
103///
104/// # Example
105/// ```
106/// use chains_sdk::security::GuardedMemory;
107///
108/// let mut guard = GuardedMemory::new(32);
109/// guard.as_mut()[..4].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
110/// // memory is automatically zeroized (and munlocked) when `guard` is dropped
111/// ```
112pub struct GuardedMemory {
113 inner: Zeroizing<Box<[u8]>>,
114}
115
116impl GuardedMemory {
117 /// Allocate a new guarded memory region of `size` bytes (zeroed).
118 ///
119 /// When the `mlock` feature is enabled, the memory is locked into RAM
120 /// to prevent the OS from swapping it to disk.
121 #[must_use]
122 pub fn new(size: usize) -> Self {
123 let boxed: Box<[u8]> = vec![0u8; size].into_boxed_slice();
124 #[cfg(feature = "mlock")]
125 lock_memory(boxed.as_ptr(), boxed.len());
126 Self { inner: Zeroizing::new(boxed) }
127 }
128
129 /// Create from existing data (takes ownership, original is NOT zeroized).
130 ///
131 /// Prefer this over copying into a `new()` buffer when you already
132 /// have the data in a `Vec<u8>`.
133 #[must_use]
134 pub fn from_vec(data: Vec<u8>) -> Self {
135 let boxed: Box<[u8]> = data.into_boxed_slice();
136 #[cfg(feature = "mlock")]
137 lock_memory(boxed.as_ptr(), boxed.len());
138 Self { inner: Zeroizing::new(boxed) }
139 }
140}
141
142impl AsRef<[u8]> for GuardedMemory {
143 /// Immutable access to the guarded bytes.
144 fn as_ref(&self) -> &[u8] {
145 &self.inner
146 }
147}
148
149impl AsMut<[u8]> for GuardedMemory {
150 /// Mutable access to the guarded bytes.
151 fn as_mut(&mut self) -> &mut [u8] {
152 &mut self.inner
153 }
154}
155
156impl Drop for GuardedMemory {
157 #[allow(unsafe_code)]
158 fn drop(&mut self) {
159 // Unlock memory before zeroization (Zeroizing handles the zeroing).
160 // SAFETY: `self.inner` is a `Box<[u8]>` allocated via `into_boxed_slice()`.
161 // Box<[u8]> never reallocates, so the pointer passed to `lock_memory`
162 // in `new()` / `from_vec()` is guaranteed to be the same pointer here.
163 #[cfg(feature = "mlock")]
164 unlock_memory(self.inner.as_ptr(), self.inner.len());
165 }
166}
167
168impl GuardedMemory {
169 /// Length of the guarded region in bytes.
170 #[must_use]
171 pub fn len(&self) -> usize {
172 self.inner.len()
173 }
174
175 /// Whether the guarded region is empty.
176 #[must_use]
177 pub fn is_empty(&self) -> bool {
178 self.inner.is_empty()
179 }
180}
181
182impl core::fmt::Debug for GuardedMemory {
183 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
184 f.debug_struct("GuardedMemory")
185 .field("len", &self.inner.len())
186 .field("data", &"[REDACTED]")
187 .finish()
188 }
189}
190
191// ─── Memory Locking ────────────────────────────────────────────────
192
193/// Lock a memory region to prevent swapping to disk.
194///
195/// Uses `mlock(2)` on Unix systems. Silently ignores errors
196/// (e.g., insufficient `RLIMIT_MEMLOCK` — the security is best-effort).
197#[cfg(feature = "mlock")]
198#[allow(unsafe_code)]
199fn lock_memory(ptr: *const u8, len: usize) {
200 if len > 0 {
201 // SAFETY: `ptr` points to a valid, heap-allocated region of at least
202 // `len` bytes owned by a `Zeroizing<Vec<u8>>`. The `mlock(2)` syscall
203 // is always safe to call on valid memory — it only advises the kernel
204 // to keep the pages resident. Errors (e.g., RLIMIT_MEMLOCK exceeded)
205 // are silently ignored as this is best-effort security.
206 unsafe { libc::mlock(ptr.cast(), len) };
207 }
208}
209
210/// Unlock a previously locked memory region.
211#[cfg(feature = "mlock")]
212#[allow(unsafe_code)]
213fn unlock_memory(ptr: *const u8, len: usize) {
214 if len > 0 {
215 // SAFETY: `ptr` was previously passed to `lock_memory` and points to
216 // a valid heap allocation of at least `len` bytes. `munlock(2)` is
217 // always safe on valid memory and simply reverses the `mlock` advisory.
218 unsafe { libc::munlock(ptr.cast(), len) };
219 }
220}
221
222// ─── Pluggable RNG ─────────────────────────────────────────────────
223
224/// Fill a buffer with cryptographically secure random bytes.
225///
226/// By default, delegates to `getrandom::getrandom()`. In enclave
227/// environments where the default OS RNG is unavailable or untrusted,
228/// use `set_custom_rng()` to provide a hardware TRNG source.
229///
230/// # Errors
231/// Returns an error if the RNG source fails.
232pub fn secure_random(buf: &mut [u8]) -> Result<(), crate::error::SignerError> {
233 #[cfg(not(feature = "custom_rng"))]
234 {
235 getrandom::getrandom(buf)
236 .map_err(|e| crate::error::SignerError::SigningFailed(format!("RNG failed: {e}")))
237 }
238
239 #[cfg(feature = "custom_rng")]
240 {
241 if let Some(f) = CUSTOM_RNG.get() {
242 f(buf)
243 } else {
244 getrandom::getrandom(buf).map_err(|e| {
245 crate::error::SignerError::SigningFailed(format!("RNG failed: {e}"))
246 })
247 }
248 }
249}
250
251/// Custom RNG function type for TEE environments.
252///
253/// Must be `Send + Sync` for use in multi-threaded enclave environments.
254#[cfg(feature = "custom_rng")]
255pub type CustomRngFn =
256 Box<dyn Fn(&mut [u8]) -> Result<(), crate::error::SignerError> + Send + Sync>;
257
258#[cfg(feature = "custom_rng")]
259static CUSTOM_RNG: std::sync::OnceLock<CustomRngFn> = std::sync::OnceLock::new();
260
261/// Set a custom RNG source for enclave environments.
262///
263/// This replaces the default `getrandom` source with a user-provided
264/// function, typically backed by a hardware TRNG (e.g., RDRAND/RDSEED
265/// in SGX, or Nitro's `/dev/nsm`).
266///
267/// This is a global, process-wide setting. It can only be set **once**;
268/// subsequent calls are silently ignored (the first RNG wins).
269///
270/// # Example
271/// ```no_run
272/// # #[cfg(feature = "custom_rng")]
273/// chains_sdk::security::set_custom_rng(Box::new(|buf| {
274/// // Fill from hardware TRNG
275/// // my_enclave_trng_fill(buf);
276/// Ok(())
277/// }));
278/// ```
279#[cfg(feature = "custom_rng")]
280pub fn set_custom_rng(f: CustomRngFn) {
281 let _ = CUSTOM_RNG.set(f);
282}
283
284// ─── Attestation Hooks ─────────────────────────────────────────────
285
286/// Enclave attestation context for remote verification.
287///
288/// Implement this trait to integrate chains-sdk with your enclave's
289/// attestation mechanism (SGX quotes, Nitro attestation documents,
290/// TDX reports, etc.).
291///
292/// # Example
293/// ```no_run
294/// use chains_sdk::security::EnclaveContext;
295/// use chains_sdk::error::SignerError;
296///
297/// struct NitroEnclave;
298///
299/// impl EnclaveContext for NitroEnclave {
300/// fn attest(&self, _user_data: &[u8]) -> Result<Vec<u8>, SignerError> {
301/// // Call /dev/nsm to generate attestation document
302/// Ok(vec![]) // placeholder
303/// }
304/// fn verify_attestation(&self, _doc: &[u8]) -> Result<bool, SignerError> {
305/// // Verify attestation signature chain
306/// Ok(true) // placeholder
307/// }
308/// }
309/// ```
310pub trait EnclaveContext {
311 /// Generate an attestation document/quote binding to `user_data`.
312 ///
313 /// For SGX: EREPORT → quote (via QE)
314 /// For Nitro: NSM attestation document
315 /// For TDX: TD report → quote
316 fn attest(&self, user_data: &[u8]) -> Result<Vec<u8>, crate::error::SignerError>;
317
318 /// Verify an attestation document/quote.
319 ///
320 /// Returns `true` if the attestation is valid and trusted.
321 fn verify_attestation(&self, attestation: &[u8]) -> Result<bool, crate::error::SignerError>;
322
323 /// Seal data for persistent storage within the enclave.
324 ///
325 /// The sealed data can only be unsealed by the same enclave identity.
326 /// Default implementation: passthrough (no sealing).
327 fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>, crate::error::SignerError> {
328 Ok(plaintext.to_vec())
329 }
330
331 /// Unseal previously sealed data.
332 ///
333 /// Default implementation: passthrough (no unsealing).
334 fn unseal(&self, sealed: &[u8]) -> Result<Zeroizing<Vec<u8>>, crate::error::SignerError> {
335 Ok(Zeroizing::new(sealed.to_vec()))
336 }
337}
338
339// ─── Key Rotation ──────────────────────────────────────────────────
340
341/// Atomically rotate a key: generates a new key and zeroizes the old one.
342///
343/// Returns `(new_key, old_public_key)` — the old private key is zeroized.
344///
345/// # Example
346/// ```
347/// use chains_sdk::security::rotate_key;
348/// use chains_sdk::ethereum::EthereumSigner;
349/// use chains_sdk::traits::{KeyPair, Signer};
350///
351/// let old_signer = EthereumSigner::generate().unwrap();
352/// let old_pubkey = old_signer.public_key_bytes();
353///
354/// let (new_signer, returned_pubkey) = rotate_key(old_signer,
355/// || EthereumSigner::generate()
356/// ).unwrap();
357///
358/// // Old public key is preserved, new signer has a different key
359/// assert_eq!(returned_pubkey, old_pubkey);
360/// assert_ne!(new_signer.public_key_bytes(), returned_pubkey);
361/// ```
362pub fn rotate_key<S>(
363 old_signer: S,
364 generate: impl FnOnce() -> Result<S, crate::error::SignerError>,
365) -> Result<(S, Vec<u8>), crate::error::SignerError>
366where
367 S: crate::traits::Signer + crate::traits::KeyPair,
368{
369 // Capture old public key before dropping
370 let old_pubkey = crate::traits::Signer::public_key_bytes(&old_signer);
371
372 // Drop old signer — Zeroizing ensures key material is wiped
373 drop(old_signer);
374
375 // Generate new key
376 let new_signer = generate()?;
377
378 Ok((new_signer, old_pubkey))
379}
380
381/// Rotate a key using a specific seed (deterministic rotation).
382///
383/// Useful when the new key must be derived from specific entropy
384/// (e.g., from an HSM or TRNG source).
385pub fn rotate_key_with_seed<S>(
386 old_signer: S,
387 new_seed: &[u8],
388) -> Result<(S, Vec<u8>), crate::error::SignerError>
389where
390 S: crate::traits::Signer<Error = crate::error::SignerError> + crate::traits::KeyPair,
391{
392 let old_pubkey = crate::traits::Signer::public_key_bytes(&old_signer);
393 drop(old_signer);
394 let new_signer = S::from_bytes(new_seed)?;
395 Ok((new_signer, old_pubkey))
396}
397
398// ─── Tests ─────────────────────────────────────────────────────────
399
400#[cfg(test)]
401#[allow(clippy::unwrap_used)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_ct_hex_encode() {
407 assert_eq!(ct_hex_encode(&[0xDE, 0xAD, 0xBE, 0xEF]), "deadbeef");
408 assert_eq!(ct_hex_encode(&[]), "");
409 assert_eq!(ct_hex_encode(&[0x00, 0xFF]), "00ff");
410 }
411
412 #[test]
413 fn test_ct_hex_decode() {
414 assert_eq!(
415 ct_hex_decode("deadbeef"),
416 Some(vec![0xDE, 0xAD, 0xBE, 0xEF])
417 );
418 assert_eq!(ct_hex_decode(""), Some(vec![]));
419 assert_eq!(ct_hex_decode("00ff"), Some(vec![0x00, 0xFF]));
420 assert_eq!(
421 ct_hex_decode("DEADBEEF"),
422 Some(vec![0xDE, 0xAD, 0xBE, 0xEF])
423 );
424 }
425
426 #[test]
427 fn test_ct_hex_decode_invalid() {
428 assert_eq!(ct_hex_decode("f"), None);
429 assert_eq!(ct_hex_decode("gg"), None);
430 }
431
432 #[test]
433 fn test_ct_hex_roundtrip() {
434 let data = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF];
435 let encoded = ct_hex_encode(&data);
436 let decoded = ct_hex_decode(&encoded).unwrap();
437 assert_eq!(decoded, data);
438 }
439
440 #[test]
441 fn test_secure_zero() {
442 let mut data = vec![0xAA; 32];
443 secure_zero(&mut data);
444 assert!(data.iter().all(|&b| b == 0));
445 }
446
447 #[test]
448 fn test_guarded_memory_new() {
449 let guard = GuardedMemory::new(32);
450 assert_eq!(guard.len(), 32);
451 assert!(guard.as_ref().iter().all(|&b| b == 0));
452 }
453
454 #[test]
455 fn test_guarded_memory_from_vec() {
456 let data = vec![0xAA; 16];
457 let guard = GuardedMemory::from_vec(data);
458 assert_eq!(guard.len(), 16);
459 assert!(guard.as_ref().iter().all(|&b| b == 0xAA));
460 }
461
462 #[test]
463 fn test_guarded_memory_mut() {
464 let mut guard = GuardedMemory::new(4);
465 guard.as_mut().copy_from_slice(&[1, 2, 3, 4]);
466 assert_eq!(guard.as_ref(), &[1, 2, 3, 4]);
467 }
468
469 #[test]
470 fn test_guarded_memory_debug_redacted() {
471 let guard = GuardedMemory::from_vec(vec![0xFF; 32]);
472 let debug = format!("{:?}", guard);
473 assert!(debug.contains("[REDACTED]"));
474 assert!(!debug.contains("255"));
475 }
476
477 #[test]
478 fn test_secure_random() {
479 let mut buf = [0u8; 32];
480 secure_random(&mut buf).unwrap();
481 // Extremely unlikely all zero after random fill
482 assert!(!buf.iter().all(|&b| b == 0));
483 }
484}