use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use sha2::{Digest, Sha256};
use crate::error::{TrustError, TrustResult};
use crate::keys::Ed25519Identity;
use super::body::CheckpointBody;
use super::verifier_key::{NoteVerifierKey, validate_note_name};
const MAX_NOTE_BYTES: usize = 1024 * 1024;
const MAX_SIGNATURE_LINES: usize = 100;
const SIG_PREFIX: &str = "\u{2014} ";
pub fn key_id(name: &str, public_key: &[u8; 32]) -> TrustResult<[u8; 4]> {
validate_note_name(name)?;
let mut hasher = Sha256::new();
hasher.update(name.as_bytes());
hasher.update([0x0a]);
hasher.update([0x01]);
hasher.update(public_key);
let digest = hasher.finalize();
let mut id = [0u8; 4];
id.copy_from_slice(&digest[..4]);
Ok(id)
}
pub fn sign_note(body: &str, name: &str, identity: &Ed25519Identity) -> TrustResult<String> {
validate_note_name(name).map_err(|e| TrustError::CheckpointEncoding {
reason: format!("invalid note key name: {e}"),
})?;
if body.is_empty() {
return Err(TrustError::CheckpointEncoding {
reason: "note body must not be empty".to_string(),
});
}
if !body.ends_with('\n') {
return Err(TrustError::CheckpointEncoding {
reason: "note body must end with a newline".to_string(),
});
}
if body.contains("\n\n") {
return Err(TrustError::CheckpointEncoding {
reason: "note body must not contain a blank line".to_string(),
});
}
if body.chars().any(|c| c < ' ' && c != '\n') {
return Err(TrustError::CheckpointEncoding {
reason: "note body must not contain ASCII control characters other than newline"
.to_string(),
});
}
let id =
key_id(name, &identity.public_key_bytes()).map_err(|e| TrustError::CheckpointEncoding {
reason: format!("invalid note key name: {e}"),
})?;
let signature = identity.sign(body.as_bytes());
let mut blob = Vec::with_capacity(4 + 64);
blob.extend_from_slice(&id);
blob.extend_from_slice(&signature);
let note = format!("{body}\n{SIG_PREFIX}{name} {}\n", STANDARD.encode(&blob));
if note.len() > MAX_NOTE_BYTES {
return Err(TrustError::CheckpointEncoding {
reason: format!(
"note would be {} bytes, exceeding the {MAX_NOTE_BYTES}-byte cap",
note.len()
),
});
}
Ok(note)
}
struct SignatureLine<'a> {
name: &'a str,
key_id: [u8; 4],
signature: Vec<u8>,
}
pub fn verify_note(note_bytes: &[u8], verifier: &NoteVerifierKey) -> TrustResult<String> {
let (body, signature_lines) = parse_note(note_bytes)?;
let public_key = verifier.public_key();
for line in &signature_lines {
if line.name != verifier.name() || line.key_id != verifier.key_id() {
continue;
}
if line.signature.len() == 64
&& Ed25519Identity::verify(&public_key, body.as_bytes(), &line.signature).is_ok()
{
return Ok(body.to_string());
}
return Err(TrustError::NoteVerification);
}
Err(TrustError::NoteVerification)
}
pub fn verify_checkpoint(
note_bytes: &[u8],
verifier: &NoteVerifierKey,
) -> TrustResult<CheckpointBody> {
let body_text = verify_note(note_bytes, verifier)?;
let body = CheckpointBody::parse(&body_text).map_err(|_err| TrustError::NoteVerification)?;
if body.origin() != verifier.name() {
return Err(TrustError::NoteVerification);
}
Ok(body)
}
fn parse_note(note_bytes: &[u8]) -> TrustResult<(&str, Vec<SignatureLine<'_>>)> {
if note_bytes.len() > MAX_NOTE_BYTES {
return Err(TrustError::NoteVerification);
}
let text = std::str::from_utf8(note_bytes).map_err(|_err| TrustError::NoteVerification)?;
if text.chars().any(|c| c < ' ' && c != '\n') {
return Err(TrustError::NoteVerification);
}
let split = text.rfind("\n\n").ok_or(TrustError::NoteVerification)?;
let body = &text[..=split];
let sig_block = &text[split + 2..];
if sig_block.is_empty() || !sig_block.ends_with('\n') {
return Err(TrustError::NoteVerification);
}
let mut lines = Vec::new();
for line in sig_block[..sig_block.len() - 1].split('\n') {
lines.push(parse_signature_line(line)?);
if lines.len() > MAX_SIGNATURE_LINES {
return Err(TrustError::NoteVerification);
}
}
Ok((body, lines))
}
fn parse_signature_line(line: &str) -> TrustResult<SignatureLine<'_>> {
let rest = line
.strip_prefix(SIG_PREFIX)
.ok_or(TrustError::NoteVerification)?;
let (name, blob_b64) = rest.split_once(' ').ok_or(TrustError::NoteVerification)?;
if blob_b64.is_empty() || validate_note_name(name).is_err() {
return Err(TrustError::NoteVerification);
}
let blob = STANDARD
.decode(blob_b64)
.map_err(|_err| TrustError::NoteVerification)?;
if blob.len() < 5 {
return Err(TrustError::NoteVerification);
}
let mut id = [0u8; 4];
id.copy_from_slice(&blob[..4]);
Ok(SignatureLine {
name,
key_id: id,
signature: blob[4..].to_vec(),
})
}
#[cfg(test)]
#[path = "note_tests.rs"]
mod tests;