Skip to main content

auths_transparency/
types.rs

1use std::fmt;
2
3use base64::{Engine, engine::general_purpose::STANDARD};
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use sha2::{Digest, Sha256};
6
7use crate::error::TransparencyError;
8
9/// SHA-256 Merkle hash (32 bytes).
10///
11/// Args:
12/// * Inner `[u8; 32]` — raw SHA-256 digest.
13///
14/// Usage:
15/// ```ignore
16/// let hash = MerkleHash::from_bytes([0u8; 32]);
17/// let hex_str = hash.to_string(); // lowercase hex
18/// ```
19#[derive(Clone, Copy, PartialEq, Eq, Hash)]
20pub struct MerkleHash([u8; 32]);
21
22impl MerkleHash {
23    /// The all-zero hash, used as a sentinel for empty trees.
24    pub const EMPTY: Self = Self([0u8; 32]);
25
26    /// Wrap raw bytes.
27    pub fn from_bytes(bytes: [u8; 32]) -> Self {
28        Self(bytes)
29    }
30
31    /// Construct from a hex string.
32    pub fn from_hex(s: &str) -> Result<Self, TransparencyError> {
33        let bytes = hex::decode(s).map_err(|e| TransparencyError::InvalidProof(e.to_string()))?;
34        let arr: [u8; 32] = bytes
35            .try_into()
36            .map_err(|_| TransparencyError::InvalidProof("hash must be 32 bytes".into()))?;
37        Ok(Self(arr))
38    }
39
40    /// Raw bytes.
41    pub fn as_bytes(&self) -> &[u8; 32] {
42        &self.0
43    }
44
45    /// Encode as standard base64 (with padding). Used in C2SP checkpoint note body.
46    pub fn to_base64(&self) -> String {
47        STANDARD.encode(self.0)
48    }
49
50    /// Decode from standard base64 (with padding).
51    pub fn from_base64(s: &str) -> Result<Self, TransparencyError> {
52        let bytes = STANDARD
53            .decode(s)
54            .map_err(|e| TransparencyError::InvalidProof(format!("base64 decode: {e}")))?;
55        let arr: [u8; 32] = bytes
56            .try_into()
57            .map_err(|_| TransparencyError::InvalidProof("hash must be 32 bytes".into()))?;
58        Ok(Self(arr))
59    }
60
61    /// Plain SHA-256 (no domain separation). Used for key-ID computation.
62    pub fn sha256(data: &[u8]) -> Self {
63        let digest = Sha256::digest(data);
64        let mut out = [0u8; 32];
65        out.copy_from_slice(&digest);
66        Self(out)
67    }
68}
69
70impl fmt::Debug for MerkleHash {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "MerkleHash({})", self)
73    }
74}
75
76impl fmt::Display for MerkleHash {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        for b in &self.0 {
79            write!(f, "{b:02x}")?;
80        }
81        Ok(())
82    }
83}
84
85impl Serialize for MerkleHash {
86    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
87        serializer.serialize_str(&self.to_string())
88    }
89}
90
91impl<'de> Deserialize<'de> for MerkleHash {
92    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
93        let s = String::deserialize(deserializer)?;
94        Self::from_hex(&s).map_err(serde::de::Error::custom)
95    }
96}
97
98impl AsRef<[u8]> for MerkleHash {
99    fn as_ref(&self) -> &[u8] {
100        &self.0
101    }
102}
103
104/// Validated log origin string (e.g., `"auths.dev/log"`).
105///
106/// Must be non-empty ASCII with no control characters.
107///
108/// Args:
109/// * Inner `String` — validated ASCII origin.
110///
111/// Usage:
112/// ```ignore
113/// let origin = LogOrigin::new("auths.dev/log")?;
114/// ```
115#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
116#[serde(try_from = "String", into = "String")]
117pub struct LogOrigin(String);
118
119impl LogOrigin {
120    /// Create a new log origin, validating that it is non-empty ASCII.
121    pub fn new(s: &str) -> Result<Self, TransparencyError> {
122        if s.is_empty() {
123            return Err(TransparencyError::InvalidOrigin("must not be empty".into()));
124        }
125        if !s.is_ascii() {
126            return Err(TransparencyError::InvalidOrigin("must be ASCII".into()));
127        }
128        if s.bytes().any(|b| b < 0x20) {
129            return Err(TransparencyError::InvalidOrigin(
130                "must not contain control characters".into(),
131            ));
132        }
133        Ok(Self(s.to_string()))
134    }
135
136    /// Create from a compile-time constant. Panics if invalid.
137    ///
138    /// Only for use in `default_config()` and similar contexts where the
139    /// string is a known-good constant.
140    #[allow(clippy::expect_used)] // INVARIANT: only called with compile-time ASCII constants
141    pub fn new_unchecked(s: &str) -> Self {
142        Self::new(s).expect("LogOrigin::new_unchecked called with invalid origin")
143    }
144
145    /// The inner string.
146    pub fn as_str(&self) -> &str {
147        &self.0
148    }
149}
150
151impl fmt::Display for LogOrigin {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        f.write_str(&self.0)
154    }
155}
156
157impl TryFrom<String> for LogOrigin {
158    type Error = TransparencyError;
159    fn try_from(s: String) -> Result<Self, Self::Error> {
160        Self::new(&s)
161    }
162}
163
164impl From<LogOrigin> for String {
165    fn from(o: LogOrigin) -> Self {
166        o.0
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn merkle_hash_hex_roundtrip() {
176        let bytes = [0xabu8; 32];
177        let h = MerkleHash::from_bytes(bytes);
178        let hex_str = h.to_string();
179        let h2 = MerkleHash::from_hex(&hex_str).unwrap();
180        assert_eq!(h, h2);
181    }
182
183    #[test]
184    fn merkle_hash_json_roundtrip() {
185        let h = MerkleHash::from_bytes([0x42u8; 32]);
186        let json = serde_json::to_string(&h).unwrap();
187        let h2: MerkleHash = serde_json::from_str(&json).unwrap();
188        assert_eq!(h, h2);
189    }
190
191    #[test]
192    fn log_origin_rejects_empty() {
193        assert!(LogOrigin::new("").is_err());
194    }
195
196    #[test]
197    fn log_origin_rejects_non_ascii() {
198        assert!(LogOrigin::new("日本語").is_err());
199    }
200
201    #[test]
202    fn log_origin_rejects_control_chars() {
203        assert!(LogOrigin::new("auths\x00log").is_err());
204    }
205
206    #[test]
207    fn log_origin_valid() {
208        let o = LogOrigin::new("auths.dev/log").unwrap();
209        assert_eq!(o.as_str(), "auths.dev/log");
210    }
211}