Skip to main content

luct_core/
lib.rs

1#![forbid(unsafe_code)]
2
3use crate::utils::base64::Base64;
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::fmt::{self, Display};
7use url::Url;
8
9mod cert;
10mod cert_chain;
11pub mod log_list;
12pub(crate) mod signature;
13pub mod store;
14pub mod tiling;
15pub mod tree;
16pub(crate) mod utils;
17pub mod v1;
18mod version;
19
20pub use cert::{Certificate, CertificateError, Fingerprint};
21pub use cert_chain::CertificateChain;
22pub use signature::{HashAlgorithm, SignatureAlgorithm, SignatureValidationError};
23pub use version::Version;
24
25// TODO: Introduce a Timestamp type and use it
26// TODO: Introduce toplevel types that wrap the inner v1 types to make version agnostic API
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct CtLog {
30    config: CtLogConfig,
31    log_id: LogId,
32}
33
34impl CtLog {
35    pub fn new(config: CtLogConfig) -> Self {
36        let log_id = match config.version() {
37            Version::V1 => LogId::V1(v1::LogId(Sha256::digest(&config.key.0).into())),
38        };
39
40        Self { config, log_id }
41    }
42
43    pub fn log_id(&self) -> &LogId {
44        &self.log_id
45    }
46
47    pub fn config(&self) -> &CtLogConfig {
48        &self.config
49    }
50
51    pub fn description(&self) -> &str {
52        &self.config.description
53    }
54}
55
56/// Configuration of a [`CtLog`]
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct CtLogConfig {
59    /// Short description of the log
60    description: String,
61
62    #[serde(default)]
63    /// The [`Version`] of this log
64    version: Version,
65
66    /// The [`Url`] at which the log operates
67    ///
68    /// In case of an RFC 6962 log, this is the url at which the endpoint lives.
69    /// In the case of a tiled log, this corresponds to the submission url
70    url: Url,
71
72    /// Public key used to sign
73    key: Base64<Vec<u8>>,
74
75    /// Maximum merge delay
76    mmd: u64,
77
78    /// The [`Url`], used by tiling logs to fetch tiles
79    tile_url: Option<Url>,
80}
81
82impl CtLogConfig {
83    /// Return the [`Url`] of this log
84    pub fn url(&self) -> &Url {
85        &self.url
86    }
87
88    /// Return the fetch [`Url`] for this log
89    pub fn fetch_url(&self) -> &Url {
90        self.url()
91    }
92
93    /// Return the tile [`Url`] for this log
94    pub fn tile_url(&self) -> &Option<Url> {
95        &self.tile_url
96    }
97
98    /// Return `true`, if the `tile_url` is set, `false` otherwise
99    pub fn is_tiling(&self) -> bool {
100        self.tile_url.is_some()
101    }
102
103    /// Return the [`Version`] of this log
104    pub fn version(&self) -> &Version {
105        &self.version
106    }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
110pub enum LogId {
111    V1(v1::LogId),
112}
113
114impl Display for LogId {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            LogId::V1(log_id) => write!(f, "{log_id}"),
118        }
119    }
120}
121
122impl From<v1::LogId> for LogId {
123    fn from(value: v1::LogId) -> Self {
124        Self::V1(value)
125    }
126}
127
128#[cfg(test)]
129mod tests {
130
131    use super::*;
132    use base64::{Engine, prelude::BASE64_STANDARD};
133
134    const ARGON2025H1: &str = "{
135        \"description\": \"Google Argon\",
136        \"url\": \"https://ct.googleapis.com/logs/us1/argon2025h1/\",
137        \"key\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIIKh+WdoqOTblJji4WiH5AltIDUzODyvFKrXCBjw/Rab0/98J4LUh7dOJEY7+66+yCNSICuqRAX+VPnV8R1Fmg==\",
138        \"mmd\": 86400
139        }
140    ";
141
142    const ARGON2025H2: &str = "{
143        \"description\": \"Google Argon\",
144        \"version\": 1,
145        \"url\": \"https://ct.googleapis.com/logs/us1/argon2025h2/\",
146        \"key\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEr+TzlCzfpie1/rJhgxnIITojqKk9VK+8MZoc08HjtsLzD8e5yjsdeWVhIiWCVk6Y6KomKTYeKGBv6xVu93zQug==\",
147        \"mmd\": 86400
148        }
149    ";
150
151    pub(crate) const ARGON2025H1_STH2806: &str = "{
152    \"tree_size\":1425614114,
153    \"timestamp\":1751114416696,
154    \"sha256_root_hash\":\"LHtW79pwJohJF5Yn/tyozEroOnho4u3JAGn7WeHSR54=\",
155    \"tree_head_signature\":\"BAMARzBFAiEAg4w8LlTFKd3KL6lo5Zde9OupHYNN0DDk8U54PenirI4CIHL8ucpkJw5zFLh8UvLA+Zf+f8Ms+tLsVtzHuqnO0qjm\"
156    }";
157
158    pub(crate)const ARGON2025H1_STH2906: &str = "{
159    \"tree_size\":1425633154,
160    \"timestamp\":1751189445313,
161    \"sha256_root_hash\":\"iH90iBSqmtLLTcCwu74RYyJ0rd3oXtLbXlBNqKcJUXA=\",
162    \"tree_head_signature\":\"BAMARjBEAiAA/UmelqZIfpd5vBs0CJZGx8kAqUhNppLX/rBVk15DWwIgbyecvj2CUl4YzAEWEoFmUwL9KkrZBZQcQgSNEFDqIgc=\"
163    }";
164
165    pub(crate) const CERT_CHAIN_GOOGLE_COM: &str = include_str!("../../testdata/google-chain.pem");
166
167    pub(crate) fn get_log_argon2025h1() -> CtLog {
168        let config = serde_json::from_str(ARGON2025H1).unwrap();
169        CtLog::new(config)
170    }
171
172    pub(crate) fn get_log_argon2025h2() -> CtLog {
173        let config = serde_json::from_str(ARGON2025H2).unwrap();
174        CtLog::new(config)
175    }
176
177    #[test]
178    fn ct_log_toml_parse() {
179        let log = get_log_argon2025h1();
180
181        let test_log_id = BASE64_STANDARD
182            .decode("TnWjJ1yaEMM4W2zU3z9S6x3w4I4bjWnAsfpksWKaOd8=")
183            .unwrap();
184
185        let LogId::V1(log_id) = log.log_id();
186        assert_eq!(log_id.0.to_vec(), test_log_id)
187    }
188}