diff --git a/Cargo.lock b/Cargo.lock index ccf1f1bd..539db160 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,6 +179,7 @@ dependencies = [ "sha2", "signed_note", "thiserror 2.0.17", + "tlog_checkpoint", "tlog_core", "tlog_tiles", "x509-cert", @@ -211,6 +212,7 @@ dependencies = [ "serde_json", "serde_with", "signed_note", + "tlog_checkpoint", "tlog_core", "tlog_tiles", "url", @@ -586,6 +588,7 @@ dependencies = [ "serde_with", "signed_note", "static_ct_api", + "tlog_checkpoint", "tlog_core", "tlog_tiles", "url", @@ -1033,6 +1036,7 @@ dependencies = [ "signed_note", "static_ct_api", "thiserror 2.0.17", + "tlog_checkpoint", "tlog_core", "tlog_tiles", "tokio", @@ -1482,6 +1486,7 @@ dependencies = [ "sha2", "signed_note", "static_ct_api", + "tlog_checkpoint", "tlog_core", "tlog_cosignature", "tlog_tiles", @@ -2814,6 +2819,7 @@ dependencies = [ "signed_note", "spki", "thiserror 2.0.17", + "tlog_checkpoint", "tlog_core", "tlog_tiles", "x509-cert", @@ -2993,9 +2999,23 @@ name = "tlog-fuzz" version = "0.0.0" dependencies = [ "libfuzzer-sys", + "tlog_checkpoint", "tlog_tiles", ] +[[package]] +name = "tlog_checkpoint" +version = "0.2.0" +dependencies = [ + "base64", + "ed25519-dalek", + "rand", + "sha2", + "signed_note", + "thiserror 2.0.17", + "tlog_core", +] + [[package]] name = "tlog_core" version = "0.2.0" @@ -3016,8 +3036,8 @@ dependencies = [ "ml-dsa", "rand", "signed_note", + "tlog_checkpoint", "tlog_core", - "tlog_tiles", ] [[package]] @@ -3025,14 +3045,11 @@ name = "tlog_tiles" version = "0.2.0" dependencies = [ "base64", - "ed25519-dalek", "length_prefixed", - "rand", "serde", "serde_json", "sha2", - "signed_note", - "thiserror 2.0.17", + "tlog_checkpoint", "tlog_core", "url", ] @@ -3043,8 +3060,8 @@ version = "0.2.0" dependencies = [ "console_error_panic_hook", "getrandom 0.4.2", + "tlog_checkpoint", "tlog_core", - "tlog_tiles", "wasm-bindgen", ] @@ -3821,9 +3838,9 @@ dependencies = [ "serde_json", "serde_with", "signed_note", + "tlog_checkpoint", "tlog_core", "tlog_cosignature", - "tlog_tiles", "tlog_witness", "witness_worker_config", "worker", diff --git a/Cargo.toml b/Cargo.toml index bc2f5ede..913bbaed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "crates/signed_note", "crates/signed_note_wasm", "crates/static_ct_api", + "crates/tlog_checkpoint", "crates/tlog_core", "crates/tlog_cosignature", "crates/tlog_tiles", @@ -43,6 +44,7 @@ default-members = [ "crates/signed_note", "crates/signed_note_wasm", "crates/static_ct_api", + "crates/tlog_checkpoint", "crates/tlog_core", "crates/tlog_cosignature", "crates/tlog_tiles", @@ -126,6 +128,7 @@ signature = "3.0.0-rc.10" signed_note = { path = "crates/signed_note", version = "0.2.0" } static_ct_api = { path = "crates/static_ct_api", version = "0.2.0" } thiserror = "2.0" +tlog_checkpoint = { path = "crates/tlog_checkpoint", version = "0.2.0" } tlog_core = { path = "crates/tlog_core", version = "0.2.0" } tlog_cosignature = { path = "crates/tlog_cosignature", version = "0.2.0" } tlog_tiles = { path = "crates/tlog_tiles", version = "0.2.0" } diff --git a/crates/bootstrap_mtc_api/Cargo.toml b/crates/bootstrap_mtc_api/Cargo.toml index 3217b793..1d68d40f 100644 --- a/crates/bootstrap_mtc_api/Cargo.toml +++ b/crates/bootstrap_mtc_api/Cargo.toml @@ -21,6 +21,7 @@ sha2.workspace = true signed_note.workspace = true thiserror.workspace = true tlog_core.workspace = true +tlog_checkpoint.workspace = true tlog_tiles.workspace = true x509-cert.workspace = true x509_util.workspace = true diff --git a/crates/bootstrap_mtc_api/src/cosigner.rs b/crates/bootstrap_mtc_api/src/cosigner.rs index 8061a2cf..743bb5fd 100644 --- a/crates/bootstrap_mtc_api/src/cosigner.rs +++ b/crates/bootstrap_mtc_api/src/cosigner.rs @@ -9,8 +9,8 @@ use ed25519_dalek::{ }; use length_prefixed::WriteLengthPrefixedBytesExt; use signed_note::{KeyName, NoteError, NoteSignature, NoteVerifier}; +use tlog_checkpoint::{CheckpointSigner, CheckpointText, UnixTimestampMillis}; use tlog_core::{Hash, LeafIndex}; -use tlog_tiles::{CheckpointSigner, CheckpointText, UnixTimestamp}; use crate::{RelativeOid, ID_RDNA_TRUSTANCHOR_ID}; @@ -93,8 +93,8 @@ impl CheckpointSigner for MtcCosigner { /// Sign a checkpoint with the subtree cosigner. For checkpoints, the start index is always 0. fn sign( &self, - _timestamp_unix_millis: UnixTimestamp, - checkpoint: &tlog_tiles::CheckpointText, + _timestamp: UnixTimestampMillis, + checkpoint: &tlog_checkpoint::CheckpointText, ) -> Result { let sig = self.sign_subtree(0, checkpoint.size(), checkpoint.hash())?; Ok(NoteSignature::new(self.name().clone(), self.key_id(), sig)) @@ -235,8 +235,8 @@ fn serialize_mtc_subtree_signature_input( #[cfg(test)] mod tests { + use tlog_checkpoint::{open_checkpoint, TreeWithTimestamp}; use tlog_core::record_hash; - use tlog_tiles::{open_checkpoint, TreeWithTimestamp}; use super::*; use signed_note::VerifierList; diff --git a/crates/bootstrap_mtc_api/src/lib.rs b/crates/bootstrap_mtc_api/src/lib.rs index c39e38fb..be202baf 100644 --- a/crates/bootstrap_mtc_api/src/lib.rs +++ b/crates/bootstrap_mtc_api/src/lib.rs @@ -24,9 +24,10 @@ use std::{ num::ParseIntError, }; use thiserror::Error; +use tlog_checkpoint::UnixTimestampMillis; use tlog_core::{Hash, LeafIndex, Proof, Subtree, TlogError}; use tlog_tiles::{ - LogEntry, PathElem, PendingLogEntry, TlogTilesLogEntry, TlogTilesPendingLogEntry, UnixTimestamp, + LogEntry, PathElem, PendingLogEntry, TlogTilesLogEntry, TlogTilesPendingLogEntry, }; use x509_cert::{ certificate::Version, @@ -123,11 +124,11 @@ pub struct AddEntryResponse { pub leaf_index: LeafIndex, /// The time at which the entry was added to the log. - pub timestamp: UnixTimestamp, + pub timestamp: UnixTimestampMillis, /// The validity period of the certificate. - pub not_before: UnixTimestamp, - pub not_after: UnixTimestamp, + pub not_before: UnixTimestampMillis, + pub not_after: UnixTimestampMillis, } /// Get-roots response. This is in the same format as the RFC 6962 get-roots @@ -184,7 +185,7 @@ impl LogEntry for BootstrapMtcLogEntry { }) } - fn new(pending: Self::Pending, leaf_index: LeafIndex, timestamp: UnixTimestamp) -> Self { + fn new(pending: Self::Pending, leaf_index: LeafIndex, timestamp: UnixTimestampMillis) -> Self { Self(TlogTilesLogEntry::new(pending.entry, leaf_index, timestamp)) } diff --git a/crates/bootstrap_mtc_worker/Cargo.toml b/crates/bootstrap_mtc_worker/Cargo.toml index 4c170b9b..19907c68 100644 --- a/crates/bootstrap_mtc_worker/Cargo.toml +++ b/crates/bootstrap_mtc_worker/Cargo.toml @@ -59,6 +59,7 @@ serde_json.workspace = true serde_with.workspace = true signed_note.workspace = true tlog_core.workspace = true +tlog_checkpoint.workspace = true tlog_tiles.workspace = true worker.workspace = true x509-cert.workspace = true diff --git a/crates/bootstrap_mtc_worker/src/cleaner_do.rs b/crates/bootstrap_mtc_worker/src/cleaner_do.rs index 3e9ff51b..a15d44d8 100644 --- a/crates/bootstrap_mtc_worker/src/cleaner_do.rs +++ b/crates/bootstrap_mtc_worker/src/cleaner_do.rs @@ -4,7 +4,8 @@ use crate::{load_checkpoint_cosigner, load_origin, CONFIG}; use bootstrap_mtc_api::BootstrapMtcPendingLogEntry; use generic_log_worker::{get_durable_object_name, CleanerConfig, GenericCleaner, CLEANER_BINDING}; use signed_note::VerifierList; -use tlog_tiles::{CheckpointSigner, PendingLogEntry}; +use tlog_checkpoint::CheckpointSigner; +use tlog_tiles::PendingLogEntry; #[allow(clippy::wildcard_imports)] use worker::*; diff --git a/crates/bootstrap_mtc_worker/src/frontend_worker.rs b/crates/bootstrap_mtc_worker/src/frontend_worker.rs index 1cdb016d..f95f5c66 100644 --- a/crates/bootstrap_mtc_worker/src/frontend_worker.rs +++ b/crates/bootstrap_mtc_worker/src/frontend_worker.rs @@ -27,10 +27,9 @@ use serde::{Deserialize, Serialize}; use serde_with::{base64::Base64, serde_as}; use signed_note::VerifierList; use std::time::Duration; +use tlog_checkpoint::{open_checkpoint, CheckpointSigner, CheckpointText}; use tlog_core::LeafIndex; -use tlog_tiles::{ - open_checkpoint, CheckpointSigner, CheckpointText, PendingLogEntry, PendingLogEntryBlob, -}; +use tlog_tiles::{PendingLogEntry, PendingLogEntryBlob}; #[allow(clippy::wildcard_imports)] use worker::*; use x509_cert::{ diff --git a/crates/bootstrap_mtc_worker/src/sequence_metadata.rs b/crates/bootstrap_mtc_worker/src/sequence_metadata.rs index af1ae74d..90d1738c 100644 --- a/crates/bootstrap_mtc_worker/src/sequence_metadata.rs +++ b/crates/bootstrap_mtc_worker/src/sequence_metadata.rs @@ -8,8 +8,9 @@ use generic_log_worker::{ deserialize_sequence_metadata_entries, serialize_sequence_metadata_entries, SequencerMetadata, }; use serde::{Deserialize, Serialize}; +use tlog_checkpoint::UnixTimestampMillis; use tlog_core::LeafIndex; -use tlog_tiles::{LookupKey, UnixTimestamp}; +use tlog_tiles::LookupKey; /// Sequencer metadata for a bootstrap MTC log entry. /// @@ -20,7 +21,7 @@ use tlog_tiles::{LookupKey, UnixTimestamp}; /// 1. **Durable Object dedup ring buffer**: 32-byte binary layout /// `[16-byte lookup key | 8-byte leaf_index BE | 8-byte timestamp BE]`. /// This format matches the one previously used when the type was -/// `(LeafIndex, UnixTimestamp)`; preserving it avoids a one-time deserialize +/// `(LeafIndex, UnixTimestampMillis)`; preserving it avoids a one-time deserialize /// warning on deploy as the sequencer loads any entries already in DO /// storage. /// 2. **DO→Worker RPC**: `bitcode` sequence of the two u64 fields (preserved by @@ -28,7 +29,7 @@ use tlog_tiles::{LookupKey, UnixTimestamp}; /// /// Bootstrap MTC does not currently use the long-term KV dedup cache. #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct BootstrapMtcSequenceMetadata(pub LeafIndex, pub UnixTimestamp); +pub struct BootstrapMtcSequenceMetadata(pub LeafIndex, pub UnixTimestampMillis); impl BootstrapMtcSequenceMetadata { /// Return the leaf index. @@ -39,7 +40,7 @@ impl BootstrapMtcSequenceMetadata { /// Return the sequencing timestamp (milliseconds since the Unix epoch). #[must_use] - pub fn timestamp(&self) -> UnixTimestamp { + pub fn timestamp(&self) -> UnixTimestampMillis { self.1 } } @@ -47,7 +48,7 @@ impl BootstrapMtcSequenceMetadata { impl SequencerMetadata for BootstrapMtcSequenceMetadata { fn new( leaf_index: LeafIndex, - timestamp: UnixTimestamp, + timestamp: UnixTimestampMillis, _old_tree_size: u64, _new_tree_size: u64, ) -> Self { diff --git a/crates/bootstrap_mtc_worker/src/sequencer_do.rs b/crates/bootstrap_mtc_worker/src/sequencer_do.rs index dba50d85..4abea2e1 100644 --- a/crates/bootstrap_mtc_worker/src/sequencer_do.rs +++ b/crates/bootstrap_mtc_worker/src/sequencer_do.rs @@ -19,8 +19,8 @@ use generic_log_worker::{ use serde::{Deserialize, Serialize}; use serde_with::{base64::Base64, serde_as}; use signed_note::Note; +use tlog_checkpoint::{CheckpointText, UnixTimestampMillis}; use tlog_core::Hash; -use tlog_tiles::{CheckpointText, UnixTimestamp}; #[allow(clippy::wildcard_imports)] use worker::*; @@ -87,8 +87,8 @@ fn checkpoint_callback(env: &Env, name: &str) -> CheckpointCallbacker { let params = &CONFIG.logs[name]; let bucket = load_public_bucket(env, name).unwrap(); Box::new( - move |old_time: UnixTimestamp, - new_time: UnixTimestamp, + move |old_time: UnixTimestampMillis, + new_time: UnixTimestampMillis, _old_tree_size: u64, _new_tree_size: u64, new_checkpoint_bytes: &[u8]| { diff --git a/crates/ct_worker/Cargo.toml b/crates/ct_worker/Cargo.toml index befd5d60..248f750c 100644 --- a/crates/ct_worker/Cargo.toml +++ b/crates/ct_worker/Cargo.toml @@ -51,6 +51,7 @@ serde_with.workspace = true static_ct_api.workspace = true signed_note.workspace = true tlog_core.workspace = true +tlog_checkpoint.workspace = true tlog_tiles.workspace = true worker.workspace = true x509-cert.workspace = true diff --git a/crates/ct_worker/src/lib.rs b/crates/ct_worker/src/lib.rs index 6abbb417..450edae9 100644 --- a/crates/ct_worker/src/lib.rs +++ b/crates/ct_worker/src/lib.rs @@ -11,7 +11,7 @@ use signed_note::KeyName; use static_ct_api::StaticCTCheckpointSigner; use std::collections::HashMap; use std::sync::{LazyLock, OnceLock}; -use tlog_tiles::{CheckpointSigner, Ed25519CheckpointSigner}; +use tlog_checkpoint::{CheckpointSigner, Ed25519CheckpointSigner}; use worker::{Env, Result}; use x509_util::CertPool; diff --git a/crates/ct_worker/src/sequence_metadata.rs b/crates/ct_worker/src/sequence_metadata.rs index a02430d4..b31dbb71 100644 --- a/crates/ct_worker/src/sequence_metadata.rs +++ b/crates/ct_worker/src/sequence_metadata.rs @@ -8,8 +8,9 @@ use generic_log_worker::{ deserialize_sequence_metadata_entries, serialize_sequence_metadata_entries, SequencerMetadata, }; use serde::{Deserialize, Serialize}; +use tlog_checkpoint::UnixTimestampMillis; use tlog_core::LeafIndex; -use tlog_tiles::{LookupKey, UnixTimestamp}; +use tlog_tiles::LookupKey; /// Sequencer metadata for a static-ct-api log entry. /// @@ -25,7 +26,7 @@ use tlog_tiles::{LookupKey, UnixTimestamp}; /// 3. **DO→Worker RPC**: `bitcode` sequence of the two u64 fields. Also /// preserved by the tuple-struct layout. #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct StaticCTSequenceMetadata(pub LeafIndex, pub UnixTimestamp); +pub struct StaticCTSequenceMetadata(pub LeafIndex, pub UnixTimestampMillis); impl StaticCTSequenceMetadata { /// Return the leaf index. @@ -36,7 +37,7 @@ impl StaticCTSequenceMetadata { /// Return the sequencing timestamp (milliseconds since the Unix epoch). #[must_use] - pub fn timestamp(&self) -> UnixTimestamp { + pub fn timestamp(&self) -> UnixTimestampMillis { self.1 } } @@ -44,7 +45,7 @@ impl StaticCTSequenceMetadata { impl SequencerMetadata for StaticCTSequenceMetadata { fn new( leaf_index: LeafIndex, - timestamp: UnixTimestamp, + timestamp: UnixTimestampMillis, _old_tree_size: u64, _new_tree_size: u64, ) -> Self { @@ -71,7 +72,7 @@ mod tests { use super::*; /// Confirm the JSON wire format used by the KV long-term dedup cache - /// metadata matches the historical `(LeafIndex, UnixTimestamp)` tuple shape + /// metadata matches the historical `(LeafIndex, UnixTimestampMillis)` tuple shape /// (i.e. `[leaf_index, timestamp]`). Changing this would orphan pre-existing /// KV entries from deployed logs. #[test] diff --git a/crates/generic_log_worker/Cargo.toml b/crates/generic_log_worker/Cargo.toml index a65a301a..31ed4abb 100644 --- a/crates/generic_log_worker/Cargo.toml +++ b/crates/generic_log_worker/Cargo.toml @@ -41,6 +41,7 @@ sha2.workspace = true signed_note.workspace = true thiserror.workspace = true tlog_core.workspace = true +tlog_checkpoint.workspace = true tlog_tiles.workspace = true tokio.workspace = true worker.workspace = true diff --git a/crates/generic_log_worker/src/cleaner_do.rs b/crates/generic_log_worker/src/cleaner_do.rs index e5f65f8a..c219d50e 100644 --- a/crates/generic_log_worker/src/cleaner_do.rs +++ b/crates/generic_log_worker/src/cleaner_do.rs @@ -281,7 +281,7 @@ impl GenericCleaner { .ok_or("missing object body")? .bytes() .await?; - let checkpoint = tlog_tiles::open_checkpoint( + let checkpoint = tlog_checkpoint::open_checkpoint( self.config.origin.as_str(), &self.config.verifiers, now_millis(), diff --git a/crates/generic_log_worker/src/lib.rs b/crates/generic_log_worker/src/lib.rs index acecada2..83764879 100644 --- a/crates/generic_log_worker/src/lib.rs +++ b/crates/generic_log_worker/src/lib.rs @@ -27,9 +27,10 @@ use std::collections::btree_map::Entry; use std::collections::{BTreeMap, HashMap, VecDeque}; use serde::de::DeserializeOwned; +use tlog_checkpoint::UnixTimestampMillis; use tlog_core::LeafIndex; pub use tlog_tiles::LookupKey; -use tlog_tiles::{PendingLogEntry, UnixTimestamp}; +use tlog_tiles::PendingLogEntry; use tokio::sync::Mutex; use util::now_millis; use worker::{ @@ -446,7 +447,7 @@ pub trait SequencerMetadata: /// for an inclusion proof without enumerating candidates). fn new( leaf_index: LeafIndex, - timestamp: UnixTimestamp, + timestamp: UnixTimestampMillis, old_tree_size: u64, new_tree_size: u64, ) -> Self; @@ -477,7 +478,7 @@ pub trait SequencerMetadata: /// A `(LookupKey, (leaf_index, timestamp))` pair as consumed by /// [`serialize_sequence_metadata_entries`] / [`deserialize_sequence_metadata_entries`]. -pub type SequenceMetadataEntry = (LookupKey, (LeafIndex, UnixTimestamp)); +pub type SequenceMetadataEntry = (LookupKey, (LeafIndex, UnixTimestampMillis)); /// Serialize a batch of [`SequenceMetadataEntry`] values using the fixed /// 32-byte binary format: @@ -855,7 +856,7 @@ mod tests { impl SequencerMetadata for TestMetadata { fn new( leaf_index: LeafIndex, - _timestamp: UnixTimestamp, + _timestamp: UnixTimestampMillis, _old_tree_size: u64, _new_tree_size: u64, ) -> Self { diff --git a/crates/generic_log_worker/src/log_ops.rs b/crates/generic_log_worker/src/log_ops.rs index e7a163ae..25f134a4 100644 --- a/crates/generic_log_worker/src/log_ops.rs +++ b/crates/generic_log_worker/src/log_ops.rs @@ -39,10 +39,11 @@ use std::{ sync::LazyLock, }; use thiserror::Error; +use tlog_checkpoint::{TreeWithTimestamp, UnixTimestampMillis}; use tlog_core::{Hash, HashReader, Proof, Subtree, TlogError, HASH_SIZE}; use tlog_tiles::{ LogEntry, PendingLogEntry, PreloadedTlogTileReader, TileHashReader, TileIterator, TlogTile, - TlogTileRecorder, TreeWithTimestamp, UnixTimestamp, + TlogTileRecorder, }; use tokio::sync::watch::{channel, Receiver, Sender}; @@ -89,7 +90,7 @@ pub(crate) struct PoolState PoolState { new_size / u64::from(TlogTile::FULL_WIDTH) > old_size / u64::from(TlogTile::FULL_WIDTH); let num_leftover_entries = usize::try_from(new_size % u64::from(TlogTile::FULL_WIDTH)).unwrap(); - let oldest_leftover_timestamp_millis: UnixTimestamp = self.leftover_timestamps_millis[(self - .leftover_timestamps_next_slot - - num_leftover_entries) - % TlogTile::FULL_WIDTH as usize]; + let oldest_leftover_timestamp_millis: UnixTimestampMillis = self.leftover_timestamps_millis + [(self.leftover_timestamps_next_slot - num_leftover_entries) + % TlogTile::FULL_WIDTH as usize]; let oldest_leftover_is_expired = sequence_skip_threshold_millis .is_some_and(|threshold| now_millis() > oldest_leftover_timestamp_millis + threshold); @@ -337,7 +337,7 @@ impl SequenceState { .collect(), ); - let (c, timestamp) = tlog_tiles::open_checkpoint( + let (c, timestamp) = tlog_checkpoint::open_checkpoint( config.origin.as_str(), &verifiers, now_millis(), @@ -362,8 +362,12 @@ impl SequenceState { "{name}: Loaded checkpoint from object storage; checkpoint={}", std::str::from_utf8(&stored_checkpoint)? ); - let (c1, _) = - tlog_tiles::open_checkpoint(config.origin.as_str(), &verifiers, now_millis(), &sth)?; + let (c1, _) = tlog_checkpoint::open_checkpoint( + config.origin.as_str(), + &verifiers, + now_millis(), + &sth, + )?; match (Ord::cmp(&c1.size(), &c.size()), c1.hash() == c.hash()) { (Ordering::Equal, false) => { @@ -1388,15 +1392,15 @@ mod tests { use tlog_core::LeafIndex; /// Local metadata type used to exercise the sequencer and batcher logic - /// end-to-end. Kept as a `(LeafIndex, UnixTimestamp)` tuple so existing + /// end-to-end. Kept as a `(LeafIndex, UnixTimestampMillis)` tuple so existing /// tests can destructure with `let (leaf_index, timestamp) = ...`. The /// JSON cache serialization defaults are fine for tests. - type SequenceMetadata = (LeafIndex, UnixTimestamp); + type SequenceMetadata = (LeafIndex, UnixTimestampMillis); impl SequencerMetadata for SequenceMetadata { fn new( leaf_index: LeafIndex, - timestamp: UnixTimestamp, + timestamp: UnixTimestampMillis, _old_tree_size: u64, _new_tree_size: u64, ) -> Self { @@ -1417,7 +1421,8 @@ mod tests { PrecertData, StaticCTCheckpointSigner, StaticCTLogEntry, StaticCTPendingLogEntry, }; use std::time::Duration; - use tlog_tiles::{CheckpointSigner, CheckpointText, Ed25519CheckpointSigner, TlogTile}; + use tlog_checkpoint::{CheckpointSigner, CheckpointText, Ed25519CheckpointSigner}; + use tlog_tiles::TlogTile; #[test] fn test_sequence_one_leaf_short() { diff --git a/crates/generic_log_worker/src/sequencer_do.rs b/crates/generic_log_worker/src/sequencer_do.rs index 2358d10b..ef81747c 100644 --- a/crates/generic_log_worker/src/sequencer_do.rs +++ b/crates/generic_log_worker/src/sequencer_do.rs @@ -22,7 +22,8 @@ use futures_util::future::join_all; use log::{error, info}; use prometheus::Registry; use signed_note::KeyName; -use tlog_tiles::{CheckpointSigner, LogEntry, PendingLogEntryBlob, UnixTimestamp}; +use tlog_checkpoint::{CheckpointSigner, UnixTimestampMillis}; +use tlog_tiles::{LogEntry, PendingLogEntryBlob}; use tokio::sync::{Mutex, RwLock}; use worker::{Env, Error as WorkerError, Request, Response, State}; @@ -51,7 +52,7 @@ pub struct SequencerConfig { pub checkpoint_signers: Vec>, /// A function that takes a Unix timestamp in milliseconds and returns /// extension lines to be included in the checkpoint - pub checkpoint_extension: Box Vec>, + pub checkpoint_extension: Box Vec>, pub sequence_interval: Duration, pub max_sequence_skips: usize, pub sequence_skip_threshold_millis: Option, @@ -357,15 +358,15 @@ impl GenericSequencer { /// so that it can have side-effects. /// /// The parameters are as follows: -/// - `old_time: UnixTimestamp`: The timestamp of the previous checkpoint. -/// - `new_time: UnixTimestamp`: The timestamp of the latest checkpoint. +/// - `old_time: UnixTimestampMillis`: The timestamp of the previous checkpoint. +/// - `new_time: UnixTimestampMillis`: The timestamp of the latest checkpoint. /// - `old_tree_size: u64`: The tree size of the previous checkpoint. /// - `new_tree_size: u64`: The tree size of the latest checkpoint. /// - `new_checkpoint: &[u8]`: The latest checkpoint bytes. This is a signed note. pub type CheckpointCallbacker = Box< dyn Fn( - UnixTimestamp, - UnixTimestamp, + UnixTimestampMillis, + UnixTimestampMillis, u64, u64, &[u8], @@ -378,8 +379,8 @@ pub type CheckpointCallbacker = Box< #[must_use] pub fn empty_checkpoint_callback() -> CheckpointCallbacker { Box::new( - move |_old_time: UnixTimestamp, - _new_time: UnixTimestamp, + move |_old_time: UnixTimestampMillis, + _new_time: UnixTimestampMillis, _old_tree_size: u64, _new_tree_size: u64, _new_checkpoint: &[u8]| { Box::pin(async move { Ok(()) }) }, diff --git a/crates/integration_tests/Cargo.toml b/crates/integration_tests/Cargo.toml index e43fc9a2..afce02c6 100644 --- a/crates/integration_tests/Cargo.toml +++ b/crates/integration_tests/Cargo.toml @@ -30,6 +30,7 @@ signed_note.workspace = true static_ct_api.workspace = true bootstrap_mtc_api.workspace = true tlog_core.workspace = true +tlog_checkpoint.workspace = true tlog_cosignature.workspace = true tlog_tiles.workspace = true tlog_witness.workspace = true diff --git a/crates/integration_tests/src/assertions.rs b/crates/integration_tests/src/assertions.rs index da559279..a4b311ae 100644 --- a/crates/integration_tests/src/assertions.rs +++ b/crates/integration_tests/src/assertions.rs @@ -14,10 +14,10 @@ use sct_validator::{ use sha2::{Digest, Sha256}; use signed_note::{Ed25519NoteVerifier, KeyName, VerifierList}; use static_ct_api::{Extensions, RFC6962NoteVerifier, StaticCTLogEntry}; +use tlog_checkpoint::{open_checkpoint, CheckpointText}; use tlog_core::HashReader; use tlog_tiles::{ - open_checkpoint, CheckpointText, LogEntry, PathElem, PreloadedTlogTileReader, TileHashReader, - TileIterator, TlogTile, + LogEntry, PathElem, PreloadedTlogTileReader, TileHashReader, TileIterator, TlogTile, }; use x509_cert::{der::Decode, der::Encode, Certificate}; diff --git a/crates/integration_tests/tests/tlog_witness.rs b/crates/integration_tests/tests/tlog_witness.rs index cb2f6779..725a5057 100644 --- a/crates/integration_tests/tests/tlog_witness.rs +++ b/crates/integration_tests/tests/tlog_witness.rs @@ -50,11 +50,11 @@ use serde::Deserialize; use serde_with::{base64::Base64, serde_as}; use signed_note::{KeyName, Note, NoteSignature, VerifierList}; use std::time::Duration; +use tlog_checkpoint::{CheckpointSigner, Ed25519CheckpointSigner, TreeWithTimestamp}; use tlog_core::{ consistency_proof, record_hash, stored_hashes, tree_hash, Hash, HashReader, TlogError, HASH_SIZE, }; -use tlog_tiles::{CheckpointSigner, Ed25519CheckpointSigner, TreeWithTimestamp}; use tlog_witness::{ parse_add_checkpoint_response, serialize_add_checkpoint_request, CONTENT_TYPE_TLOG_SIZE, }; diff --git a/crates/static_ct_api/Cargo.toml b/crates/static_ct_api/Cargo.toml index 1ea621bb..f4858810 100644 --- a/crates/static_ct_api/Cargo.toml +++ b/crates/static_ct_api/Cargo.toml @@ -38,6 +38,7 @@ signature.workspace = true signed_note.workspace = true thiserror.workspace = true tlog_core.workspace = true +tlog_checkpoint.workspace = true tlog_tiles.workspace = true spki.workspace = true x509-cert.workspace = true diff --git a/crates/static_ct_api/src/rfc6962.rs b/crates/static_ct_api/src/rfc6962.rs index 4c0ae1ba..8881f6f7 100644 --- a/crates/static_ct_api/src/rfc6962.rs +++ b/crates/static_ct_api/src/rfc6962.rs @@ -32,7 +32,7 @@ use der::{ use serde::{Deserialize, Serialize}; use serde_with::{base64::Base64, serde_as}; use sha2::{Digest, Sha256}; -use tlog_tiles::UnixTimestamp; +use tlog_checkpoint::UnixTimestampMillis; use x509_cert::{ der::Encode, ext::pkix::ExtendedKeyUsage, impl_newtype, Certificate, TbsCertificate, }; @@ -56,7 +56,7 @@ pub struct AddChainResponse { pub sct_version: u8, #[serde_as(as = "Base64")] pub id: Vec, - pub timestamp: UnixTimestamp, + pub timestamp: UnixTimestampMillis, #[serde_as(as = "Base64")] pub extensions: Vec, #[serde_as(as = "Base64")] @@ -81,8 +81,8 @@ pub struct GetRootsResponse { pub fn partially_validate_chain( raw_chain: &[Vec], roots: &CertPool, - not_after_start: Option, - not_after_end: Option, + not_after_start: Option, + not_after_end: Option, expect_precert: bool, require_server_auth_eku: bool, ) -> Result<(StaticCTPendingLogEntry, Option), StaticCTError> { @@ -247,7 +247,7 @@ mod tests { use x509_cert::ext::Extension; use x509_cert::{Certificate, TbsCertificate}; - fn parse_datetime(s: &str) -> UnixTimestamp { + fn parse_datetime(s: &str) -> UnixTimestampMillis { u64::try_from(DateTime::parse_from_rfc3339(s).unwrap().timestamp_millis()).unwrap() } diff --git a/crates/static_ct_api/src/static_ct.rs b/crates/static_ct_api/src/static_ct.rs index cc4e1ba9..c799c820 100644 --- a/crates/static_ct_api/src/static_ct.rs +++ b/crates/static_ct_api/src/static_ct.rs @@ -61,7 +61,7 @@ //! //! // Make a list of the verifiers that MUST apear on the checkpoint, and load the checkpoint //! let verifiers = VerifierList::new(vec![Box::new(rfc6962_verifier), Box::new(witness_verifier)]); -//! let (_checkpoint, _timestamp) = tlog_tiles::open_checkpoint( +//! let (_checkpoint, _timestamp) = tlog_checkpoint::open_checkpoint( //! "static-ct-dev.cloudflareresearch.com/logs/dev2024h2b", //! &verifiers, //! now, @@ -119,10 +119,9 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use signed_note::{KeyName, NoteError, NoteSignature, NoteVerifier, SignatureType}; use std::io::Read; +use tlog_checkpoint::{CheckpointSigner, CheckpointText, UnixTimestampMillis}; use tlog_core::{Hash, LeafIndex}; -use tlog_tiles::{ - CheckpointSigner, CheckpointText, LogEntry, LookupKey, PathElem, PendingLogEntry, UnixTimestamp, -}; +use tlog_tiles::{LogEntry, LookupKey, PathElem, PendingLogEntry}; #[repr(u16)] enum EntryType { @@ -240,7 +239,7 @@ pub struct StaticCTLogEntry { pub leaf_index: LeafIndex, /// The `TimestampedEntry.timestamp`. - pub timestamp: UnixTimestamp, + pub timestamp: UnixTimestampMillis, } impl StaticCTLogEntry { @@ -288,7 +287,7 @@ impl LogEntry for StaticCTLogEntry { fn new( pending: StaticCTPendingLogEntry, leaf_index: LeafIndex, - timestamp: UnixTimestamp, + timestamp: UnixTimestampMillis, ) -> Self { StaticCTLogEntry { inner: pending, @@ -576,7 +575,10 @@ impl NoteVerifier for RFC6962NoteVerifier { self.verifying_key.verify(&sth_bytes, &signature).is_ok() } - fn extract_timestamp_millis(&self, mut sig: &[u8]) -> Result, NoteError> { + fn extract_timestamp_millis( + &self, + mut sig: &[u8], + ) -> Result, NoteError> { // In a static-ct signed tree head, the timestamp is the first 8 bytes of the sig // https://github.com/C2SP/C2SP/blob/efb68c16664309a68120e37528fa1c046dd1ac09/static-ct-api.md#checkpoints // and it's in milliseconds @@ -718,7 +720,7 @@ impl CheckpointSigner for StaticCTCheckpointSigner { /// then prepend the timestamp. fn sign( &self, - timestamp_unix_millis: UnixTimestamp, + timestamp: UnixTimestampMillis, checkpoint: &CheckpointText, ) -> Result { // RFC 6962-type signatures do not sign extension lines. If this checkpoint has extension lines, this is an error. @@ -727,11 +729,8 @@ impl CheckpointSigner for StaticCTCheckpointSigner { } // Produce the bytestring that will be signed - let tree_head_bytes = serialize_sth_signature_input( - timestamp_unix_millis, - checkpoint.size(), - checkpoint.hash(), - ); + let tree_head_bytes = + serialize_sth_signature_input(timestamp, checkpoint.size(), checkpoint.hash()); // Sign the string let tree_head_sig = sign(&self.signing_key, &tree_head_bytes); @@ -742,9 +741,7 @@ impl CheckpointSigner for StaticCTCheckpointSigner { // TreeHeadSignature signature; // } RFC6962NoteSignature; let mut note_sig = Vec::new(); - note_sig - .write_u64::(timestamp_unix_millis) - .unwrap(); + note_sig.write_u64::(timestamp).unwrap(); note_sig.extend(&tree_head_sig); // Return the note signature. We can unwrap() here because the only cause for error is if diff --git a/crates/tlog_checkpoint/Cargo.toml b/crates/tlog_checkpoint/Cargo.toml new file mode 100644 index 00000000..ea31dace --- /dev/null +++ b/crates/tlog_checkpoint/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "tlog_checkpoint" +readme = "README.md" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +description = "An implementation of c2sp.org/tlog-checkpoint" +categories = ["cryptography"] +keywords = ["transparency", "checkpoint", "merkle", "crypto", "pki"] + +[package.metadata.release] +release = false + +# https://github.com/rustwasm/wasm-pack/issues/1247 +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + +[lib] +crate-type = ["rlib"] + +[dependencies] +base64.workspace = true +ed25519-dalek.workspace = true +rand.workspace = true +sha2.workspace = true +signed_note.workspace = true +thiserror.workspace = true +tlog_core.workspace = true diff --git a/crates/tlog_checkpoint/LICENSE b/crates/tlog_checkpoint/LICENSE new file mode 100644 index 00000000..53e625b8 --- /dev/null +++ b/crates/tlog_checkpoint/LICENSE @@ -0,0 +1,73 @@ +Copyright 2025 Cloudflare, Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== + +Copyright 2023 The Sunlight Authors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +======================================================================== + +Copyright 2009 The Go Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/crates/tlog_checkpoint/README.md b/crates/tlog_checkpoint/README.md new file mode 100644 index 00000000..7176c1cb --- /dev/null +++ b/crates/tlog_checkpoint/README.md @@ -0,0 +1,38 @@ +# tlog_checkpoint + +A Rust implementation of the [C2SP tlog-checkpoint][tc] specification: the +signed-note format that tiled transparency logs use to represent their +signed tree heads. + +This crate provides: + +- [`CheckpointText`] — the text portion of a signed checkpoint (origin, + size, root hash, optional extension lines). +- [`CheckpointSigner`] — a trait for signers that produce checkpoint + cosignatures, plus an [`Ed25519CheckpointSigner`] implementation for + the basic Ed25519 signed-note algorithm (`0x01`). +- [`TreeWithTimestamp`] — pairs a tree size + root hash with a Unix + timestamp; what a log signs to produce a checkpoint. +- [`open_checkpoint`] — caller-side parsing/verification path used by + full-coverage clients (every configured verifier must sign). + +The Merkle math (the [`Hash`] type, the proof builders/verifiers, the +`Subtree` type) lives in the [`tlog_core`] crate; this crate adds the +checkpoint signed-note shape on top. + +[tc]: https://c2sp.org/tlog-checkpoint +[`Hash`]: https://docs.rs/tlog_core/latest/tlog_core/struct.Hash.html +[`tlog_core`]: https://docs.rs/tlog_core + +## Test + + cargo test + +## Acknowledgements + +Ports code from [tlog](https://golang.org/x/mod/sumdb/tlog) and +[sunlight](https://github.com/FiloSottile/sunlight). + +## License + +[BSD-3-Clause](./LICENSE). diff --git a/crates/tlog_tiles/src/checkpoint.rs b/crates/tlog_checkpoint/src/lib.rs similarity index 92% rename from crates/tlog_tiles/src/checkpoint.rs rename to crates/tlog_checkpoint/src/lib.rs index e5e7d236..49e371f7 100644 --- a/crates/tlog_tiles/src/checkpoint.rs +++ b/crates/tlog_checkpoint/src/lib.rs @@ -11,7 +11,9 @@ // Modifications and Rust implementation Copyright (c) 2025 Cloudflare, Inc. // Licensed under the BSD-3-Clause license found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause -//! A Checkpoint is a tree head to be formatted according to the [C2SP tlog-checkpoint](https://c2sp.org/tlog-checkpoint) specification. +//! A Checkpoint is a tree head formatted as a [signed-note][note] +//! per the [C2SP tlog-checkpoint](https://c2sp.org/tlog-checkpoint) +//! specification. //! //! A checkpoint looks like this: //! ```text @@ -25,16 +27,33 @@ //! — PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM= //! — EnochRoot rwz+eBzmZa0SO3NbfRGzPCpDckykFXSdeX+MNtCOXm2/5n2tiOHp+vAF1aGrQ5ovTG01oOTGwnWLox33WWd1RvMc+QQ= //! ``` -//! We call everything up to and including the last extension line (including its final newline) the **checkpoint text**. //! -//! This file contains code ported from the original projects [tlog](https://pkg.go.dev/golang.org/x/mod/sumdb/tlog) and [sunlight](https://github.com/FiloSottile/sunlight). +//! We call everything up to and including the last extension line +//! (including its final newline) the **checkpoint text**. //! -//! References: -//! - [note.go](https://cs.opensource.google/go/x/mod/+/refs/tags/v0.21.0:sumdb/tlog/note.go) -//! - [note_test.go](https://cs.opensource.google/go/x/mod/+/refs/tags/v0.21.0:sumdb/tlog/note_test.go) -//! - [checkpoint.go](https://github.com/FiloSottile/sunlight/blob/36be227ff4599ac11afe3cec37a5febcd61da16a/checkpoint.go) +//! This crate provides: +//! +//! - [`CheckpointText`] — parser/serializer for the text portion. +//! - [`CheckpointSigner`] / [`Ed25519CheckpointSigner`] — signers that +//! produce checkpoint signatures. +//! - [`TreeWithTimestamp`] — a (size, hash, timestamp) tuple ready to +//! be signed. +//! - [`open_checkpoint`] — the caller-side +//! parse-and-fully-verify entry point. +//! +//! The Merkle math (the [`Hash`] type, proof builders/verifiers, +//! `Subtree`) lives in the [`tlog_core`] crate. +//! +//! Ports code from the Go [`tlog`][tlog-go] package and from +//! [sunlight][sunlight]; see per-symbol references in the source for +//! upstream pointers. +//! +//! [note]: https://c2sp.org/signed-note +//! [`Hash`]: tlog_core::Hash +//! [`tlog_core`]: https://docs.rs/tlog_core +//! [tlog-go]: https://pkg.go.dev/golang.org/x/mod/sumdb/tlog +//! [sunlight]: https://github.com/FiloSottile/sunlight -use crate::UnixTimestamp; use base64::{prelude::BASE64_STANDARD, Engine}; use ed25519_dalek::{Signer, SigningKey as Ed25519SigningKey}; use rand::{seq::SliceRandom, Rng, RngExt}; @@ -50,6 +69,14 @@ use std::{ use thiserror::Error; use tlog_core::{Hash, HashReader, TlogError}; +/// Unix-epoch timestamp, in milliseconds. +/// +/// Embedded in [`TreeWithTimestamp`] and signed by every +/// [`CheckpointSigner`] that produces a timestamped cosignature +/// (e.g. `cosignature/v1`). A type alias rather than a newtype so it +/// composes naturally with arithmetic / comparisons at every callsite. +pub type UnixTimestampMillis = u64; + /// Errors returned by the checkpoint-signing and -verification entry /// points in this module ([`open_checkpoint`], [`TreeWithTimestamp::sign`], /// [`TreeWithTimestamp::from_hash_reader`]). @@ -206,7 +233,7 @@ impl CheckpointText { /// /// # Errors /// - /// Returns a [`MalformedCheckpointError`] if the arguments do not comply with + /// Returns a [`MalformedCheckpointTextError`] if the arguments do not comply with /// the [C2SP tlog-checkpoint](https://c2sp.org/tlog-checkpoint) specification. pub fn new( origin: &str, @@ -345,7 +372,7 @@ pub trait CheckpointSigner { /// Errors if the signing fails. fn sign( &self, - timestamp_unix_millis: UnixTimestamp, + timestamp: UnixTimestampMillis, checkpoint: &CheckpointText, ) -> Result; @@ -385,7 +412,7 @@ impl CheckpointSigner for Ed25519CheckpointSigner { fn sign( &self, - _: UnixTimestamp, + _: UnixTimestampMillis, checkpoint_text: &CheckpointText, ) -> Result { let msg = checkpoint_text.to_bytes(); @@ -424,9 +451,9 @@ impl CheckpointSigner for Ed25519CheckpointSigner { pub fn open_checkpoint( origin: &str, verifiers: &VerifierList, - current_time: UnixTimestamp, + current_time: UnixTimestampMillis, checkpoint_bytes: &[u8], -) -> Result<(CheckpointText, Option), CheckpointError> { +) -> Result<(CheckpointText, Option), CheckpointError> { // A checkpoint is a signed note whose text is a CheckpointText let n = Note::from_bytes(checkpoint_bytes)?; let (verified_sigs, _) = n.verify(verifiers)?; @@ -434,7 +461,7 @@ pub fn open_checkpoint( // Go through the signatures and make sure we find all the key IDs in our verifiers list let mut key_ids_to_observe = verifiers.key_ids(); // The latest timestamp of the signatures in the note. We use this to check that nothing was signed in the future - let mut latest_timestamp: Option = None; + let mut latest_timestamp: Option = None; for sig in &verified_sigs { // Fetch the verifier for this signature, if it's here let verif = match verifiers.verifier(sig.name(), sig.id()) { @@ -474,13 +501,13 @@ pub fn open_checkpoint( pub struct TreeWithTimestamp { size: u64, hash: Hash, - time: UnixTimestamp, + time: UnixTimestampMillis, } impl TreeWithTimestamp { /// Returns a new tree with the given hash. #[must_use] - pub fn new(size: u64, hash: Hash, time: UnixTimestamp) -> Self { + pub fn new(size: u64, hash: Hash, time: UnixTimestampMillis) -> Self { Self { size, hash, time } } @@ -493,7 +520,7 @@ impl TreeWithTimestamp { pub fn from_hash_reader( size: u64, r: &R, - time: UnixTimestamp, + time: UnixTimestampMillis, ) -> Result { let hash = tlog_core::tree_hash(size, r)?; Ok(Self { size, hash, time }) @@ -513,7 +540,7 @@ impl TreeWithTimestamp { /// Returns the timestamp of the tree. #[must_use] - pub fn time(&self) -> UnixTimestamp { + pub fn time(&self) -> UnixTimestampMillis { self.time } diff --git a/crates/tlog_cosignature/Cargo.toml b/crates/tlog_cosignature/Cargo.toml index e1dfc067..740193d3 100644 --- a/crates/tlog_cosignature/Cargo.toml +++ b/crates/tlog_cosignature/Cargo.toml @@ -21,7 +21,7 @@ length_prefixed.workspace = true ml-dsa.workspace = true signed_note.workspace = true tlog_core.workspace = true -tlog_tiles.workspace = true +tlog_checkpoint.workspace = true [dev-dependencies] rand.workspace = true diff --git a/crates/tlog_cosignature/src/cosignature_v1.rs b/crates/tlog_cosignature/src/cosignature_v1.rs index 6a83356b..35fec8bd 100644 --- a/crates/tlog_cosignature/src/cosignature_v1.rs +++ b/crates/tlog_cosignature/src/cosignature_v1.rs @@ -7,7 +7,7 @@ use ed25519_dalek::{ }; use signed_note::{KeyName, NoteError, NoteSignature, NoteVerifier, SignatureType}; -use tlog_tiles::{CheckpointSigner, CheckpointText, UnixTimestamp}; +use tlog_checkpoint::{CheckpointSigner, CheckpointText, UnixTimestampMillis}; /// Implementation of [`CheckpointSigner`] that produces a timestamped Ed25519 cosignature/v1 (alg 0x04 from ). pub struct CosignatureV1CheckpointSigner { @@ -37,11 +37,11 @@ impl CheckpointSigner for CosignatureV1CheckpointSigner { fn sign( &self, - timestamp_unix_millis: UnixTimestamp, + timestamp: UnixTimestampMillis, checkpoint: &CheckpointText, ) -> Result { // Timestamp is in seconds. - let timestamp_unix_secs = timestamp_unix_millis / 1000; + let timestamp_unix_secs = timestamp / 1000; let mut msg = format!("cosignature/v1\ntime {timestamp_unix_secs}\n").into_bytes(); msg.extend(checkpoint.to_bytes()); @@ -140,8 +140,8 @@ impl NoteVerifier for CosignatureV1NoteVerifier { #[cfg(test)] mod tests { + use tlog_checkpoint::{open_checkpoint, TreeWithTimestamp}; use tlog_core::record_hash; - use tlog_tiles::{open_checkpoint, TreeWithTimestamp}; use super::*; use signed_note::VerifierList; diff --git a/crates/tlog_cosignature/src/subtree_v1.rs b/crates/tlog_cosignature/src/subtree_v1.rs index 8293f54f..fa71c1c6 100644 --- a/crates/tlog_cosignature/src/subtree_v1.rs +++ b/crates/tlog_cosignature/src/subtree_v1.rs @@ -89,8 +89,8 @@ use ml_dsa::{ MlDsa44, Signature as MlDsaSignature, VerifyingKey as MlDsaVerifyingKey, }; use signed_note::{KeyName, NoteError, NoteSignature, NoteVerifier, SignatureType}; +use tlog_checkpoint::{CheckpointSigner, CheckpointText, UnixTimestampMillis}; use tlog_core::{Hash, Subtree, HASH_SIZE}; -use tlog_tiles::{CheckpointSigner, CheckpointText, UnixTimestamp}; /// The fixed 12-byte label prefix domain-separating `subtree/v1` from /// other cosignature formats. @@ -328,7 +328,7 @@ impl CheckpointSigner for SubtreeV1CheckpointSigner { fn sign( &self, - timestamp_unix_millis: UnixTimestamp, + timestamp: UnixTimestampMillis, checkpoint: &CheckpointText, ) -> Result { // The checkpoint case fixes start = 0 and end = checkpoint_size. @@ -342,7 +342,7 @@ impl CheckpointSigner for SubtreeV1CheckpointSigner { // timestamp unchanged (in seconds), trusting them not to pass // zero in this path. Ok(self.sign_subtree( - timestamp_unix_millis / 1000, + timestamp / 1000, checkpoint.origin(), &subtree, checkpoint.hash(), @@ -664,7 +664,7 @@ mod tests { // Build a checkpoint note body and sign with the CheckpointSigner // path. Timestamp is in millis; signer divides by 1000 internally. - let cp_text = tlog_tiles::CheckpointText::new( + let cp_text = tlog_checkpoint::CheckpointText::new( "log.example/origin", 42, Hash([0x55u8; HASH_SIZE]), diff --git a/crates/tlog_tiles/Cargo.toml b/crates/tlog_tiles/Cargo.toml index bba2cbfe..023d6e8f 100644 --- a/crates/tlog_tiles/Cargo.toml +++ b/crates/tlog_tiles/Cargo.toml @@ -7,7 +7,7 @@ edition.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true -description = "An implementation of c2sp.org/tlog-tiles and c2sp.org/tlog-checkpoint" +description = "An implementation of c2sp.org/tlog-tiles" categories = ["cryptography"] keywords = ["ct", "certificate", "transparency", "crypto", "pki"] @@ -26,12 +26,9 @@ serde_json.workspace = true [dependencies] base64.workspace = true -ed25519-dalek.workspace = true length_prefixed.workspace = true -rand.workspace = true serde.workspace = true sha2.workspace = true -signed_note.workspace = true -thiserror.workspace = true +tlog_checkpoint.workspace = true tlog_core.workspace = true url.workspace = true diff --git a/crates/tlog_tiles/README.md b/crates/tlog_tiles/README.md index a2cd20ad..4bb382fb 100644 --- a/crates/tlog_tiles/README.md +++ b/crates/tlog_tiles/README.md @@ -1,6 +1,17 @@ # Tlog Tiles -A Rust implementation of the [C2SP tlog-tiles](https://c2sp.org/tlog-tiles) and [C2SP checkpoint](https://c2sp.org/tlog-checkpoint) specifications. +A Rust implementation of the [C2SP tlog-tiles](https://c2sp.org/tlog-tiles) +HTTP wire format for tile-encoded transparency logs. + +The Merkle math primitives ([`Hash`], proof builders/verifiers, +[`Subtree`]) live in the [`tlog_core`] crate. The signed-note checkpoint +format lives in [`tlog_checkpoint`]. This crate is the tile-encoding +layer on top of both. + +[`Hash`]: https://docs.rs/tlog_core/latest/tlog_core/struct.Hash.html +[`Subtree`]: https://docs.rs/tlog_core/latest/tlog_core/struct.Subtree.html +[`tlog_core`]: https://docs.rs/tlog_core +[`tlog_checkpoint`]: https://docs.rs/tlog_checkpoint ## Test diff --git a/crates/tlog_tiles/src/entries.rs b/crates/tlog_tiles/src/entries.rs index 0476ffbe..c874fa38 100644 --- a/crates/tlog_tiles/src/entries.rs +++ b/crates/tlog_tiles/src/entries.rs @@ -4,14 +4,12 @@ use sha2::{Digest, Sha256}; use std::{io::Read, marker::PhantomData}; use crate::PathElem; +use tlog_checkpoint::UnixTimestampMillis; use tlog_core::{Hash, LeafIndex}; pub const LOOKUP_KEY_LEN: usize = 16; pub type LookupKey = [u8; LOOKUP_KEY_LEN]; -/// Unix timestamp. -pub type UnixTimestamp = u64; - /// An opaque `PendingLogEntry` that can be passed around without requiring full /// deserialization. #[derive(Serialize, Deserialize)] @@ -58,7 +56,7 @@ pub trait LogEntry: core::fmt::Debug + Sized { /// sequencer-generated values common to every tlog application: the leaf /// index and the sequencing timestamp. Application-specific sequencer /// metadata (e.g. tree sizes) is handled outside this trait. - fn new(pending: Self::Pending, leaf_index: LeafIndex, timestamp: UnixTimestamp) -> Self; + fn new(pending: Self::Pending, leaf_index: LeafIndex, timestamp: UnixTimestampMillis) -> Self; /// Returns the Merkle tree leaf hash for this entry. For tlog-tiles, this is the Merkle Tree Hash /// (according to ) @@ -157,7 +155,11 @@ impl LogEntry for TlogTilesLogEntry { None } - fn new(pending: Self::Pending, _leaf_index: LeafIndex, _timestamp: UnixTimestamp) -> Self { + fn new( + pending: Self::Pending, + _leaf_index: LeafIndex, + _timestamp: UnixTimestampMillis, + ) -> Self { Self { inner: pending } } diff --git a/crates/tlog_tiles/src/lib.rs b/crates/tlog_tiles/src/lib.rs index 8c050a68..42c8d55f 100644 --- a/crates/tlog_tiles/src/lib.rs +++ b/crates/tlog_tiles/src/lib.rs @@ -3,22 +3,23 @@ //! # `tlog_tiles` //! -//! Implementation of the [C2SP tlog-tiles](https://c2sp.org/tlog-tiles) and -//! [C2SP tlog-checkpoint](https://c2sp.org/tlog-checkpoint) specifications. +//! Implementation of the [C2SP tlog-tiles](https://c2sp.org/tlog-tiles) +//! HTTP wire format for tile-encoded transparency logs. //! -//! The underlying RFC 6962 Merkle math (the [`Hash`] type, the proof -//! builders/verifiers, the `Subtree` type) lives in the [`tlog_core`] -//! crate; this crate adds the tile-encoding wire format and the -//! checkpoint note shape on top. +//! The Merkle math (the [`Hash`] type, the proof builders/verifiers, +//! the `Subtree` type) lives in the [`tlog_core`] crate; the +//! [tlog-checkpoint][tc] signed-note format lives in the +//! [`tlog_checkpoint`] crate; this crate is the tile-encoding layer on +//! top of both. //! +//! [tc]: https://c2sp.org/tlog-checkpoint //! [`Hash`]: tlog_core::Hash //! [`tlog_core`]: https://docs.rs/tlog_core +//! [`tlog_checkpoint`]: https://docs.rs/tlog_checkpoint -pub mod checkpoint; pub mod entries; pub mod tile; -pub use checkpoint::*; pub use entries::*; pub use tile::*; diff --git a/crates/tlog_tiles_wasm/Cargo.toml b/crates/tlog_tiles_wasm/Cargo.toml index 3dfc2b73..018227e6 100644 --- a/crates/tlog_tiles_wasm/Cargo.toml +++ b/crates/tlog_tiles_wasm/Cargo.toml @@ -19,7 +19,7 @@ crate-type = ["cdylib"] [dependencies] tlog_core = { workspace = true } -tlog_tiles = { workspace = true } +tlog_checkpoint = { workspace = true } wasm-bindgen = { workspace = true } console_error_panic_hook = { workspace = true } getrandom = { workspace = true } diff --git a/crates/tlog_tiles_wasm/src/lib.rs b/crates/tlog_tiles_wasm/src/lib.rs index edf72557..8b9ae24e 100644 --- a/crates/tlog_tiles_wasm/src/lib.rs +++ b/crates/tlog_tiles_wasm/src/lib.rs @@ -19,7 +19,7 @@ pub fn init() { /// Parse with `CheckpointText.fromBytes()`. #[wasm_bindgen] pub struct CheckpointText { - inner: tlog_tiles::CheckpointText, + inner: tlog_checkpoint::CheckpointText, } #[wasm_bindgen] @@ -34,7 +34,7 @@ impl CheckpointText { /// Returns a JS error string if the input is not a valid checkpoint text. #[wasm_bindgen(js_name = "fromBytes")] pub fn from_bytes(data: &[u8]) -> Result { - tlog_tiles::CheckpointText::from_bytes(data) + tlog_checkpoint::CheckpointText::from_bytes(data) .map(|c| CheckpointText { inner: c }) .map_err(|e| JsValue::from_str(&e.to_string())) } diff --git a/crates/tlog_witness/src/lib.rs b/crates/tlog_witness/src/lib.rs index 6fc85661..79bc6f8a 100644 --- a/crates/tlog_witness/src/lib.rs +++ b/crates/tlog_witness/src/lib.rs @@ -29,7 +29,7 @@ //! signatures still attached so the caller can inspect them. //! - Cosignature production: producing a `cosignature/v1` signature is //! handled by [`tlog_cosignature::CosignatureV1CheckpointSigner`] (or any -//! other [`tlog_tiles::CheckpointSigner`] impl). +//! other [`tlog_checkpoint::CheckpointSigner`] impl). //! - Consistency-proof verification: use //! [`tlog_core::verify_consistency_proof`] directly. //! - Persistent state for the "latest cosigned checkpoint per origin" check diff --git a/crates/witness_worker/Cargo.toml b/crates/witness_worker/Cargo.toml index ddd39fbc..a8589464 100644 --- a/crates/witness_worker/Cargo.toml +++ b/crates/witness_worker/Cargo.toml @@ -41,8 +41,8 @@ serde_json.workspace = true serde_with.workspace = true signed_note.workspace = true tlog_core.workspace = true +tlog_checkpoint.workspace = true tlog_cosignature.workspace = true -tlog_tiles.workspace = true tlog_witness.workspace = true worker.workspace = true diff --git a/crates/witness_worker/src/frontend_worker.rs b/crates/witness_worker/src/frontend_worker.rs index b4e34a67..364122ae 100644 --- a/crates/witness_worker/src/frontend_worker.rs +++ b/crates/witness_worker/src/frontend_worker.rs @@ -17,7 +17,7 @@ use generic_log_worker::util::now_millis; use signed_note::NoteError; -use tlog_tiles::{CheckpointSigner, CheckpointText}; +use tlog_checkpoint::{CheckpointSigner, CheckpointText}; use tlog_witness::{ parse_add_checkpoint_request, serialize_add_checkpoint_response, AddCheckpointRequest, CONTENT_TYPE_TLOG_SIZE, @@ -203,7 +203,7 @@ async fn add_checkpoint(mut req: Request, env: Env) -> Result { // `NoteError` variants indicate a syntactically malformed signature // line and are surfaced as `400 Bad Request`. // - // `tlog_tiles::open_checkpoint` (in the c2sp.org/tlog-checkpoint slice + // `tlog_checkpoint::open_checkpoint` (in the c2sp.org/tlog-checkpoint slice // of `tlog_tiles`) is deliberately not used here because // it additionally requires *every* configured verifier to sign — the // full-coverage semantics an issuer or monitor wants, but not a diff --git a/crates/x509_util/src/lib.rs b/crates/x509_util/src/lib.rs index 509e1e4b..bf42c2b2 100644 --- a/crates/x509_util/src/lib.rs +++ b/crates/x509_util/src/lib.rs @@ -270,7 +270,7 @@ impl CertPool { /// Unix timestamp, measured since the epoch (January 1, 1970, 00:00), /// ignoring leap seconds, in milliseconds. /// This can be unsigned as we never deal with negative timestamps. -pub type UnixTimestamp = u64; +pub type UnixTimestampMillis = u64; #[derive(thiserror::Error, Debug)] pub enum ValidationError { @@ -311,8 +311,8 @@ pub struct ValidationOptions { // Bounds on allowed NotAfter values appearing in the end-entity // certificate. This is only used by CT. - pub not_after_start: Option, - pub not_after_end: Option, + pub not_after_start: Option, + pub not_after_end: Option, } /// Validates a certificate chain. This is not a super strict validation @@ -685,7 +685,7 @@ mod tests { use der::DecodePem; use x509_cert::Certificate; - fn parse_datetime(s: &str) -> UnixTimestamp { + fn parse_datetime(s: &str) -> UnixTimestampMillis { u64::try_from(DateTime::parse_from_rfc3339(s).unwrap().timestamp_millis()).unwrap() } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 030ab5d8..9ef3df09 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -9,6 +9,7 @@ cargo-fuzz = true [dependencies] libfuzzer-sys.workspace = true +tlog_checkpoint.workspace = true tlog_tiles.workspace = true [[bin]] diff --git a/fuzz/fuzz_targets/fuzz_parse_checkpoint.rs b/fuzz/fuzz_targets/fuzz_parse_checkpoint.rs index c5457dfd..9f5d43ae 100644 --- a/fuzz/fuzz_targets/fuzz_parse_checkpoint.rs +++ b/fuzz/fuzz_targets/fuzz_parse_checkpoint.rs @@ -15,7 +15,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use tlog_tiles::checkpoint::CheckpointText; +use tlog_checkpoint::CheckpointText; fuzz_target!(|data: &[u8]| { let _ = CheckpointText::from_bytes(data);