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