1use 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#[derive(Clone, Zeroize, ZeroizeOnDrop)]
13pub struct RootTag {
14 secret: [u8; 32],
16
17 #[zeroize(skip)]
19 hash: [u8; 64],
20}
21
22impl RootTag {
23 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 if hex.len() != 64 {
31 return Err(EntropyValidationError::insufficient_length(hex.len(), 64));
32 }
33
34 let mut bytes = [0u8; 32];
36 hex::decode_to_slice(hex, &mut bytes).map_err(EntropyValidationError::invalid_hex)?;
37
38 Self::validate_entropy(&bytes)?;
40
41 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 pub fn generate() -> Result<Self> {
62 let started = Instant::now();
63 use rand::RngCore;
64
65 let mut bytes = [0u8; 32]; rand::rngs::OsRng.fill_bytes(&mut bytes);
67
68 Self::validate_entropy(&bytes)?;
71
72 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 fn validate_entropy(bytes: &[u8]) -> Result<()> {
91 if bytes.is_empty() {
92 return Err(EntropyValidationError::insufficient_length(0, 32));
93 }
94
95 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 if unique_count < bytes.len() / 4 {
127 return Err(EntropyValidationError::low_diversity(
128 unique_count,
129 bytes.len(),
130 ));
131 }
132
133 if sequential_count > bytes.len() / 2 {
135 return Err(EntropyValidationError::sequential_pattern());
136 }
137
138 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 #[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 #[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 #[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 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 #[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 #[must_use]
213 pub fn hash(&self) -> &[u8; 64] {
214 &self.hash
215 }
216
217 #[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}