use core::fmt;
use crate::hash::Hash;
use crate::merkle::TreeHead;
use super::base64;
const SIGNATURE_PREFIX: &str = "\u{2014} ";
fn is_forbidden_in_field(c: char) -> bool {
c.is_whitespace() || c == '+' || c.is_control()
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct KeyName(String);
impl KeyName {
pub const MAX_LEN: usize = 128;
pub fn new(name: impl Into<String>) -> Result<Self, NoteError> {
let name = name.into();
if name.is_empty() || name.len() > Self::MAX_LEN || name.chars().any(is_forbidden_in_field)
{
return Err(NoteError::MalformedKeyName);
}
Ok(Self(name))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for KeyName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Origin(String);
impl Origin {
pub const MAX_LEN: usize = 256;
pub fn new(origin: impl Into<String>) -> Result<Self, NoteError> {
let origin = origin.into();
if origin.is_empty()
|| origin.len() > Self::MAX_LEN
|| origin.chars().any(is_forbidden_in_field)
{
return Err(NoteError::MalformedOrigin);
}
Ok(Self(origin))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Origin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NoteSignature {
pub name: KeyName,
pub key_hash: [u8; 4],
pub value: Vec<u8>,
}
impl NoteSignature {
#[must_use]
pub fn to_line(&self) -> String {
let mut blob = Vec::with_capacity(4usize.saturating_add(self.value.len()));
blob.extend_from_slice(&self.key_hash);
blob.extend_from_slice(&self.value);
format!(
"{SIGNATURE_PREFIX}{} {}\n",
self.name,
base64::encode(&blob)
)
}
fn parse_line(line: &str) -> Result<Self, NoteError> {
let rest = line
.strip_prefix(SIGNATURE_PREFIX)
.ok_or(NoteError::MalformedSignature)?;
let (name, encoded) = rest.split_once(' ').ok_or(NoteError::MalformedSignature)?;
let blob = base64::decode(encoded).ok_or(NoteError::MalformedSignature)?;
let key_hash: [u8; 4] = blob
.get(..4)
.and_then(|s| s.try_into().ok())
.ok_or(NoteError::MalformedSignature)?;
let value = blob.get(4..).ok_or(NoteError::MalformedSignature)?.to_vec();
if value.is_empty() {
return Err(NoteError::MalformedSignature);
}
Ok(Self {
name: KeyName::new(name)?,
key_hash,
value,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedTreeHead {
pub origin: Origin,
pub head: TreeHead,
pub extensions: Vec<String>,
pub signatures: Vec<NoteSignature>,
}
impl SignedTreeHead {
#[must_use]
pub fn new(origin: Origin, head: TreeHead) -> Self {
Self {
origin,
head,
extensions: Vec::new(),
signatures: Vec::new(),
}
}
#[must_use]
pub fn body(&self) -> String {
let mut out = format!(
"{}\n{}\n{}\n",
self.origin,
self.head.size,
base64::encode(self.head.root.as_bytes())
);
for line in &self.extensions {
out.push_str(line);
out.push('\n');
}
out
}
#[must_use]
pub fn to_note(&self) -> String {
let mut out = self.body();
out.push('\n');
for signature in &self.signatures {
out.push_str(&signature.to_line());
}
out
}
pub fn add_signature(&mut self, signature: NoteSignature) {
self.signatures.push(signature);
}
#[must_use]
pub fn has_signature_from(&self, name: &KeyName, key_hash: [u8; 4]) -> bool {
self.signatures
.iter()
.any(|s| s.name == *name && s.key_hash == key_hash)
}
pub fn parse(text: &str) -> Result<Self, NoteError> {
let (body, rest) = text.split_once("\n\n").ok_or(NoteError::MissingSeparator)?;
let mut lines = body.split('\n');
let origin = Origin::new(lines.next().ok_or(NoteError::Truncated)?)?;
let size_line = lines.next().ok_or(NoteError::Truncated)?;
if size_line.is_empty()
|| !size_line.bytes().all(|b| b.is_ascii_digit())
|| (size_line.len() > 1 && size_line.starts_with('0'))
{
return Err(NoteError::MalformedSize);
}
let size: u64 = size_line.parse().map_err(|_| NoteError::MalformedSize)?;
let root_line = lines.next().ok_or(NoteError::Truncated)?;
let root_bytes = base64::decode(root_line).ok_or(NoteError::MalformedRoot)?;
let root: [u8; Hash::LEN] = root_bytes
.as_slice()
.try_into()
.map_err(|_| NoteError::MalformedRoot)?;
let mut extensions = Vec::new();
for line in lines {
if line.is_empty() {
return Err(NoteError::Truncated);
}
extensions.push(line.to_owned());
}
let mut signatures = Vec::new();
for line in rest.split('\n') {
if line.is_empty() {
continue;
}
signatures.push(NoteSignature::parse_line(line)?);
}
Ok(Self {
origin,
head: TreeHead {
size,
root: Hash::from_bytes(root),
},
extensions,
signatures,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum NoteError {
#[error("origin is empty, too long, or contains a control character")]
MalformedOrigin,
#[error("key name is empty, too long, or contains a space or control character")]
MalformedKeyName,
#[error("tree size is not a canonical decimal integer")]
MalformedSize,
#[error("root is not canonical base64 of 32 bytes")]
MalformedRoot,
#[error("signature line is malformed")]
MalformedSignature,
#[error("note has no blank line between its body and its signatures")]
MissingSeparator,
#[error("note body ended early")]
Truncated,
}
#[cfg(test)]
mod tests {
use super::*;
fn origin() -> Origin {
Origin::new("example.com/ledgers/acme").expect("valid")
}
fn head(size: u64) -> TreeHead {
TreeHead {
size,
root: Hash::from_bytes([7u8; 32]),
}
}
#[test]
fn a_note_round_trips_exactly() {
let mut sth = SignedTreeHead::new(origin(), head(4812));
sth.add_signature(NoteSignature {
name: KeyName::new("witness.example").expect("valid"),
key_hash: [1, 2, 3, 4],
value: vec![9u8; 64],
});
let text = sth.to_note();
assert_eq!(SignedTreeHead::parse(&text).expect("parses"), sth);
assert_eq!(
SignedTreeHead::parse(&text).expect("parses").to_note(),
text
);
}
#[test]
fn the_body_is_the_first_three_lines_and_ends_in_a_newline() {
let sth = SignedTreeHead::new(origin(), head(1));
let body = sth.body();
assert!(body.ends_with('\n'));
assert_eq!(body.lines().count(), 3);
assert!(sth.to_note().starts_with(&body));
}
#[test]
fn an_unsigned_note_still_parses() {
let sth = SignedTreeHead::new(origin(), head(0));
assert_eq!(
SignedTreeHead::parse(&sth.to_note()).expect("parses"),
sth,
"a head with no signatures yet is the normal starting state"
);
}
#[test]
fn a_non_canonical_size_is_two_notes_for_one_head_and_is_refused() {
let text =
"example.com/ledgers/acme\n04812\nBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=\n\n";
assert_eq!(SignedTreeHead::parse(text), Err(NoteError::MalformedSize));
}
#[test]
fn a_root_of_the_wrong_width_is_refused() {
let text = "example.com/ledgers/acme\n1\nZm9v\n\n";
assert_eq!(SignedTreeHead::parse(text), Err(NoteError::MalformedRoot));
}
#[test]
fn an_extension_line_is_kept_verbatim() {
let text =
"example.com/ledgers/acme\n1\nBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=\nextra\n\n";
let parsed = SignedTreeHead::parse(text).expect("parses");
assert_eq!(parsed.extensions, vec!["extra".to_owned()]);
assert_eq!(
parsed.to_note(),
text,
"a signature covers the body byte for byte, so it has to round-trip"
);
assert!(
parsed.body().ends_with("extra\n"),
"dropping it would make `body` render something nobody signed"
);
}
#[test]
fn this_crate_never_produces_an_extension_line() {
assert!(SignedTreeHead::new(origin(), head(1)).extensions.is_empty());
}
#[test]
fn an_empty_line_inside_a_body_is_refused() {
let text = "example.com/ledgers/acme\n1\nBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=\n\nextra\n\n";
assert!(SignedTreeHead::parse(text).is_err());
}
#[test]
fn a_key_name_may_not_carry_a_unicode_space_or_a_plus() {
assert_eq!(
KeyName::new("has\u{a0}nbsp"),
Err(NoteError::MalformedKeyName)
);
assert_eq!(
KeyName::new("has\u{2003}emsp"),
Err(NoteError::MalformedKeyName)
);
assert_eq!(KeyName::new("has+plus"), Err(NoteError::MalformedKeyName));
assert_eq!(
Origin::new("has\u{a0}nbsp"),
Err(NoteError::MalformedOrigin)
);
assert_eq!(Origin::new("has+plus"), Err(NoteError::MalformedOrigin));
assert!(Origin::new("example.com/ledgers/acme-gmbh").is_ok());
assert!(KeyName::new("witness.example.com/w1").is_ok());
}
#[test]
fn a_note_without_a_blank_line_is_refused() {
let text = "example.com/ledgers/acme\n1\nBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=\n";
assert_eq!(
SignedTreeHead::parse(text),
Err(NoteError::MissingSeparator)
);
}
#[test]
fn a_key_name_cannot_split_or_end_a_line() {
assert_eq!(KeyName::new("has space"), Err(NoteError::MalformedKeyName));
assert_eq!(
KeyName::new("has\nnewline"),
Err(NoteError::MalformedKeyName)
);
assert_eq!(KeyName::new(""), Err(NoteError::MalformedKeyName));
}
#[test]
fn an_origin_cannot_forge_a_tree_size() {
assert_eq!(Origin::new("acme\n999999"), Err(NoteError::MalformedOrigin));
}
}