#![cfg(test)]
use super::*;
#[test]
fn dash_lookalikes_are_rejected() {
let note = golden_note();
assert_rejected(
note.replacen('\u{2014}', "\u{2013}", 1).as_bytes(),
"en dash U+2013",
);
assert_rejected(
note.replacen('\u{2014}', "\u{2015}", 1).as_bytes(),
"horizontal bar U+2015",
);
assert_rejected(
note.replacen('\u{2014}', "--", 1).as_bytes(),
"double hyphen",
);
assert_rejected(
note.replacen("\u{2014} ", "\u{2014}", 1).as_bytes(),
"em dash without following space",
);
}
#[test]
fn structural_tampers_are_rejected() {
let note = golden_note();
assert_rejected(b"", "empty note");
assert_rejected(GOLDEN_BODY.as_bytes(), "body with no signature block");
assert_rejected(
note.replacen("\n\n", "\n", 1).as_bytes(),
"missing blank line",
);
assert_rejected(
format!("{GOLDEN_BODY}\n").as_bytes(),
"blank line but empty signature block",
);
assert_rejected(
note.trim_end_matches('\n').to_string().as_bytes(),
"signature block not newline-terminated",
);
}
#[test]
fn control_characters_and_invalid_utf8_are_rejected() {
let note = golden_note();
assert_rejected(
note.replacen("test\n3", "test\r\n3", 1).as_bytes(),
"carriage return in body",
);
let mut invalid_utf8 = note.into_bytes();
invalid_utf8[0] = 0xff;
assert_rejected(&invalid_utf8, "invalid UTF-8 byte");
}
#[test]
fn oversized_note_is_rejected() {
let mut oversized = golden_note().into_bytes();
oversized.extend(std::iter::repeat_n(b'a', 1024 * 1024));
assert_rejected(&oversized, "note above the 1 MiB cap");
}
fn hand_signed_note_with_body_len(body_len: usize) -> String {
let (_dir, identity) = golden_identity();
let mut body = "a".repeat(body_len - 1);
body.push('\n');
let signature = identity.sign(body.as_bytes());
let mut blob = GOLDEN_KEY_ID.to_vec();
blob.extend_from_slice(&signature);
format!(
"{body}\n\u{2014} {GOLDEN_NAME} {}\n",
STANDARD.encode(&blob)
)
}
const GOLDEN_NOTE_OVERHEAD: usize = 1 + 3 + 1 + GOLDEN_NAME.len() + 1 + 92 + 1;
#[test]
fn size_cap_boundary_is_exact_for_otherwise_valid_notes() {
let at_cap = hand_signed_note_with_body_len(MAX_NOTE_BYTES - GOLDEN_NOTE_OVERHEAD);
assert_eq!(at_cap.len(), MAX_NOTE_BYTES);
let body = verify_note(at_cap.as_bytes(), &golden_verifier()).unwrap();
assert_eq!(body.len(), MAX_NOTE_BYTES - GOLDEN_NOTE_OVERHEAD);
let one_over = hand_signed_note_with_body_len(MAX_NOTE_BYTES - GOLDEN_NOTE_OVERHEAD + 1);
assert_eq!(one_over.len(), MAX_NOTE_BYTES + 1);
assert_rejected(
one_over.as_bytes(),
"otherwise-valid note one byte above the 1 MiB cap",
);
}
#[test]
fn sign_note_refuses_bodies_that_would_exceed_the_cap() {
let (_dir, identity) = golden_identity();
let verifier = golden_verifier();
let mut at_cap_body = "a".repeat(MAX_NOTE_BYTES - GOLDEN_NOTE_OVERHEAD - 1);
at_cap_body.push('\n');
let note = sign_note(&at_cap_body, GOLDEN_NAME, &identity).unwrap();
assert_eq!(note.len(), MAX_NOTE_BYTES);
assert_eq!(
verify_note(note.as_bytes(), &verifier).unwrap(),
at_cap_body
);
let mut over_body = "a".repeat(MAX_NOTE_BYTES - GOLDEN_NOTE_OVERHEAD);
over_body.push('\n');
let err = sign_note(&over_body, GOLDEN_NAME, &identity).unwrap_err();
assert!(matches!(err, TrustError::CheckpointEncoding { .. }));
}
#[test]
fn malformed_signature_blobs_are_rejected() {
let blob = STANDARD.decode(GOLDEN_SIG_BLOB_B64).unwrap();
let short = STANDARD.encode(&blob[..4]);
assert_rejected(
golden_note()
.replacen(GOLDEN_SIG_BLOB_B64, &short, 1)
.as_bytes(),
"blob shorter than 5 bytes",
);
let mut altered_id = blob.clone();
altered_id[0] ^= 0xff;
assert_rejected(
golden_note()
.replacen(GOLDEN_SIG_BLOB_B64, &STANDARD.encode(&altered_id), 1)
.as_bytes(),
"altered key ID",
);
let sixty_three = STANDARD.encode(&blob[..4 + 63]);
let mut long = blob.clone();
long.push(0x00);
let sixty_five = STANDARD.encode(&long);
for (bad, label) in [(sixty_three, "63-byte"), (sixty_five, "65-byte")] {
assert_rejected(
golden_note()
.replacen(GOLDEN_SIG_BLOB_B64, &bad, 1)
.as_bytes(),
label,
);
}
let mut flipped = blob;
flipped[10] ^= 0x01;
assert_rejected(
golden_note()
.replacen(GOLDEN_SIG_BLOB_B64, &STANDARD.encode(&flipped), 1)
.as_bytes(),
"bit-flipped signature",
);
}
#[test]
fn non_canonical_base64_signature_blobs_are_rejected() {
let unpadded = GOLDEN_SIG_BLOB_B64.trim_end_matches('=');
assert_rejected(
golden_note()
.replacen(GOLDEN_SIG_BLOB_B64, unpadded, 1)
.as_bytes(),
"unpadded base64",
);
let non_canonical = GOLDEN_SIG_BLOB_B64.replacen("8QU=", "8QV=", 1);
assert_rejected(
golden_note()
.replacen(GOLDEN_SIG_BLOB_B64, &non_canonical, 1)
.as_bytes(),
"non-canonical trailing bits",
);
}
#[test]
fn any_malformed_signature_line_rejects_the_whole_note() {
let note = golden_note();
assert_rejected(
format!("{note}garbage line\n").as_bytes(),
"malformed second line",
);
assert_rejected(
format!("{note}\u{2014} {GOLDEN_NAME} not*base64\n").as_bytes(),
"second line with invalid base64",
);
assert_rejected(
format!("{note}\u{2014} bad+name {GOLDEN_SIG_BLOB_B64}\n").as_bytes(),
"second line with invalid name",
);
}
#[test]
fn one_hundred_one_signature_lines_are_rejected_and_one_hundred_accepted() {
let sig_line = format!("\u{2014} {GOLDEN_NAME} {GOLDEN_SIG_BLOB_B64}\n");
let hundred = format!("{GOLDEN_BODY}\n{}", sig_line.repeat(100));
let body = verify_note(hundred.as_bytes(), &golden_verifier()).unwrap();
assert_eq!(body, GOLDEN_BODY);
let hundred_one = format!("{GOLDEN_BODY}\n{}", sig_line.repeat(101));
assert_rejected(hundred_one.as_bytes(), "101 signature lines");
}