use crate::WIRE_VERSION;
#[derive(Debug, Clone)]
pub struct Builder {
buf: Vec<u8>,
}
impl Builder {
pub fn new(domain_suffix: &str) -> Self {
let mut me = Self {
buf: Vec::with_capacity(64),
};
let domain = format!("freenet-git/{}/{}", WIRE_VERSION, domain_suffix);
me.field_bytes(domain.as_bytes());
me
}
pub fn field_bytes(&mut self, bytes: &[u8]) -> &mut Self {
let len: u32 = bytes
.len()
.try_into()
.expect("freenet-git signed payloads do not contain >4GiB fields");
self.buf.extend_from_slice(&len.to_le_bytes());
self.buf.extend_from_slice(bytes);
self
}
pub fn field_str(&mut self, s: &str) -> &mut Self {
self.field_bytes(s.as_bytes())
}
pub fn field_u32(&mut self, x: u32) -> &mut Self {
self.field_bytes(&x.to_le_bytes())
}
pub fn field_u64(&mut self, x: u64) -> &mut Self {
self.field_bytes(&x.to_le_bytes())
}
pub fn field_bool(&mut self, b: bool) -> &mut Self {
self.field_bytes(&[u8::from(b)])
}
pub fn field_option_bytes(&mut self, value: Option<&[u8]>) -> &mut Self {
match value {
None => self.field_bytes(&[0x00]),
Some(b) => {
let mut tagged = Vec::with_capacity(1 + b.len());
tagged.push(0x01);
tagged.extend_from_slice(b);
self.field_bytes(&tagged)
}
}
}
pub fn finish(self) -> Vec<u8> {
self.buf
}
}
pub fn build<F>(domain_suffix: &str, f: F) -> Vec<u8>
where
F: FnOnce(&mut Builder),
{
let mut b = Builder::new(domain_suffix);
f(&mut b);
b.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn domain_only_payload_is_length_prefixed() {
let bytes = Builder::new("example").finish();
assert_eq!(&bytes[..4], &22u32.to_le_bytes());
assert_eq!(&bytes[4..], b"freenet-git/v1/example");
}
#[test]
fn every_primitive_round_trip() {
let payload = build("worked-example", |b| {
b.field_bytes(&[0xAA, 0xBB, 0xCC]);
b.field_str("hi");
b.field_u32(0x01020304);
b.field_u64(0x0807060504030201);
b.field_bool(true);
b.field_bool(false);
b.field_option_bytes(None);
b.field_option_bytes(Some(&[0xDE, 0xAD]));
});
let expected = hex::decode(concat!(
"1d000000",
"667265656e65742d6769742f76312f776f726b65642d6578616d706c65",
"03000000",
"aabbcc",
"02000000",
"6869",
"04000000",
"04030201",
"08000000",
"0102030405060708",
"01000000",
"01",
"01000000",
"00",
"01000000",
"00",
"03000000",
"01dead",
))
.unwrap();
assert_eq!(payload, expected);
}
#[test]
fn ref_update_signs_and_verifies() {
use ed25519_dalek::{Signer, SigningKey, Verifier};
let mut secret_bytes = [0u8; 32];
for (i, b) in secret_bytes.iter_mut().enumerate() {
*b = i as u8;
}
let signing_key = SigningKey::from_bytes(&secret_bytes);
let verifying_key = signing_key.verifying_key();
let repo_key = [0xAAu8; 32];
let commit_hash = [0xBBu8; 20];
let payload = build("ref-update", |b| {
b.field_bytes(&repo_key);
b.field_str("refs/heads/main");
b.field_bytes(&commit_hash);
b.field_u64(1);
b.field_u64(0);
});
let sig = signing_key.sign(&payload);
assert!(verifying_key.verify(&payload, &sig).is_ok());
let other_payload = build("object-bundle", |b| {
b.field_bytes(&repo_key);
b.field_str("refs/heads/main");
b.field_bytes(&commit_hash);
b.field_u64(1);
b.field_u64(0);
});
assert!(verifying_key.verify(&other_payload, &sig).is_err());
}
}