Skip to main content

auths_verifier/tlog/
types.rs

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