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
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,34 @@ use ed25519_dalek::{
VerifyingKey as Ed25519VerifyingKey,
};
use length_prefixed::WriteLengthPrefixedBytesExt;
use signed_note::{compute_key_id, KeyName, NoteError, NoteSignature, NoteVerifier, SignatureType};
use sha2::{Digest, Sha256};
use signed_note::{KeyName, NoteError, NoteSignature, NoteVerifier};
use tlog_tiles::{CheckpointSigner, CheckpointText, Hash, LeafIndex, UnixTimestamp};

#[derive(Clone)]
pub struct TrustAnchorID(pub Vec<u8>);
pub struct MTCSubtreeCosigner {
v: MTCSubtreeNoteVerifier,
use crate::{RelativeOid, ID_RDNA_TRUSTANCHOR_ID};

pub type TrustAnchorID = RelativeOid;

pub struct MtcCosigner {
v: MtcNoteVerifier,
k: Ed25519SigningKey,
}

impl MTCSubtreeCosigner {
pub fn new(
impl MtcCosigner {
/// Return a checkpoint cosigner.
pub fn new_checkpoint(
cosigner_id: TrustAnchorID,
log_id: TrustAnchorID,
name: KeyName,
k: Ed25519SigningKey,
) -> Self {
Self {
v: MTCSubtreeNoteVerifier::new(cosigner_id, log_id, name, k.verifying_key()),
v: MtcNoteVerifier::new_checkpoint(cosigner_id, log_id, k.verifying_key()),
k,
}
}
}

impl MTCSubtreeCosigner {
impl MtcCosigner {
/// Compute an Ed25519 subtree cosignature as defined in
/// <https://www.ietf.org/archive/id/draft-davidben-tls-merkle-tree-certs-05.html#name-signature-format>.
///
Expand All @@ -57,14 +60,14 @@ impl MTCSubtreeCosigner {
Ok(self.k.try_sign(&serialized)?.to_vec())
}

/// Return the log ID as bytes.
pub fn log_id(&self) -> &[u8] {
&self.v.log_id.0
/// Return the log ID.
pub fn log_id(&self) -> &TrustAnchorID {
&self.v.log_id
}

/// Return the cosigner ID as bytes.
pub fn cosigner_id(&self) -> &[u8] {
&self.v.cosigner_id.0
/// Return the cosigner ID.
pub fn cosigner_id(&self) -> &TrustAnchorID {
&self.v.cosigner_id
}

/// Return the verifying key as bytes.
Expand All @@ -74,7 +77,7 @@ impl MTCSubtreeCosigner {
}

/// Support signing tlog-checkpoint with the subtree cosigner.
impl CheckpointSigner for MTCSubtreeCosigner {
impl CheckpointSigner for MtcCosigner {
fn name(&self) -> &KeyName {
self.v.name()
}
Expand All @@ -98,33 +101,35 @@ impl CheckpointSigner for MTCSubtreeCosigner {
}
}

/// [`MTCSubtreeNoteVerifier`] is the verifier for subtree cosignatures defined in <https://www.ietf.org/archive/id/draft-davidben-tls-merkle-tree-certs-05.html#name-cosigners>
/// [`MtcNoteVerifier`] is the verifier for subtree cosignatures defined in <https://www.ietf.org/archive/id/draft-davidben-tls-merkle-tree-certs-05.html#name-cosigners>
/// It currently supports only Ed25519 signatures.
#[derive(Clone)]
pub struct MTCSubtreeNoteVerifier {
pub struct MtcNoteVerifier {
cosigner_id: TrustAnchorID,
log_id: TrustAnchorID,
name: KeyName,
id: u32,
verifying_key: Ed25519VerifyingKey,
}

impl MTCSubtreeNoteVerifier {
pub fn new(
impl MtcNoteVerifier {
/// Return a checkpoint verifier.
pub fn new_checkpoint(
cosigner_id: TrustAnchorID,
log_id: TrustAnchorID,
name: KeyName,
verifying_key: Ed25519VerifyingKey,
) -> Self {
let name = KeyName::new(format!("oid/{}.{}", ID_RDNA_TRUSTANCHOR_ID, log_id)).unwrap();

let id = {
// TODO what signature algorithm to use for mtc-subtree/v1?
let pubkey = [
&[SignatureType::Undefined as u8],
verifying_key.to_bytes().as_slice(),
]
.concat();
compute_key_id(&name, &pubkey)
let mut hasher = Sha256::new();
hasher.update(name.as_str().as_bytes());
hasher.update([0x0a, 0xff]);
hasher.update(b"mtc-checkpoint/v1");
let result = hasher.finalize();
u32::from_be_bytes(result[0..4].try_into().unwrap())
};

Self {
cosigner_id,
log_id,
Expand All @@ -136,7 +141,7 @@ impl MTCSubtreeNoteVerifier {
}

/// Support verifying signed note signatures created using the subtree cosigner.
impl NoteVerifier for MTCSubtreeNoteVerifier {
impl NoteVerifier for MtcNoteVerifier {
fn name(&self) -> &KeyName {
&self.name
}
Expand Down Expand Up @@ -182,6 +187,9 @@ impl NoteVerifier for MTCSubtreeNoteVerifier {
///
/// opaque HashValue[HASH_SIZE];
///
/// /* From Section 4.1 of draft-ietf-tls-trust-anchor-ids */
/// opaque TrustAnchorID<1..2^8-1>;
///
/// struct {
/// TrustAnchorID log_id;
/// uint64 start;
Expand All @@ -190,8 +198,7 @@ impl NoteVerifier for MTCSubtreeNoteVerifier {
/// } MTCSubtree;
///
/// struct {
/// uint8 label[14] = "mtc-subtree/v1";
/// uint8 separator = 0;
/// uint8 label[16] = "mtc-subtree/v1\n\0";
/// TrustAnchorID cosigner_id;
/// MTCSubtree subtree;
/// } MTCSubtreeSignatureInput;
Expand All @@ -207,9 +214,11 @@ fn serialize_mtc_subtree_signature_input(
end: LeafIndex,
root_hash: &Hash,
) -> Vec<u8> {
let mut buffer: Vec<u8> = b"mtc-subtree/v1\x00".to_vec();
buffer.write_length_prefixed(&cosigner_id.0, 1).unwrap();
buffer.write_length_prefixed(&log_id.0, 1).unwrap();
let mut buffer: Vec<u8> = b"mtc-subtree/v1\n\x00".to_vec();
buffer
.write_length_prefixed(cosigner_id.as_bytes(), 1)
.unwrap();
buffer.write_length_prefixed(log_id.as_bytes(), 1).unwrap();
buffer.write_u64::<BigEndian>(start).unwrap();
buffer.write_u64::<BigEndian>(end).unwrap();
buffer.extend(root_hash.0);
Expand All @@ -225,6 +234,7 @@ mod tests {
use super::*;
use rand::rngs::OsRng;
use signed_note::VerifierList;
use std::str::FromStr;

#[test]
fn test_cosignature_v1_sign_verify() {
Expand All @@ -238,11 +248,9 @@ mod tests {
let tree = TreeWithTimestamp::new(tree_size, record_hash(b"hello world"), timestamp);
let signer = {
let sk = Ed25519SigningKey::generate(&mut rng);
let name = KeyName::new("my-signer".into()).unwrap();
MTCSubtreeCosigner::new(
TrustAnchorID(vec![0, 1, 2]),
TrustAnchorID(vec![3, 4, 5]),
name,
MtcCosigner::new_checkpoint(
TrustAnchorID::from_str("1.2.3").unwrap(),
TrustAnchorID::from_str("4.5.6").unwrap(),
sk,
)
};
Expand Down
6 changes: 3 additions & 3 deletions crates/mtc_api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// 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

mod cosigner;
mod landmark;
mod relative_oid;
mod subtree_cosignature;
pub use cosigner::*;
pub use landmark::*;
pub use relative_oid::*;
pub use subtree_cosignature::*;

use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use der::{
Expand Down Expand Up @@ -62,7 +62,7 @@ impl MtcSignature {
fn to_bytes(&self) -> Vec<u8> {
let mut buffer = Vec::new();
buffer
.write_length_prefixed(&self.cosigner_id.0, 1)
.write_length_prefixed(self.cosigner_id.as_bytes(), 1)
.unwrap();
buffer.write_length_prefixed(&self.signature, 2).unwrap();
buffer
Expand Down
53 changes: 51 additions & 2 deletions crates/mtc_api/src/relative_oid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ use std::str::FromStr;
/// ASN.1 `RELATIVE OID`.
///
/// TODO upstream this to the `der` crate.
#[derive(Clone)]
pub struct RelativeOid {
ber: Vec<u8>,
arcs: Vec<u32>,
}

impl RelativeOid {
Expand All @@ -29,7 +31,10 @@ impl RelativeOid {
if ber.len() > 255 {
return Err(MtcError::Dynamic("invalid relative OID".into()));
}
Ok(Self { ber })
Ok(Self {
ber,
arcs: arcs.to_vec(),
})
}

/// Returns the DER-encoded content bytes.
Expand All @@ -38,6 +43,15 @@ impl RelativeOid {
}
}

impl std::fmt::Display for RelativeOid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for arc in self.arcs.iter().take(self.arcs.len() - 1) {
write!(f, "{}.", arc)?;
}
write!(f, "{}", self.arcs[self.arcs.len() - 1])
}
}

impl FromStr for RelativeOid {
type Err = MtcError;
/// Parse the [`RelativeOid`] from a decimal-dotted string.
Expand All @@ -60,9 +74,44 @@ mod tests {
use super::*;

#[test]
fn encode_decode() {
fn encode_tagged() {
let relative_oid = RelativeOid::from_str("13335.2").unwrap();
let any = Any::new(Tag::RelativeOid, relative_oid.as_bytes()).unwrap();
assert_eq!(any.to_der().unwrap(), b"\x0d\x03\xe8\x17\x02");
}

#[test]
fn encode_string() {
let relative_oid = RelativeOid::from_str("13335.2").unwrap();
assert_eq!(relative_oid.to_string(), "13335.2");
}

#[test]
fn decode_string_encode_bytes() {
struct TestCase {
s: &'static str,
b: &'static [u8],
}
for TestCase { s, b } in [
TestCase {
s: "237",
b: &[129, 109],
},
TestCase {
s: "1.2.3.4",
b: &[1, 2, 3, 4],
},
TestCase {
s: "13335.2",
b: &[232, 23, 2],
},
TestCase {
s: "44363.48.10",
b: &[130, 218, 75, 48, 10],
},
] {
let relative_oid = RelativeOid::from_str(s).unwrap();
assert_eq!(relative_oid.as_bytes(), b);
}
}
}
4 changes: 2 additions & 2 deletions crates/mtc_worker/src/cleaner_do.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::time::Duration;

use crate::{load_cosigner, load_origin, CONFIG};
use crate::{load_checkpoint_cosigner, load_origin, CONFIG};
use generic_log_worker::{get_durable_object_name, CleanerConfig, GenericCleaner, CLEANER_BINDING};
use mtc_api::BootstrapMtcPendingLogEntry;
use signed_note::VerifierList;
Expand All @@ -26,7 +26,7 @@ impl DurableObject for Cleaner {
origin: load_origin(name),
data_path: BootstrapMtcPendingLogEntry::DATA_TILE_PATH,
aux_path: BootstrapMtcPendingLogEntry::AUX_TILE_PATH,
verifiers: VerifierList::new(vec![load_cosigner(&env, name).verifier()]),
verifiers: VerifierList::new(vec![load_checkpoint_cosigner(&env, name).verifier()]),
clean_interval: Duration::from_secs(params.clean_interval_secs),
};

Expand Down
23 changes: 10 additions & 13 deletions crates/mtc_worker/src/frontend_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

//! Entrypoint for the static CT submission APIs.

use crate::{load_cosigner, load_origin, load_roots, SequenceMetadata, CONFIG};
use crate::{load_checkpoint_cosigner, load_origin, load_roots, SequenceMetadata, CONFIG};
use der::{
asn1::{SetOfVec, UtcTime, Utf8StringRef},
Any, Encode, Tag,
Expand Down Expand Up @@ -45,10 +45,8 @@ const UNKNOWN_LOG_MSG: &str = "unknown log";
struct MetadataResponse<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
description: &'a Option<String>,
#[serde_as(as = "Base64")]
log_id: &'a [u8],
#[serde_as(as = "Base64")]
cosigner_id: &'a [u8],
log_id: String,
cosigner_id: String,
#[serde_as(as = "Base64")]
cosigner_public_key: &'a [u8],
submission_url: &'a str,
Expand Down Expand Up @@ -77,9 +75,8 @@ pub struct GetCertificateResponse {
/// GET response structure for the `/get-landmark-bundle` endpoint
#[serde_as]
#[derive(Serialize, Deserialize)]
pub struct GetLandmarkBundleResponse {
#[serde_as(as = "Base64")]
pub checkpoint: Vec<u8>,
pub struct GetLandmarkBundleResponse<'a> {
pub checkpoint: &'a str,
pub subtrees: Vec<SubtreeWithConsistencyProof>,
}

Expand Down Expand Up @@ -224,11 +221,11 @@ async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
.get("/logs/:log/metadata", |_req, ctx| {
let name = ctx.data;
let params = &CONFIG.logs[name];
let cosigner = load_cosigner(&ctx.env, name);
let cosigner = load_checkpoint_cosigner(&ctx.env, name);
Response::from_json(&MetadataResponse {
description: &params.description,
log_id: cosigner.log_id(),
cosigner_id: cosigner.cosigner_id(),
log_id: cosigner.log_id().to_string(),
cosigner_id: cosigner.cosigner_id().to_string(),
cosigner_public_key: cosigner.verifying_key(),
submission_url: &params.submission_url,
monitoring_url: if params.monitoring_url.is_empty() {
Expand Down Expand Up @@ -446,7 +443,7 @@ async fn get_landmark_bundle(env: &Env, name: &str) -> Result<Response> {
}

Response::from_json(&GetLandmarkBundleResponse {
checkpoint: checkpoint_bytes,
checkpoint: std::str::from_utf8(&checkpoint_bytes).unwrap(),
subtrees,
})
}
Expand All @@ -462,7 +459,7 @@ async fn get_current_checkpoint(
.ok_or("no checkpoint in object storage".to_string())?;

let origin = &load_origin(name);
let verifiers = &VerifierList::new(vec![load_cosigner(env, name).verifier()]);
let verifiers = &VerifierList::new(vec![load_checkpoint_cosigner(env, name).verifier()]);
let (checkpoint, _timestamp) =
open_checkpoint(origin.as_str(), verifiers, now_millis(), &checkpoint_bytes)
.map_err(|e| e.to_string())?;
Expand Down
Loading