Skip to main content

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}