Skip to content

Commit 264564a

Browse files
authored
Add support for Bootstrap Merkle Tree Certificate log (#48)
* Add mtc_api and mtc_worker crates, implementing a (bootstrap) Merkle Tree Certificate Authority log that accepts a X.509 chain and issues a TbsCertificateLogEntry covered by that chain. There are several outstanding TODOs to add necessary validation. * Add associated constants to PendingLogEntry to specify the data path elem (e.g., 'data' for static-ct-api, 'entries' for tlog-tiles, etc.), and an optional 'unhashed' path elem. This allows a log to publish unauthenticated ('unhashed') extra data to a separate path in the public bucket. The intended use case if for the 'bootstrap' X.509 chain in Merkle Tree Certificates. * Add associated constant REQUIRE_CHECKPOINT_TIMESTAMP to LogEntry specifying whether checkpoints require at least one timestamped signature. * Add aux_entry() method to PendingLogEntry to retrieve the auxiliary entry, if configured for the log. * Change get_cached_entry method to get_cached_metadata, since we don't always have a way to retrieve metadata from a LogEntry. * Remove inner() method for LogEntry, since it's never actually needed. * Remove logging_labels() method for LogEntry, since not every generic log has a 'type' field for log entries. Counts of 'add-chain' vs 'add-pre-chain' requests can be recorded elsewhere if needed. * Replace Tile::set_data_with_path() with the slightly more ergonomic TlogTile::with_data_path(). * Use lifetimes to remove 'Cursor' type from TileIterator and avoid some unnecessary cloning. * Refactor to avoid unnecessary clones in 'load' and 'sequence_entries'.
1 parent 895a4a5 commit 264564a

File tree

29 files changed

+14201
-127
lines changed

29 files changed

+14201
-127
lines changed

Cargo.lock

Lines changed: 61 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ct_worker/src/frontend_worker.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ use crate::{load_signing_key, load_witness_key, LookupKey, SequenceMetadata, CON
99
use config::TemporalInterval;
1010
use futures_util::future::try_join_all;
1111
use generic_log_worker::{
12-
ctlog::UploadOptions, get_cached_entry, get_durable_object_stub, init_logging, load_cache_kv,
13-
load_public_bucket, put_cache_entry_metadata, ObjectBackend, ObjectBucket, ENTRY_ENDPOINT,
14-
METRICS_ENDPOINT,
12+
ctlog::UploadOptions, get_cached_metadata, get_durable_object_stub, init_logging,
13+
load_cache_kv, load_public_bucket, put_cache_entry_metadata, ObjectBackend, ObjectBucket,
14+
ENTRY_ENDPOINT, METRICS_ENDPOINT,
1515
};
1616
use log::{debug, info, warn};
1717
use p256::pkcs8::EncodePublicKey;
1818
use serde::Serialize;
1919
use serde_with::{base64::Base64, serde_as};
2020
use sha2::{Digest, Sha256};
2121
use static_ct_api::{AddChainRequest, GetRootsResponse, StaticCTLogEntry};
22-
use tlog_tiles::PendingLogEntry;
22+
use tlog_tiles::{LogEntry, PendingLogEntry};
2323
#[allow(clippy::wildcard_imports)]
2424
use worker::*;
2525

@@ -192,8 +192,9 @@ async fn add_chain_or_pre_chain(
192192

193193
// Check if entry is cached and return right away if so.
194194
let kv = load_cache_kv(env, name)?;
195-
if let Some(entry) = get_cached_entry(&kv, &pending_entry, params.enable_dedup).await? {
195+
if let Some(metadata) = get_cached_metadata(&kv, &pending_entry, params.enable_dedup).await? {
196196
debug!("{name}: Entry is cached");
197+
let entry = StaticCTLogEntry::new(pending_entry, metadata);
197198
let sct = static_ct_api::signed_certificate_timestamp(signing_key, &entry)
198199
.map_err(|e| e.to_string())?;
199200
return Response::from_json(&sct);

crates/generic_log_worker/src/ctlog.rs

Lines changed: 119 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,16 @@ use std::{
3636
};
3737
use thiserror::Error;
3838
use tlog_tiles::{
39-
Hash, HashReader, LogEntry, PathElem, PendingLogEntry, Tile, TileIterator, TlogError, TlogTile,
39+
Hash, HashReader, LogEntry, PendingLogEntry, Tile, TileIterator, TlogError, TlogTile,
4040
TreeWithTimestamp, UnixTimestamp, HASH_SIZE,
4141
};
4242
use tokio::sync::watch::{channel, Receiver, Sender};
4343

4444
/// The maximum tile level is 63 (<c2sp.org/static-ct-api>), so safe to use [`u8::MAX`] as
4545
/// the special level for data tiles. The Go implementation uses -1.
46-
const DATA_TILE_KEY: u8 = u8::MAX;
46+
const DATA_TILE_LEVEL_KEY: u8 = u8::MAX;
47+
/// Same as above, anything above 63 is fine to use as the level key.
48+
const UNHASHED_TILE_LEVEL_KEY: u8 = u8::MAX - 1;
4749
const CHECKPOINT_KEY: &str = "checkpoint";
4850
const STAGING_KEY: &str = "staging";
4951

@@ -323,9 +325,13 @@ impl SequenceState {
323325
"unexpected extension in DO checkpoint"
324326
);
325327

326-
// TODO: This is guaranteed to succeed right now bc we've hardcoded the verifier types. But
327-
// this won't in general. Make an error type for this
328-
let timestamp = timestamp.expect("no verifiers with timestamped signatures were used");
328+
let timestamp = match timestamp {
329+
Some(timestamp) => timestamp,
330+
None if L::REQUIRE_CHECKPOINT_TIMESTAMP => {
331+
bail!("no verifiers with timestamped signatures were used")
332+
}
333+
_ => 0,
334+
};
329335

330336
// Load the checkpoint from the object storage backend, verify it, and compare it to the
331337
// DO storage checkpoint.
@@ -372,33 +378,29 @@ impl SequenceState {
372378
// Fetch the tiles on the right edge, and verify them against the checkpoint.
373379
let mut edge_tiles = HashMap::new();
374380
if c.size() > 0 {
375-
// Fetch the right-most edge tiles by reading the last leaf. TileHashReader will fetch
381+
// Fetch the right-most tree tiles by reading the last leaf. TileHashReader will fetch
376382
// and verify the right tiles as a side-effect.
377383
edge_tiles = read_edge_tiles(object, c.size(), c.hash()).await?;
378384

379385
// Fetch the right-most data tile.
380-
let mut data_tile = edge_tiles
381-
.get(&0)
382-
.ok_or(anyhow!("no level 0 tile found"))?
383-
.clone();
384-
data_tile.tile.set_data_with_path(PathElem::Data);
385-
data_tile.b = object
386-
.fetch(&data_tile.tile.path())
386+
let (level0_tile, level0_tile_bytes) = {
387+
let x = edge_tiles.get(&0).ok_or(anyhow!("no level 0 tile found"))?;
388+
(x.tile, &x.b)
389+
};
390+
let data_tile = level0_tile.with_data_path(L::Pending::DATA_TILE_PATH);
391+
let data_tile_bytes = object
392+
.fetch(&data_tile.path())
387393
.await?
388394
.ok_or(anyhow!("no data tile in object storage"))?;
389-
edge_tiles.insert(DATA_TILE_KEY, data_tile.clone());
390395

391396
// Verify the data tile against the level 0 tile.
392-
let start = u64::from(TlogTile::FULL_WIDTH) * data_tile.tile.level_index();
393-
for (i, entry) in TileIterator::<L>::new(
394-
edge_tiles.get(&DATA_TILE_KEY).unwrap().b.clone(),
395-
data_tile.tile.width() as usize,
396-
)
397-
.enumerate()
397+
let start = u64::from(TlogTile::FULL_WIDTH) * data_tile.level_index();
398+
for (i, entry) in
399+
TileIterator::<L>::new(&data_tile_bytes, data_tile.width() as usize).enumerate()
398400
{
399401
let got = entry?.merkle_tree_leaf();
400-
let exp = edge_tiles.get(&0).unwrap().tile.hash_at_index(
401-
&edge_tiles.get(&0).unwrap().b,
402+
let exp = level0_tile.hash_at_index(
403+
level0_tile_bytes,
402404
tlog_tiles::stored_hash_index(0, start + i as u64),
403405
)?;
404406
if got != exp {
@@ -408,6 +410,32 @@ impl SequenceState {
408410
);
409411
}
410412
}
413+
414+
// Store the data tile.
415+
edge_tiles.insert(
416+
DATA_TILE_LEVEL_KEY,
417+
TileWithBytes {
418+
tile: data_tile,
419+
b: data_tile_bytes,
420+
},
421+
);
422+
423+
// Fetch and store the right-most auxiliary tile, if configured.
424+
if let Some(path_elem) = L::Pending::AUX_TILE_PATH {
425+
let aux_tile = level0_tile.with_data_path(path_elem);
426+
let aux_tile_bytes = object
427+
.fetch(&aux_tile.path())
428+
.await?
429+
.ok_or(anyhow!("no auxiliary tile in object storage"))?;
430+
431+
edge_tiles.insert(
432+
UNHASHED_TILE_LEVEL_KEY,
433+
TileWithBytes {
434+
tile: aux_tile,
435+
b: aux_tile_bytes,
436+
},
437+
);
438+
}
411439
}
412440

413441
for tile in &edge_tiles {
@@ -601,12 +629,23 @@ async fn sequence_entries<L: LogEntry>(
601629
// Load the current partial data tile, if any.
602630
let mut tile_uploads: Vec<UploadAction> = Vec::new();
603631
let mut edge_tiles = sequence_state.edge_tiles.clone();
604-
let mut data_tile: Vec<u8> = Vec::new();
605-
if let Some(t) = edge_tiles.get(&DATA_TILE_KEY) {
632+
let mut data_tile = Vec::new();
633+
if let Some(t) = edge_tiles.get(&DATA_TILE_LEVEL_KEY) {
606634
if t.tile.width() < TlogTile::FULL_WIDTH {
607635
data_tile.clone_from(&t.b);
608636
}
609637
}
638+
639+
// Load the current partial auxiliary tile, if configured.
640+
let mut aux_tile = Vec::new();
641+
if L::Pending::AUX_TILE_PATH.is_some() {
642+
if let Some(t) = edge_tiles.get(&UNHASHED_TILE_LEVEL_KEY) {
643+
if t.tile.width() < TlogTile::FULL_WIDTH {
644+
aux_tile.clone_from(&t.b);
645+
}
646+
}
647+
}
648+
610649
let mut overlay = HashMap::new();
611650
let mut n = old_size;
612651
let mut sequenced_metadata = Vec::with_capacity(entries.len());
@@ -618,6 +657,11 @@ async fn sequence_entries<L: LogEntry>(
618657
cache_metadata.push((entry.lookup_key(), metadata));
619658
sequenced_metadata.push((sender, metadata));
620659

660+
// Write to the auxiliary tile, if configured.
661+
if L::Pending::AUX_TILE_PATH.is_some() {
662+
aux_tile.extend(entry.aux_entry());
663+
}
664+
621665
let sequenced_entry = L::new(entry, metadata);
622666
let tile_leaf = sequenced_entry.to_data_tile_entry();
623667
let merkle_tree_leaf = sequenced_entry.merkle_tree_leaf();
@@ -649,22 +693,33 @@ async fn sequence_entries<L: LogEntry>(
649693

650694
// If the data tile is full, stage it.
651695
if n % u64::from(TlogTile::FULL_WIDTH) == 0 {
652-
stage_data_tile(n, &mut edge_tiles, &mut tile_uploads, &data_tile);
653696
metrics
654697
.seq_data_tile_size
655698
.with_label_values(&["full"])
656699
.observe(data_tile.len().as_f64());
657-
data_tile.clear();
700+
stage_data_tile::<L>(
701+
n,
702+
&mut edge_tiles,
703+
&mut tile_uploads,
704+
std::mem::take(&mut data_tile),
705+
std::mem::take(&mut aux_tile),
706+
);
658707
}
659708
}
660709

661710
// Stage leftover partial data tile, if any.
662711
if n != old_size && n % u64::from(TlogTile::FULL_WIDTH) != 0 {
663-
stage_data_tile(n, &mut edge_tiles, &mut tile_uploads, &data_tile);
664712
metrics
665713
.seq_data_tile_size
666714
.with_label_values(&["partial"])
667715
.observe(data_tile.len().as_f64());
716+
stage_data_tile::<L>(
717+
n,
718+
&mut edge_tiles,
719+
&mut tile_uploads,
720+
std::mem::take(&mut data_tile),
721+
std::mem::take(&mut aux_tile),
722+
);
668723
}
669724

670725
// Produce and stage new tree tiles.
@@ -815,28 +870,45 @@ async fn sequence_entries<L: LogEntry>(
815870
Ok(())
816871
}
817872

818-
// Stage a data tile. This is used as a helper function for [`sequence_entries`].
819-
fn stage_data_tile(
873+
// Stage a data tile, and if configured an auxiliary tile.
874+
// This is used as a helper function for [`sequence_entries`].
875+
fn stage_data_tile<L: LogEntry>(
820876
n: u64,
821877
edge_tiles: &mut HashMap<u8, TileWithBytes>,
822878
tile_uploads: &mut Vec<UploadAction>,
823-
data_tile: &[u8],
879+
data_tile: Vec<u8>,
880+
aux_tile: Vec<u8>,
824881
) {
825-
let mut tile = TlogTile::from_index(tlog_tiles::stored_hash_index(0, n - 1));
826-
tile.set_data_with_path(PathElem::Data);
882+
let tile = TlogTile::from_index(tlog_tiles::stored_hash_index(0, n - 1))
883+
.with_data_path(L::Pending::DATA_TILE_PATH);
827884
edge_tiles.insert(
828-
DATA_TILE_KEY,
885+
DATA_TILE_LEVEL_KEY,
829886
TileWithBytes {
830887
tile,
831-
b: data_tile.to_owned(),
888+
b: data_tile.clone(),
832889
},
833890
);
834-
let action = UploadAction {
891+
tile_uploads.push(UploadAction {
835892
key: tile.path(),
836-
data: data_tile.to_owned(),
893+
data: data_tile,
837894
opts: OPTS_DATA_TILE.clone(),
838-
};
839-
tile_uploads.push(action);
895+
});
896+
if let Some(path_elem) = L::Pending::AUX_TILE_PATH {
897+
let tile =
898+
TlogTile::from_index(tlog_tiles::stored_hash_index(0, n - 1)).with_data_path(path_elem);
899+
edge_tiles.insert(
900+
UNHASHED_TILE_LEVEL_KEY,
901+
TileWithBytes {
902+
tile,
903+
b: aux_tile.clone(),
904+
},
905+
);
906+
tile_uploads.push(UploadAction {
907+
key: tile.path(),
908+
data: aux_tile,
909+
opts: OPTS_DATA_TILE.clone(),
910+
});
911+
}
840912
}
841913

842914
/// Applies previously-staged uploads to the object backend where contents can be retrieved by log clients.
@@ -2224,17 +2296,22 @@ mod tests {
22242296
let leaf_hashes = read_tile_hashes(&self.object, c.size(), c.hash(), &indexes)
22252297
.map_err(|e| anyhow!(e))?;
22262298

2227-
let mut last_tile = TlogTile::from_index(tlog_tiles::stored_hash_count(c.size() - 1));
2228-
last_tile.set_data_with_path(PathElem::Data);
2299+
let last_tile = TlogTile::from_index(tlog_tiles::stored_hash_count(c.size() - 1))
2300+
.with_data_path(StaticCTPendingLogEntry::DATA_TILE_PATH);
22292301

22302302
for n in 0..last_tile.level_index() {
22312303
let tile = if n == last_tile.level_index() {
22322304
last_tile
22332305
} else {
2234-
TlogTile::new(0, n, TlogTile::FULL_WIDTH, Some(PathElem::Data))
2306+
TlogTile::new(
2307+
0,
2308+
n,
2309+
TlogTile::FULL_WIDTH,
2310+
Some(StaticCTPendingLogEntry::DATA_TILE_PATH),
2311+
)
22352312
};
22362313
for (i, entry) in TileIterator::<StaticCTLogEntry>::new(
2237-
block_on(self.object.fetch(&tile.path()))
2314+
&block_on(self.object.fetch(&tile.path()))
22382315
.map_err(|e| anyhow!(e))?
22392316
.unwrap(),
22402317
tile.width() as usize,

0 commit comments

Comments
 (0)