Skip to main content

palisade_config/
tags.rs

1//! Cryptographic tag derivation for honeypot artifacts.
2
3use crate::errors::EntropyValidationError;
4use crate::timing::{enforce_operation_min_timing, TimingOperation};
5use palisade_errors::Result;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use sha3::{Digest, Sha3_512};
8use std::time::Instant;
9use zeroize::{Zeroize, ZeroizeOnDrop};
10
11/// Root cryptographic tag with hierarchical derivation capability.
12#[derive(Clone, Zeroize, ZeroizeOnDrop)]
13pub struct RootTag {
14    /// The root secret (never exposed, never serialized, zeroized on drop)
15    secret: [u8; 32],
16
17    /// SHA3-512 hash of the root secret (for secure diffing/comparison)
18    #[zeroize(skip)]
19    hash: [u8; 64],
20}
21
22impl RootTag {
23    /// Create from hex-encoded string with comprehensive validation.
24    pub fn new(hex: impl AsRef<str>) -> Result<Self> {
25        let started = Instant::now();
26        let hex = hex.as_ref();
27
28        let result = (|| {
29            // Validation 1: Exact length (256-bit security)
30            if hex.len() != 64 {
31                return Err(EntropyValidationError::insufficient_length(hex.len(), 64));
32            }
33
34            // Validation 2: Hex encoding (decode into stack buffer, no heap allocation)
35            let mut bytes = [0u8; 32];
36            hex::decode_to_slice(hex, &mut bytes).map_err(EntropyValidationError::invalid_hex)?;
37
38            // Validation 3: Entropy (CRITICAL - applies to ALL tags)
39            Self::validate_entropy(&bytes)?;
40
41            // Compute SHA3-512 hash for secure diffing/comparison
42            let mut hasher = Sha3_512::new();
43            hasher.update(bytes);
44            let hash_result = hasher.finalize();
45
46            let mut hash = [0u8; 64];
47            hash.copy_from_slice(hash_result.as_slice());
48
49            Ok(Self { secret: bytes, hash })
50        })();
51
52        enforce_operation_min_timing(started, TimingOperation::RootTagNew);
53        result
54    }
55
56    /// Generate cryptographically secure root tag using OS RNG.
57    /// 
58    /// # Errors
59    /// 
60    /// Returns error if the OS RNG produces invalid entropy (catastrophic system failure).
61    pub fn generate() -> Result<Self> {
62        let started = Instant::now();
63        use rand::RngCore;
64
65        let mut bytes = [0u8; 32]; // 256-bit entropy
66        rand::rngs::OsRng.fill_bytes(&mut bytes);
67
68        // SECURITY: Validate even generated entropy
69        // If OsRng produces invalid entropy, this is a critical system failure
70        Self::validate_entropy(&bytes)?;
71
72        // Compute SHA3-512 hash
73        let mut hasher = Sha3_512::new();
74        hasher.update(bytes);
75        let hash_result = hasher.finalize();
76
77        let mut hash = [0u8; 64];
78        hash.copy_from_slice(hash_result.as_slice());
79
80        let out = Ok(Self {
81            secret: bytes,
82            hash,
83        });
84
85        enforce_operation_min_timing(started, TimingOperation::RootTagGenerate);
86        out
87    }
88
89    /// Validate entropy quality with comprehensive heuristic checks.
90    fn validate_entropy(bytes: &[u8]) -> Result<()> {
91        if bytes.is_empty() {
92            return Err(EntropyValidationError::insufficient_length(0, 32));
93        }
94
95        // Allocation-free entropy heuristics:
96        // - track all-zero input
97        // - track unique byte cardinality with a 256-bit bitmap
98        // - detect sequential runs
99        let mut all_zeros = true;
100        let mut unique_bitmap = [0u64; 4];
101        let mut unique_count = 0usize;
102        let mut sequential_count = 0usize;
103
104        let mut prev = bytes[0];
105        for (i, &b) in bytes.iter().enumerate() {
106            all_zeros &= b == 0;
107
108            let idx = (b as usize) >> 6;
109            let bit = 1u64 << ((b as usize) & 63);
110            if (unique_bitmap[idx] & bit) == 0 {
111                unique_bitmap[idx] |= bit;
112                unique_count += 1;
113            }
114
115            if i > 0 && b == prev.wrapping_add(1) {
116                sequential_count += 1;
117            }
118            prev = b;
119        }
120
121        if all_zeros {
122            return Err(EntropyValidationError::all_zeros());
123        }
124
125        // Check 2: Byte Diversity (require at least 25% unique bytes)
126        if unique_count < bytes.len() / 4 {
127            return Err(EntropyValidationError::low_diversity(
128                unique_count,
129                bytes.len(),
130            ));
131        }
132
133        // Check 3: Sequential Pattern Detection
134        if sequential_count > bytes.len() / 2 {
135            return Err(EntropyValidationError::sequential_pattern());
136        }
137
138        // Check 4: Substring Repetition Detection
139        if bytes.len() >= 8 {
140            let first_quarter = &bytes[0..bytes.len() / 4];
141            let rest = &bytes[bytes.len() / 4..];
142            if rest.windows(first_quarter.len()).any(|w| w == first_quarter) {
143                return Err(EntropyValidationError::repeated_substring());
144            }
145        }
146
147        Ok(())
148    }
149
150    /// Derive host-specific tag bytes using SHA3-512 (no heap allocation).
151    #[must_use]
152    pub fn derive_host_tag_bytes(&self, hostname: &str) -> [u8; 64] {
153        let started = Instant::now();
154        let mut hasher = Sha3_512::new();
155        hasher.update(self.secret);
156        hasher.update(hostname.as_bytes());
157
158        let digest = hasher.finalize();
159        let mut out = [0u8; 64];
160        out.copy_from_slice(&digest);
161        enforce_operation_min_timing(started, TimingOperation::RootTagDeriveHost);
162        out
163    }
164
165    /// Derive host-specific tag using SHA3-512.
166    #[must_use]
167    pub fn derive_host_tag(&self, hostname: &str) -> Vec<u8> {
168        self.derive_host_tag_bytes(hostname).to_vec()
169    }
170
171    /// Derive artifact-specific tag bytes using SHA3-512 (no heap allocation).
172    #[must_use]
173    pub fn derive_artifact_tag_bytes(&self, hostname: &str, artifact_id: &str) -> [u8; 64] {
174        let started = Instant::now();
175        let host_tag = self.derive_host_tag_bytes(hostname);
176
177        let mut hasher = Sha3_512::new();
178        hasher.update(host_tag);
179        hasher.update(artifact_id.as_bytes());
180
181        let digest = hasher.finalize();
182        let mut out = [0u8; 64];
183        out.copy_from_slice(&digest);
184        enforce_operation_min_timing(started, TimingOperation::RootTagDeriveArtifact);
185        out
186    }
187
188    /// Derive artifact-specific tag as lowercase hex bytes into caller-provided buffer.
189    ///
190    /// This method performs no heap allocation.
191    pub fn derive_artifact_tag_hex_into(
192        &self,
193        hostname: &str,
194        artifact_id: &str,
195        out: &mut [u8; 128],
196    ) {
197        const HEX: &[u8; 16] = b"0123456789abcdef";
198        let digest = self.derive_artifact_tag_bytes(hostname, artifact_id);
199        for (i, &b) in digest.iter().enumerate() {
200            out[i * 2] = HEX[(b >> 4) as usize];
201            out[i * 2 + 1] = HEX[(b & 0x0f) as usize];
202        }
203    }
204
205    /// Derive artifact-specific tag using SHA3-512.
206    #[must_use]
207    pub fn derive_artifact_tag(&self, hostname: &str, artifact_id: &str) -> String {
208        hex::encode(self.derive_artifact_tag_bytes(hostname, artifact_id))
209    }
210
211    /// Get SHA3-512 hash for comparison without exposing secret.
212    #[must_use]
213    pub fn hash(&self) -> &[u8; 64] {
214        &self.hash
215    }
216
217    /// Constant-time comparison of root tag hashes.
218    #[must_use]
219    pub fn hash_eq_ct(&self, other: &Self) -> bool {
220        let started = Instant::now();
221        let eq = ct_eq(self.hash(), other.hash());
222        enforce_operation_min_timing(started, TimingOperation::RootTagHashCompare);
223        eq
224    }
225}
226
227#[inline]
228fn ct_eq(left: &[u8], right: &[u8]) -> bool {
229    if left.len() != right.len() {
230        return false;
231    }
232
233    let mut diff = 0u8;
234    for (&l, &r) in left.iter().zip(right.iter()) {
235        diff |= l ^ r;
236    }
237    diff == 0
238}
239
240impl std::fmt::Debug for RootTag {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        f.debug_struct("RootTag")
243            .field("secret", &"[REDACTED]")
244            .field("hash", &format!("{:x?}...", &self.hash[..8]))
245            .finish()
246    }
247}
248
249impl Serialize for RootTag {
250    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
251    where
252        S: Serializer,
253    {
254        serializer.serialize_str(&hex::encode(self.secret))
255    }
256}
257
258impl<'de> Deserialize<'de> for RootTag {
259    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
260    where
261        D: Deserializer<'de>,
262    {
263        let value = String::deserialize(deserializer)?;
264
265        if value == "***REDACTED***" {
266            return Err(serde::de::Error::custom(
267                "Cannot deserialize redacted root tag. Provide actual hex-encoded tag.",
268            ));
269        }
270
271        Self::new(value).map_err(serde::de::Error::custom)
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_root_tag_generation_creates_valid_entropy() {
281        let tag = RootTag::generate().expect("Failed to generate tag");
282        assert_eq!(tag.secret.len(), 32);
283        assert_eq!(tag.hash.len(), 64);
284    }
285
286    #[test]
287    fn test_root_tag_generation_is_random() {
288        let tag1 = RootTag::generate().expect("Failed to generate tag1");
289        let tag2 = RootTag::generate().expect("Failed to generate tag2");
290        assert_ne!(tag1.hash(), tag2.hash());
291    }
292
293    #[test]
294    fn test_tag_derivation_is_deterministic() {
295        let root = RootTag::generate().expect("Failed to generate root");
296        let tag1 = root.derive_artifact_tag("host1", "artifact1");
297        let tag2 = root.derive_artifact_tag("host1", "artifact1");
298        assert_eq!(tag1, tag2);
299    }
300
301    #[test]
302    fn test_tag_derivation_different_hosts() {
303        let root = RootTag::generate().expect("Failed to generate root");
304        let tag1 = root.derive_artifact_tag("host1", "artifact1");
305        let tag2 = root.derive_artifact_tag("host2", "artifact1");
306        assert_ne!(tag1, tag2);
307    }
308
309    #[test]
310    fn test_entropy_validation_rejects_all_zeros() {
311        let result = RootTag::new(hex::encode(vec![0u8; 32]));
312        assert!(result.is_err());
313    }
314
315    #[test]
316    fn test_entropy_validation_rejects_sequential() {
317        let sequential: Vec<u8> = (0..32).collect();
318        let result = RootTag::new(hex::encode(sequential));
319        assert!(result.is_err());
320    }
321
322    #[test]
323    fn test_debug_does_not_expose_secret() {
324        let root = RootTag::generate().expect("Failed to generate root");
325        let debug_str = format!("{:?}", root);
326
327        assert!(debug_str.contains("REDACTED"));
328        assert!(!debug_str.contains(&hex::encode(&root.secret)));
329    }
330}