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
48 changes: 48 additions & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ jsonschema = "0.30"
length_prefixed = { path = "crates/length_prefixed" }
libfuzzer-sys = "0.4"
log = { version = "0.4" }
ml-dsa = "=0.1.0-rc.8"
Comment thread
lukevalenta marked this conversation as resolved.
bootstrap_mtc_api = { version = "0.2.0", path = "crates/bootstrap_mtc_api" }
p256 = { version = "0.14.0-rc.8", features = ["ecdsa"] }
p384 = { version = "0.14.0-rc.8", features = ["ecdsa"] }
Expand Down Expand Up @@ -141,4 +142,4 @@ rsa = { version = "0.10.0-rc.17", default-features = false, features = ["sha2",
hashbrown = "0.15"
spki = "0.8"
const-oid = "0.10"
pkcs8 = { version = "0.11.0-rc.11", features = ["pem"] }
pkcs8 = { version = "=0.11.0-rc.11", features = ["pem"] }
63 changes: 59 additions & 4 deletions crates/signed_note/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ pub enum SignatureType {
Ed25519 = 0x01,
CosignatureV1 = 0x04,
RFC6962TreeHead = 0x05,
MlDsa44 = 0x06,
Undefined = 0xff,
}

Expand All @@ -243,6 +244,7 @@ impl TryFrom<u8> for SignatureType {
0x01 => Ok(SignatureType::Ed25519),
0x04 => Ok(SignatureType::CosignatureV1),
0x05 => Ok(SignatureType::RFC6962TreeHead),
0x06 => Ok(SignatureType::MlDsa44),
0xff => Ok(SignatureType::Undefined),
_ => Err(NoteError::UnknownSignatureType),
}
Expand All @@ -253,13 +255,36 @@ impl TryFrom<u8> for SignatureType {
pub struct KeyName(String);

impl KeyName {
/// Return a valid key name according to <https://c2sp.org/signed-note#format>.
/// It must be non-empty and not have any Unicode spaces or pluses.
/// Maximum length, in bytes, of a key name's UTF-8 encoding.
///
/// The signed-note spec does not itself impose a length cap, but every
/// C2SP-defined consumer that embeds a key name in a length-prefixed
/// wire format does so as a TLS-presentation `opaque<1..2^8-1>` —
/// notably the `cosigner_name` and `log_origin` fields of
/// [`tlog-cosignature`][cosig]'s ML-DSA-44 `cosigned_message` struct.
/// Capping `KeyName` at the same 255-byte limit means consumers can
/// rely on `as_str().as_bytes().len() <= MAX_LEN` without a redundant
/// runtime check at every encoding site, and an over-long key name
/// fails closed at construction rather than at first use.
///
/// [cosig]: https://c2sp.org/tlog-cosignature
pub const MAX_LEN: usize = 255;

/// Return a valid key name according to <https://c2sp.org/signed-note#format>:
/// non-empty, with no Unicode spaces or `+` characters. Additionally
/// capped at [`Self::MAX_LEN`] bytes for compatibility with C2SP
/// consumers (see the constant for rationale).
///
/// # Errors
/// Will return `Err` if the key name is empty or has Unicode spaces or pluses.
/// Returns [`NoteError::InvalidKeyName`] if the key name is empty,
/// contains Unicode whitespace or `+`, or exceeds
/// [`Self::MAX_LEN`] bytes when UTF-8-encoded.
pub fn new(name: String) -> Result<Self, NoteError> {
if name.is_empty() || name.chars().any(char::is_whitespace) || name.contains('+') {
if name.is_empty()
|| name.len() > Self::MAX_LEN
|| name.chars().any(char::is_whitespace)
|| name.contains('+')
{
Err(NoteError::InvalidKeyName)
} else {
Ok(Self(name))
Expand Down Expand Up @@ -950,4 +975,34 @@ mod tests {
.unwrap_err();
assert!(matches!(err, NoteError::MismatchedVerifier));
}

/// `KeyName::new` enforces the signed-note key-name rules plus the
/// 255-byte cap (see `KeyName::MAX_LEN` for rationale).
#[test]
fn test_key_name_validation() {
// Spec rules: empty, whitespace, '+' all rejected.
assert!(KeyName::new(String::new()).is_err());
assert!(KeyName::new("a b".into()).is_err());
assert!(KeyName::new("a+b".into()).is_err());
// Unicode whitespace, not just ASCII.
assert!(KeyName::new("a\u{00a0}b".into()).is_err());

// Length cap: 255 bytes ok, 256+ rejected.
let max = "a".repeat(KeyName::MAX_LEN);
assert_eq!(max.len(), KeyName::MAX_LEN);
KeyName::new(max).expect("max-length name accepted");

let over = "a".repeat(KeyName::MAX_LEN + 1);
let err = KeyName::new(over).unwrap_err();
assert!(matches!(err, NoteError::InvalidKeyName));

// The cap is in bytes, not characters: a multi-byte-char string
// whose char count is ≤ MAX_LEN but byte length exceeds it is
// rejected. "é" is 2 bytes in UTF-8.
let two_byte_chars = "é".repeat(KeyName::MAX_LEN / 2 + 1);
assert!(two_byte_chars.chars().count() <= KeyName::MAX_LEN);
assert!(two_byte_chars.len() > KeyName::MAX_LEN);
let err = KeyName::new(two_byte_chars).unwrap_err();
assert!(matches!(err, NoteError::InvalidKeyName));
}
}
2 changes: 2 additions & 0 deletions crates/tlog_cosignature/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ crate-type = ["rlib"]
[dependencies]
byteorder.workspace = true
ed25519-dalek.workspace = true
length_prefixed.workspace = true
ml-dsa.workspace = true
signed_note.workspace = true
tlog_tiles.workspace = true

Expand Down
16 changes: 12 additions & 4 deletions crates/tlog_cosignature/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@
//! inclusion proof.
//!
//! This crate provides signers and verifiers for the cosignature formats
//! specified by the C2SP document. Today only the Ed25519 `cosignature/v1`
//! format is implemented (in the [`cosignature_v1`] module); the
//! ML-DSA-44 `subtree/v1` format will be added in a follow-up alongside
//! support for signing arbitrary subtrees.
//! specified by the C2SP document:
//!
//! - [`cosignature_v1`]: the legacy Ed25519 timestamped checkpoint
//! cosignature (signed-note algorithm byte `0x04`). Locked to
//! start = 0; signs the checkpoint note body verbatim.
//! - [`subtree_v1`]: an ML-DSA-44 cosignature over an arbitrary subtree
//! (signed-note algorithm byte `0x06`). The checkpoint case
//! (`start = 0`, `end = size`) is interchangeable with `cosignature/v1`
//! semantically; non-zero `start` cosignatures are used by tlog-witness'
//! `sign-subtree` API and by Merkle Tree certificates.
//!
//! HTTP transports for requesting cosignatures are out of scope; see
//! [`tlog_witness`] for parsers/serializers of the
Expand All @@ -23,5 +29,7 @@
//! [`tlog_witness`]: https://docs.rs/tlog_witness

pub mod cosignature_v1;
pub mod subtree_v1;

pub use cosignature_v1::{CosignatureV1CheckpointSigner, CosignatureV1NoteVerifier};
pub use subtree_v1::{SubtreeV1CheckpointSigner, SubtreeV1NoteVerifier};
Loading
Loading