Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ members = [
"crates/tlog_core",
"crates/tlog_cosignature",
"crates/tlog_entry",
"crates/tlog_mirror",
"crates/tlog_tiles",
"crates/tlog_tiles_wasm",
"crates/tlog_witness",
Expand Down Expand Up @@ -49,6 +50,7 @@ default-members = [
"crates/tlog_core",
"crates/tlog_cosignature",
"crates/tlog_entry",
"crates/tlog_mirror",
"crates/tlog_tiles",
"crates/tlog_tiles_wasm",
"crates/tlog_witness",
Expand Down Expand Up @@ -80,6 +82,10 @@ opt-level = 3
debug = 1

[workspace.dependencies]
# aes-gcm-siv has no stable release on the current RustCrypto trait
# batch (aead 0.6), so it stays on a pre-release (as does rsa, below);
# the rc still resolves aead/cipher up to their stable releases.
aes-gcm-siv = { version = "0.12.0-rc.3", default-features = false, features = ["aes", "alloc"] }
anyhow = "1.0"
axum = { version = "0.8", default-features = false, features = ["json", "matched-path"] }
base64 = "0.22"
Expand Down Expand Up @@ -132,6 +138,7 @@ 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_entry = { path = "crates/tlog_entry", version = "0.2.0" }
tlog_mirror = { path = "crates/tlog_mirror", version = "0.2.0" }
tlog_tiles = { path = "crates/tlog_tiles", version = "0.2.0" }
tlog_witness = { path = "crates/tlog_witness", version = "0.2.0" }
tokio = { version = "1", features = ["sync"] }
Expand Down
29 changes: 29 additions & 0 deletions crates/tlog_mirror/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "tlog_mirror"
# Held back from crates.io while c2sp.org/tlog-mirror stabilizes. Lift
# the publish gate (and remove this comment) once the spec is final.
publish = false
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
readme.workspace = true
homepage.workspace = true
repository.workspace = true
description = "An implementation of c2sp.org/tlog-mirror"
categories = ["cryptography"]
keywords = ["transparency", "mirror", "checkpoint", "crypto", "pki"]

[package.metadata.release]
release = false

[lib]
crate-type = ["rlib"]

[dependencies]
aes-gcm-siv.workspace = true
base64.workspace = true
byteorder.workspace = true
rand.workspace = true
thiserror.workspace = true
tlog_core.workspace = true
1 change: 1 addition & 0 deletions crates/tlog_mirror/LICENSE
81 changes: 81 additions & 0 deletions crates/tlog_mirror/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) 2025-2026 Cloudflare, Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause

//! Error types for `tlog_mirror`.

use thiserror::Error;

/// Errors returned when parsing tlog-mirror wire-format messages.
#[derive(Debug, Error)]
pub enum ParseError {
/// An IO error occurred while reading from the input stream.
#[error("io: {0}")]
Io(#[from] std::io::Error),

/// The `log_origin_size` u16 prefix advertised more bytes than were
/// available in the input.
#[error("log_origin truncated: advertised {advertised} bytes")]
LogOriginTruncated {
/// Size advertised by the wire `log_origin_size` u16.
advertised: u16,
},

/// The `log_origin` bytes were not valid UTF-8.
#[error("log_origin is not valid UTF-8")]
LogOriginNotUtf8,

/// `upload_start` was greater than `upload_end`.
#[error("upload_start ({start}) > upload_end ({end})")]
UploadRangeInverted {
/// `upload_start` from the wire.
start: u64,
/// `upload_end` from the wire.
end: u64,
},

/// `num_hashes` for a subtree consistency proof exceeded the spec's
/// maximum of 63.
#[error("num_hashes {0} exceeds spec maximum of 63")]
TooManyHashes(u8),

/// An entry package's entry count exceeded the spec's per-package
/// maximum of [`PACKAGE_ALIGNMENT`](crate::PACKAGE_ALIGNMENT) (256).
#[error("entry package has {0} entries, spec maximum is 256")]
TooManyEntries(u64),

/// The `text/x.tlog.mirror-info` body did not have exactly three
/// newline-terminated lines.
#[error("mirror-info body is malformed: {0}")]
MalformedMirrorInfo(&'static str),

/// A decimal field in the `text/x.tlog.mirror-info` body could not be
/// parsed as a `u64`.
#[error("mirror-info decimal field {field} could not be parsed: {value:?}")]
InvalidDecimal {
/// Field name (`tree_size` or `next_entry`).
field: &'static str,
/// Verbatim bytes from the wire.
value: String,
},

/// The base64-encoded ticket in a `text/x.tlog.mirror-info` body could
/// not be decoded.
#[error("mirror-info ticket is not valid base64")]
InvalidTicketBase64,
}

/// Errors returned by [`TicketSealer`](crate::TicketSealer).
#[derive(Debug, Error)]
pub enum TicketError {
/// The sealed ticket was shorter than the prepended nonce
/// ([`NONCE_LEN`](crate::NONCE_LEN), 12 bytes) plus the AEAD tag
/// ([`TAG_LEN`](crate::TAG_LEN), 16 bytes), so it cannot possibly be a
/// valid ticket.
#[error("sealed ticket too short: {0} bytes, need at least 28")]
TooShort(usize),

/// AEAD decryption failed: the ticket, tag, key, or associated data
/// (`aad`) did not match.
#[error("ticket authentication failed")]
AuthenticationFailed,
}
29 changes: 29 additions & 0 deletions crates/tlog_mirror/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) 2025-2026 Cloudflare, Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause

//! An implementation of [c2sp.org/tlog-mirror](https://c2sp.org/tlog-mirror).
//!
//! Wire-format pieces for building a tlog mirror, scoped to spec-level
//! concerns:
//!
//! * [`wire::AddEntriesRequestHeader`] and [`wire::EntryPackage`] for the
//! `add-entries` request body.
//! * [`wire::MirrorInfo`] for the `text/x.tlog.mirror-info` 409 Conflict
//! response body.
//! * [`TicketSealer`] for the default opaque-ticket scheme.
//!
//! A mirror's `add-checkpoint` endpoint is expected to reuse the witness
//! wire format via the `tlog_witness` crate. Storage, retention, and
//! pruning policy are out of scope.

pub mod ticket;
pub mod wire;

mod error;

pub use error::{ParseError, TicketError};
pub use ticket::{NONCE_LEN, TAG_LEN, TicketSealer};
pub use wire::{
AddEntriesRequestHeader, EntryPackage, MAX_HASHES_PER_PROOF, MIRROR_INFO_CONTENT_TYPE,
MirrorInfo, PACKAGE_ALIGNMENT, PackageRanges, package_ranges,
};
Loading
Loading