use base64::{Engine, engine::general_purpose::STANDARD};
use crate::error::TransparencyError;
use crate::types::MerkleHash;
const ALG_ED25519: u8 = 0x01;
pub fn compute_key_id(key_name: &str, pubkey: &[u8; 32]) -> [u8; 4] {
let mut data = Vec::with_capacity(key_name.len() + 1 + 1 + 32);
data.extend_from_slice(key_name.as_bytes());
data.push(b'\n');
data.push(ALG_ED25519);
data.extend_from_slice(pubkey);
let hash = MerkleHash::sha256(&data);
let mut id = [0u8; 4];
id.copy_from_slice(&hash.as_bytes()[..4]);
id
}
pub fn build_signature_line(key_name: &str, key_id: &[u8; 4], signature: &[u8; 64]) -> String {
let mut sig_data = Vec::with_capacity(1 + 4 + 64);
sig_data.push(ALG_ED25519);
sig_data.extend_from_slice(key_id);
sig_data.extend_from_slice(signature);
let encoded = STANDARD.encode(&sig_data);
format!("\u{2014} {key_name} {encoded}\n")
}
pub fn parse_signed_note(note: &str) -> Result<(String, Vec<NoteSignature>), TransparencyError> {
let sig_marker = "\u{2014} ";
let mut body_end = None;
let mut sig_start = None;
for (i, line) in note.lines().enumerate() {
if line.starts_with(sig_marker) && sig_start.is_none() {
sig_start = Some(i);
if body_end.is_none() {
body_end = Some(i);
}
}
}
let lines: Vec<&str> = note.lines().collect();
let body_end_idx = body_end
.ok_or_else(|| TransparencyError::InvalidNote("no signature lines found".into()))?;
let mut body_lines = &lines[..body_end_idx];
if body_lines.last() == Some(&"") {
body_lines = &body_lines[..body_lines.len() - 1];
}
let body = body_lines.join("\n") + "\n";
let mut signatures = Vec::new();
for line in &lines[body_end_idx..] {
if let Some(rest) = line.strip_prefix(sig_marker) {
let sig = parse_signature_line(rest)?;
signatures.push(sig);
}
}
Ok((body, signatures))
}
#[derive(Debug, Clone)]
pub struct NoteSignature {
pub key_name: String,
pub algorithm: u8,
pub key_id: [u8; 4],
pub signature: Vec<u8>,
}
fn parse_signature_line(line: &str) -> Result<NoteSignature, TransparencyError> {
let space_idx = line
.find(' ')
.ok_or_else(|| TransparencyError::InvalidNote("malformed signature line".into()))?;
let key_name = &line[..space_idx];
let b64 = &line[space_idx + 1..];
let raw = STANDARD
.decode(b64.trim())
.map_err(|e| TransparencyError::InvalidNote(format!("base64 decode: {e}")))?;
if raw.len() < 5 {
return Err(TransparencyError::InvalidNote(
"signature data too short".into(),
));
}
let algorithm = raw[0];
let mut key_id = [0u8; 4];
key_id.copy_from_slice(&raw[1..5]);
let signature = raw[5..].to_vec();
Ok(NoteSignature {
key_name: key_name.to_string(),
algorithm,
key_id,
signature,
})
}
pub fn serialize_signed_note(body: &str, signatures: &[String]) -> String {
let mut out =
String::with_capacity(body.len() + signatures.iter().map(|s| s.len()).sum::<usize>() + 1);
out.push_str(body);
out.push('\n');
for sig in signatures {
out.push_str(sig);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_id_computation() {
let pubkey = [0xab; 32];
let key_id = compute_key_id("auths.dev/log", &pubkey);
assert_eq!(key_id.len(), 4);
let key_id2 = compute_key_id("auths.dev/log", &pubkey);
assert_eq!(key_id, key_id2);
let key_id3 = compute_key_id("other.dev/log", &pubkey);
assert_ne!(key_id, key_id3);
}
#[test]
fn signature_line_format() {
let key_id = [0x01, 0x02, 0x03, 0x04];
let sig = [0xaa; 64];
let line = build_signature_line("auths.dev/log", &key_id, &sig);
assert!(line.starts_with("\u{2014} auths.dev/log "));
assert!(line.ends_with('\n'));
let parts: Vec<&str> = line.trim().splitn(3, ' ').collect();
let decoded = STANDARD.decode(parts[2]).unwrap();
assert_eq!(decoded[0], ALG_ED25519);
assert_eq!(&decoded[1..5], &key_id);
assert_eq!(&decoded[5..], &sig);
}
#[test]
fn signed_note_roundtrip() {
let body = "auths.dev/log\n42\nq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s=\n";
let key_id = [0x01, 0x02, 0x03, 0x04];
let sig = [0xcc; 64];
let sig_line = build_signature_line("auths.dev/log", &key_id, &sig);
let note = serialize_signed_note(body, &[sig_line]);
let (parsed_body, parsed_sigs) = parse_signed_note(¬e).unwrap();
assert_eq!(parsed_body, body);
assert_eq!(parsed_sigs.len(), 1);
assert_eq!(parsed_sigs[0].key_name, "auths.dev/log");
assert_eq!(parsed_sigs[0].algorithm, ALG_ED25519);
assert_eq!(parsed_sigs[0].key_id, key_id);
assert_eq!(parsed_sigs[0].signature, sig.to_vec());
}
#[test]
fn parse_note_rejects_no_signatures() {
let note = "just body\nno sigs\n";
assert!(parse_signed_note(note).is_err());
}
}