use crate::decoding::parse;
use crate::file_format::HeaderField;
use crate::types::VsfType;
pub struct ParsedHeader {
pub version: usize,
pub backward_compat: usize,
pub file_length: usize, pub rolling_hash: Option<VsfType>,
pub fields: Vec<HeaderField>,
pub header_end: usize, }
pub fn parse_full_header(data: &[u8]) -> Result<ParsedHeader, String> {
if data.len() < 4 {
return Err("File too small".to_string());
}
if &data[0..3] != "RÃ…".as_bytes() || data[3] != b'<' {
return Err("Invalid magic number".to_string());
}
let mut ptr = 4;
let version =
match parse(data, &mut ptr).map_err(|e| format!("Failed to parse version: {}", e))? {
VsfType::z(v) => v,
_ => return Err("Expected z type for version".to_string()),
};
let backward_compat = match parse(data, &mut ptr)
.map_err(|e| format!("Failed to parse backward compat: {}", e))?
{
VsfType::y(v) => v,
_ => return Err("Expected y type for backward compat".to_string()),
};
if backward_compat > crate::VSF_VERSION {
return Err(format!(
"File requires VSF v{} but this implementation is v{}",
backward_compat,
crate::VSF_VERSION
));
}
let _ = parse(data, &mut ptr).map_err(|e| format!("Failed to parse header length: {}", e))?;
let file_length = if ptr < data.len() && data[ptr] == b'L' {
match parse(data, &mut ptr) {
Ok(VsfType::L(len, _)) => len,
Ok(other) => return Err(format!("Expected L value, got: {:?}", other)),
Err(e) => return Err(format!("Failed to parse file length: {}", e)),
}
} else {
0 };
let _ = parse(data, &mut ptr).map_err(|e| format!("Failed to parse creation time: {}", e))?;
let prov_type =
parse(data, &mut ptr).map_err(|e| format!("Failed to parse provenance hash: {}", e))?;
match prov_type {
VsfType::hp(_) => {} _ => {
return Err(format!(
"Expected hp after creation time, got: {:?}",
prov_type
))
}
}
let mut rolling_hash = None;
if ptr < data.len() && data[ptr] == b'k' {
let _ =
parse(data, &mut ptr).map_err(|e| format!("Failed to parse signer pubkey: {}", e))?;
if ptr < data.len() && data[ptr] == b'g' {
let _ =
parse(data, &mut ptr).map_err(|e| format!("Failed to parse signature: {}", e))?;
}
}
else if ptr < data.len() && data[ptr] == b'h' {
rolling_hash = Some(
parse(data, &mut ptr).map_err(|e| format!("Failed to parse rolling hash: {}", e))?,
);
}
let field_count =
match parse(data, &mut ptr).map_err(|e| format!("Failed to parse field count: {}", e))? {
VsfType::n(count) => count,
_ => return Err("Expected n type for field count".to_string()),
};
let mut fields = Vec::with_capacity(field_count);
for _ in 0..field_count {
fields.push(parse_header_field(data, &mut ptr)?);
}
if data[ptr] != b'>' {
return Err("Expected '>' at end of header".to_string());
}
ptr += 1;
Ok(ParsedHeader {
version,
backward_compat,
file_length,
rolling_hash,
fields,
header_end: ptr,
})
}
fn parse_header_field(data: &[u8], ptr: &mut usize) -> Result<HeaderField, String> {
use crate::file_format::{validate_name, VsfField};
let field =
VsfField::parse(data, ptr).map_err(|e| format!("Failed to parse header field: {}", e))?;
validate_name(&field.name)?;
let mut hash = None;
let mut signature = None;
let mut key = None;
let mut offset_bytes = None;
let mut size_bytes = None;
let mut child_count = None;
for value in field.values {
match value {
VsfType::hb(_) | VsfType::hs(_) | VsfType::hp(_) => hash = Some(value),
VsfType::ge(_) | VsfType::gp(_) | VsfType::gr(_) => signature = Some(value),
VsfType::ke(_) | VsfType::kx(_) | VsfType::kp(_) | VsfType::kc(_) | VsfType::ka(_) => {
key = Some(value)
}
VsfType::o(bytes) => offset_bytes = Some(bytes),
VsfType::b(bytes, _) => size_bytes = Some(bytes),
VsfType::n(count) => child_count = Some(count),
_ => {}
}
}
let is_metadata_only = offset_bytes.is_none() && size_bytes.is_none();
if is_metadata_only {
Ok(HeaderField {
name: field.name,
hash,
signature,
key,
offset_bytes: 0,
size_bytes: 0,
child_count: 0,
inline_values: Vec::new(),
})
} else {
let offset_bytes = offset_bytes.ok_or("Missing required offset (o) field")?;
let size_bytes = size_bytes.ok_or("Missing required size (b) field")?;
let child_count = child_count.ok_or("Missing required child count (n) field")?;
Ok(HeaderField {
name: field.name,
hash,
signature,
key,
offset_bytes,
size_bytes,
child_count,
inline_values: Vec::new(),
})
}
}
fn rebuild_with_header(
old_data: &[u8],
mut fields: Vec<HeaderField>,
version: usize,
backward_compat: usize,
old_header_end: usize,
include_rolling_hash: bool,
) -> Result<Vec<u8>, String> {
use crate::file_format::VsfHeader;
let old_header_size = old_header_end;
const MAX_ITERATIONS: usize = 10;
let mut prev_header_size = old_header_size;
for _iteration in 0..MAX_ITERATIONS {
let mut test_header = VsfHeader::new(version, backward_compat);
test_header.provenance_hash = VsfType::hp(vec![0u8; 32]);
test_header.rolling_hash = if include_rolling_hash {
Some(VsfType::hb(vec![0u8; 32]))
} else {
None
};
for field in &fields {
test_header.add_field(field.clone());
}
let mut test_encoded = test_header.encode()?;
VsfHeader::update_header_length(&mut test_encoded)?;
let new_header_size = test_encoded.len();
if new_header_size == prev_header_size {
let field_signatures: Vec<Option<VsfType>> =
fields.iter().map(|f| f.signature.clone()).collect();
let mut final_header = VsfHeader::new(version, backward_compat);
final_header.provenance_hash = VsfType::hp(vec![0u8; 32]);
final_header.rolling_hash = if include_rolling_hash {
Some(VsfType::hb(vec![0u8; 32]))
} else {
None
};
for field in fields {
final_header.add_field(field);
}
let mut new_file = final_header.encode()?;
VsfHeader::update_header_length(&mut new_file)?;
new_file.extend_from_slice(&old_data[old_header_end..]);
let hp_hash = compute_provenance_hash(&new_file)?;
new_file = write_provenance_hash(new_file, &hp_hash)?;
new_file = write_header_field_signatures_from_list(new_file, field_signatures)?;
if include_rolling_hash {
let hb_hash = compute_file_hash(&new_file)?;
new_file = write_file_hash(new_file, &hb_hash)?;
}
return Ok(new_file);
}
let offset_adjustment = new_header_size as isize - prev_header_size as isize;
for field in &mut fields {
field.offset_bytes = ((field.offset_bytes as isize) + offset_adjustment) as usize;
}
prev_header_size = new_header_size;
}
Err(format!(
"Failed to stabilize header after {} iterations",
MAX_ITERATIONS
))
}
pub fn compute_provenance_hash(vsf_bytes: &[u8]) -> Result<[u8; 32], String> {
if vsf_bytes.len() < 4 {
return Err("File too small to be valid VSF".to_string());
}
if &vsf_bytes[0..3] != "RÃ…".as_bytes() || vsf_bytes[3] != b'<' {
return Err("Invalid VSF magic number".to_string());
}
let mut pointer = 4;
let _version =
parse(vsf_bytes, &mut pointer).map_err(|e| format!("Failed to parse version: {}", e))?;
let _backward = parse(vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse backward compat: {}", e))?;
let _header_length_type = parse(vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse header length: {}", e))?;
if pointer < vsf_bytes.len() && vsf_bytes[pointer] == b'L' {
let _ = parse(vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse file length: {}", e))?;
}
let _creation_time = parse(vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse creation time: {}", e))?;
let hash_position = pointer;
if pointer >= vsf_bytes.len() {
return Err(format!(
"Pointer {} beyond file size {}",
pointer,
vsf_bytes.len()
));
}
if vsf_bytes[pointer] != b'h' {
return Err(format!(
"No provenance hash placeholder found at position {}. Found byte: 0x{:02X} ('{}')",
pointer, vsf_bytes[pointer], vsf_bytes[pointer] as char
));
}
let hash_type =
parse(vsf_bytes, &mut pointer).map_err(|e| format!("Failed to parse hash: {}", e))?;
match hash_type {
VsfType::hp(hash_bytes) => {
if hash_bytes.len() != 32 {
return Err(format!(
"Invalid hash size: expected 32 bytes, found {}",
hash_bytes.len()
));
}
let mut temp_bytes = vsf_bytes.to_vec();
let hash_start = find_hp_value_position(&temp_bytes, hash_position)?;
for i in 0..32 {
temp_bytes[hash_start + i] = 0;
}
let mut ptr_after_hp = pointer;
if ptr_after_hp < temp_bytes.len() && temp_bytes[ptr_after_hp] == b'h' {
let hb_position = ptr_after_hp;
let hb_type = parse(&temp_bytes, &mut ptr_after_hp)
.map_err(|e| format!("Failed to parse rolling hash: {}", e))?;
if let VsfType::hb(hb_bytes) = hb_type {
if hb_bytes.len() == 32 {
let hb_start = find_hash_value_position(&temp_bytes, hb_position)?;
for i in 0..32 {
temp_bytes[hb_start + i] = 0;
}
}
}
}
zero_all_signatures(&mut temp_bytes)?;
let computed_hash = blake3::hash(&temp_bytes);
Ok(*computed_hash.as_bytes())
}
_ => Err("Expected BLAKE3 provenance hash (hp)".to_string()),
}
}
fn zero_all_signatures(vsf_bytes: &mut Vec<u8>) -> Result<(), String> {
let mut ptr = 0;
while ptr < vsf_bytes.len() - 1 {
if vsf_bytes[ptr] == b'g'
&& (vsf_bytes[ptr + 1] == b'e'
|| vsf_bytes[ptr + 1] == b'p'
|| vsf_bytes[ptr + 1] == b'r')
{
let sig_position = ptr;
let sig_type = match parse(vsf_bytes, &mut ptr) {
Ok(t) => t,
Err(_) => {
ptr = sig_position + 1;
continue;
}
};
match sig_type {
VsfType::ge(sig_bytes) | VsfType::gp(sig_bytes) | VsfType::gr(sig_bytes) => {
let sig_len = sig_bytes.len();
if let Ok(sig_start) = find_signature_value_position(vsf_bytes, sig_position) {
for i in 0..sig_len {
vsf_bytes[sig_start + i] = 0;
}
}
}
_ => {}
}
} else {
ptr += 1;
}
}
Ok(())
}
fn find_signature_value_position(data: &[u8], sig_marker_pos: usize) -> Result<usize, String> {
let mut pos = sig_marker_pos;
let sig_type =
parse(data, &mut pos).map_err(|e| format!("Failed to parse signature: {}", e))?;
match sig_type {
VsfType::ge(bytes) | VsfType::gp(bytes) | VsfType::gr(bytes) => {
let sig_start = pos - bytes.len();
Ok(sig_start)
}
_ => Err("Expected signature type (ge/gp/gr)".to_string()),
}
}
pub fn write_provenance_hash(mut vsf_bytes: Vec<u8>, hash: &[u8; 32]) -> Result<Vec<u8>, String> {
if vsf_bytes.len() < 4 {
return Err("File too small to be valid VSF".to_string());
}
let mut pointer = 4;
let _version =
parse(&vsf_bytes, &mut pointer).map_err(|e| format!("Failed to parse version: {}", e))?;
let _backward = parse(&vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse backward compat: {}", e))?;
let _header_length_type = parse(&vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse header length: {}", e))?;
if pointer < vsf_bytes.len() && vsf_bytes[pointer] == b'L' {
let _ = parse(&vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse file length: {}", e))?;
}
let _creation_time = parse(&vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse creation time: {}", e))?;
let hash_position = pointer;
if pointer >= vsf_bytes.len() {
return Err(format!(
"Pointer {} beyond file size {}",
pointer,
vsf_bytes.len()
));
}
if vsf_bytes[pointer] != b'h' {
return Err(format!(
"No provenance hash placeholder found at position {}. Found byte: 0x{:02X} ('{}')",
pointer, vsf_bytes[pointer], vsf_bytes[pointer] as char
));
}
let hash_start = find_hp_value_position(&vsf_bytes, hash_position)?;
vsf_bytes[hash_start..hash_start + 32].copy_from_slice(hash);
Ok(vsf_bytes)
}
fn find_hp_value_position(data: &[u8], hash_marker_pos: usize) -> Result<usize, String> {
let mut pos = hash_marker_pos;
let hash_type = parse(data, &mut pos).map_err(|e| {
format!(
"Failed to parse hp hash at position {}: {}",
hash_marker_pos, e
)
})?;
match hash_type {
VsfType::hp(hash_bytes) => {
let hash_start = pos - hash_bytes.len();
Ok(hash_start)
}
_ => Err("Expected BLAKE3 provenance hash type (hp)".to_string()),
}
}
pub fn fill_provenance_hash(vsf_bytes: &mut [u8], hash: &[u8; 32]) -> Result<(), String> {
let mut pointer = 4;
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("version: {}", e))?;
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("backward: {}", e))?;
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("header_len: {}", e))?;
if pointer < vsf_bytes.len() && vsf_bytes[pointer] == b'L' {
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("file_len: {}", e))?;
}
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("creation_time: {}", e))?;
if pointer >= vsf_bytes.len() || vsf_bytes[pointer] != b'h' {
return Err("No hp field found".to_string());
}
let hash_start = find_hp_value_position(vsf_bytes, pointer)?;
vsf_bytes[hash_start..hash_start + 32].copy_from_slice(hash);
Ok(())
}
pub fn fill_signature(vsf_bytes: &mut [u8], signature: &[u8]) -> Result<(), String> {
if signature.len() != 64 {
return Err(format!(
"Signature must be 64 bytes, got {}",
signature.len()
));
}
let mut pointer = 4;
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("version: {}", e))?;
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("backward: {}", e))?;
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("header_len: {}", e))?;
if pointer < vsf_bytes.len() && vsf_bytes[pointer] == b'L' {
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("file_len: {}", e))?;
}
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("creation_time: {}", e))?;
if pointer >= vsf_bytes.len() || vsf_bytes[pointer] != b'h' {
return Err("No hp field found".to_string());
}
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("hp: {}", e))?;
if pointer < vsf_bytes.len() && vsf_bytes[pointer] == b'k' {
let _ = parse(vsf_bytes, &mut pointer).map_err(|e| format!("ke: {}", e))?;
}
if pointer >= vsf_bytes.len() || vsf_bytes[pointer] != b'g' {
return Err("No ge field found".to_string());
}
let ge_marker = pointer;
let ge_type = parse(vsf_bytes, &mut pointer).map_err(|e| format!("ge: {}", e))?;
match ge_type {
VsfType::ge(sig_bytes) => {
let sig_start = pointer - sig_bytes.len();
vsf_bytes[sig_start..sig_start + 64].copy_from_slice(signature);
Ok(())
}
_ => Err(format!("Expected ge at position {}", ge_marker)),
}
}
pub fn compute_file_hash(vsf_bytes: &[u8]) -> Result<[u8; 32], String> {
if vsf_bytes.len() < 4 {
return Err("File too small to be valid VSF".to_string());
}
if &vsf_bytes[0..3] != "RÃ…".as_bytes() || vsf_bytes[3] != b'<' {
return Err("Invalid VSF magic number".to_string());
}
let mut pointer = 4;
let _version =
parse(vsf_bytes, &mut pointer).map_err(|e| format!("Failed to parse version: {}", e))?;
let _backward = parse(vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse backward compat: {}", e))?;
let _header_length_type = parse(vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse header length: {}", e))?;
if pointer < vsf_bytes.len() && vsf_bytes[pointer] == b'L' {
let _ = parse(vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse file length: {}", e))?;
}
let _creation_time = parse(vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse creation time: {}", e))?;
let _hp = parse(vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse provenance hash: {}", e))?;
if pointer < vsf_bytes.len() && vsf_bytes[pointer] == b'g' {
let _sig = parse(vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse signature: {}", e))?;
}
let hash_position = pointer;
if pointer >= vsf_bytes.len() {
return Err(format!(
"Pointer {} beyond file size {}",
pointer,
vsf_bytes.len()
));
}
if vsf_bytes[pointer] != b'h' {
return Err(format!(
"No rolling hash placeholder found at position {}. Found byte: 0x{:02X} ('{}')",
pointer, vsf_bytes[pointer], vsf_bytes[pointer] as char
));
}
let hash_type =
parse(vsf_bytes, &mut pointer).map_err(|e| format!("Failed to parse hash: {}", e))?;
match hash_type {
VsfType::hb(hash_bytes) => {
if hash_bytes.len() != 32 {
return Err(format!(
"Invalid hash size: expected 32 bytes, found {}",
hash_bytes.len()
));
}
let mut temp_bytes = vsf_bytes.to_vec();
let hash_start = find_hash_value_position(&temp_bytes, hash_position)?;
for i in 0..32 {
temp_bytes[hash_start + i] = 0;
}
let computed_hash = blake3::hash(&temp_bytes);
Ok(*computed_hash.as_bytes())
}
_ => Err("Expected BLAKE3 rolling hash (hb)".to_string()),
}
}
pub fn write_file_hash(mut vsf_bytes: Vec<u8>, hash: &[u8; 32]) -> Result<Vec<u8>, String> {
if vsf_bytes.len() < 4 {
return Err("File too small to be valid VSF".to_string());
}
let mut pointer = 4;
let _version =
parse(&vsf_bytes, &mut pointer).map_err(|e| format!("Failed to parse version: {}", e))?;
let _backward = parse(&vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse backward compat: {}", e))?;
let _header_length_type = parse(&vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse header length: {}", e))?;
if pointer < vsf_bytes.len() && vsf_bytes[pointer] == b'L' {
let _ = parse(&vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse file length: {}", e))?;
}
let _creation_time = parse(&vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse creation time: {}", e))?;
let _hp = parse(&vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse provenance hash: {}", e))?;
if pointer < vsf_bytes.len() && vsf_bytes[pointer] == b'g' {
let _sig = parse(&vsf_bytes, &mut pointer)
.map_err(|e| format!("Failed to parse signature: {}", e))?;
}
let hash_position = pointer;
if pointer >= vsf_bytes.len() {
return Err(format!(
"Pointer {} beyond file size {}",
pointer,
vsf_bytes.len()
));
}
if vsf_bytes[pointer] != b'h' {
return Err(format!(
"No rolling hash placeholder found at position {}. Found byte: 0x{:02X} ('{}')",
pointer, vsf_bytes[pointer], vsf_bytes[pointer] as char
));
}
let hash_start = find_hash_value_position(&vsf_bytes, hash_position)?;
vsf_bytes[hash_start..hash_start + 32].copy_from_slice(hash);
Ok(vsf_bytes)
}
#[deprecated(note = "Use compute_file_hash() and write_file_hash() separately")]
pub fn add_file_hash(vsf_bytes: Vec<u8>) -> Result<Vec<u8>, String> {
let hash = compute_file_hash(&vsf_bytes)?;
write_file_hash(vsf_bytes, &hash)
}
fn find_hash_value_position(data: &[u8], hash_marker_pos: usize) -> Result<usize, String> {
let mut pos = hash_marker_pos;
let hash_type = parse(data, &mut pos).map_err(|e| {
format!(
"Failed to parse hash at position {}: {}",
hash_marker_pos, e
)
})?;
match hash_type {
VsfType::hb(hash_bytes) => {
let hash_start = pos - hash_bytes.len();
Ok(hash_start)
}
_ => Err("Expected BLAKE3 hash type (hb)".to_string()),
}
}
fn write_header_field_signatures_from_list(
mut vsf_bytes: Vec<u8>,
field_signatures: Vec<Option<VsfType>>,
) -> Result<Vec<u8>, String> {
let header = parse_full_header(&vsf_bytes)?;
let mut signatures = Vec::new();
for sig_vsf in field_signatures.into_iter().flatten() {
let sig_bytes = match sig_vsf {
VsfType::ge(bytes) => bytes,
VsfType::gp(bytes) => bytes,
VsfType::gr(bytes) => bytes,
_ => continue,
};
signatures.push(sig_bytes);
}
let header_end = header.header_end;
let mut sig_index = 0;
let mut pos = 0;
while pos < header_end - 1 && sig_index < signatures.len() {
if vsf_bytes[pos] == b'g'
&& (vsf_bytes[pos + 1] == b'e'
|| vsf_bytes[pos + 1] == b'p'
|| vsf_bytes[pos + 1] == b'r')
{
let mut test_ptr = pos;
if let Ok(sig_type) = parse(&vsf_bytes, &mut test_ptr) {
match sig_type {
VsfType::ge(test_bytes) | VsfType::gp(test_bytes) | VsfType::gr(test_bytes) => {
if test_bytes.iter().all(|&b| b == 0)
&& test_bytes.len() == signatures[sig_index].len()
{
let sig_start = test_ptr - test_bytes.len();
vsf_bytes[sig_start..sig_start + signatures[sig_index].len()]
.copy_from_slice(&signatures[sig_index]);
sig_index += 1;
}
pos = test_ptr; }
_ => {
pos += 1;
}
}
} else {
pos += 1;
}
} else {
pos += 1;
}
}
Ok(vsf_bytes)
}
#[cfg(feature = "crypto")]
pub fn sign_section(
vsf_bytes: Vec<u8>,
section_name: &str,
signing_key: &[u8],
) -> Result<Vec<u8>, String> {
use ed25519_dalek::{Signer, SigningKey};
let key_bytes: [u8; 32] = signing_key.try_into().map_err(|_| {
format!(
"Signing key must be exactly 32 bytes, got {}",
signing_key.len()
)
})?;
let signing_key = SigningKey::from_bytes(&key_bytes);
let header = parse_full_header(&vsf_bytes)?;
let _section_field = header
.fields
.iter()
.find(|f| f.name == section_name)
.ok_or_else(|| format!("Section '{}' not found", section_name))?;
let mut new_fields = header.fields.clone();
for field in &mut new_fields {
if field.name == section_name {
field.signature = Some(VsfType::ge(vec![0u8; 64]));
break;
}
}
let file_with_placeholder = rebuild_with_header(
&vsf_bytes,
new_fields.clone(),
header.version,
header.backward_compat,
header.header_end,
false, )?;
let hash_to_sign = compute_signature_hash(&file_with_placeholder, section_name)?;
let signature = signing_key.sign(&hash_to_sign);
let sig_bytes = signature.to_bytes().to_vec();
write_section_signature(file_with_placeholder, section_name, &sig_bytes)
}
#[cfg(feature = "crypto")]
fn compute_signature_hash(vsf_bytes: &[u8], section_name: &str) -> Result<[u8; 32], String> {
let header = parse_full_header(vsf_bytes)?;
let section_field = header
.fields
.iter()
.find(|f| f.name == section_name)
.ok_or_else(|| format!("Section '{}' not found", section_name))?;
let sig_position = match §ion_field.signature {
Some(VsfType::ge(_)) => {
find_section_signature_position(vsf_bytes, section_name)?
}
_ => return Err("Section has no signature placeholder".to_string()),
};
let mut temp_bytes = vsf_bytes.to_vec();
for i in 0..64 {
temp_bytes[sig_position + i] = 0;
}
Ok(*blake3::hash(&temp_bytes).as_bytes())
}
#[cfg(feature = "crypto")]
fn find_section_signature_position(vsf_bytes: &[u8], section_name: &str) -> Result<usize, String> {
let mut pointer = 4;
while pointer < vsf_bytes.len() && vsf_bytes[pointer] != b'>' {
let byte = vsf_bytes[pointer];
if byte == b'(' {
pointer += 1;
let field_name = match parse(vsf_bytes, &mut pointer).map_err(|e| e.to_string())? {
VsfType::d(name) => name,
_ => return Err("Expected field name".to_string()),
};
if pointer < vsf_bytes.len() && vsf_bytes[pointer] == b':' {
pointer += 1;
}
let mut found_sig_position = None;
while pointer < vsf_bytes.len() && vsf_bytes[pointer] != b')' {
if vsf_bytes[pointer] == b',' {
pointer += 1;
continue;
}
let value = parse(vsf_bytes, &mut pointer).map_err(|e| e.to_string())?;
if field_name == section_name {
if let VsfType::ge(sig_bytes) = value {
found_sig_position = Some(pointer - sig_bytes.len());
}
}
}
if pointer < vsf_bytes.len() && vsf_bytes[pointer] == b')' {
pointer += 1; }
if field_name == section_name {
return found_sig_position
.ok_or_else(|| format!("Section '{}' has no signature field", section_name));
}
} else {
let _ = parse(vsf_bytes, &mut pointer)
.map_err(|e| format!("Header parse error at byte {}: {}", pointer, e))?;
}
}
Err(format!("Section '{}' not found in header", section_name))
}
#[cfg(feature = "crypto")]
fn write_section_signature(
mut vsf_bytes: Vec<u8>,
section_name: &str,
signature: &[u8],
) -> Result<Vec<u8>, String> {
if signature.len() != 64 {
return Err(format!(
"Signature must be 64 bytes, got {}",
signature.len()
));
}
let sig_position = find_section_signature_position(&vsf_bytes, section_name)?;
vsf_bytes[sig_position..sig_position + 64].copy_from_slice(signature);
Ok(vsf_bytes)
}
pub fn add_encryption_metadata(
vsf_bytes: Vec<u8>,
section_name: &str,
algorithm: u8,
encryption_key: &[u8],
) -> Result<Vec<u8>, String> {
let header = parse_full_header(&vsf_bytes)?;
let mut new_fields = header.fields.clone();
let mut found = false;
for field in &mut new_fields {
if field.name == section_name {
use crate::crypto_algorithms::{WRAP_AES256_GCM, WRAP_CHACHA20POLY1305};
let key_vsf = match algorithm {
WRAP_CHACHA20POLY1305 => VsfType::kc(encryption_key.to_vec()),
WRAP_AES256_GCM => VsfType::ka(encryption_key.to_vec()),
_ => {
return Err(format!(
"Unsupported encryption algorithm: {}",
algorithm as char
))
}
};
field.key = Some(key_vsf);
found = true;
break;
}
}
if !found {
return Err(format!("Section '{}' not found", section_name));
}
rebuild_with_header(
&vsf_bytes,
new_fields,
header.version,
header.backward_compat,
header.header_end,
header.rolling_hash.is_some(),
)
}
pub fn is_original(vsf_bytes: &[u8]) -> Result<(), String> {
use crate::file_format::VsfHeader;
let computed_hash = compute_provenance_hash(vsf_bytes)?;
let (header, _) = VsfHeader::decode(vsf_bytes)?;
let stored_hash = match &header.provenance_hash {
VsfType::hp(hash_bytes) if hash_bytes.len() == 32 => hash_bytes,
VsfType::hp(hash_bytes) => {
return Err(format!(
"Invalid provenance hash size: expected 32 bytes, found {}",
hash_bytes.len()
))
}
_ => return Err("Missing provenance hash (hp) in header".to_string()),
};
if computed_hash.as_slice() == stored_hash.as_slice() {
Ok(())
} else {
Err("File has been modified - provenance hash does not match content".to_string())
}
}
pub fn verify_file_hash(vsf_bytes: &[u8]) -> Result<(), String> {
use crate::file_format::VsfHeader;
let computed_hash = compute_file_hash(vsf_bytes)?;
let (header, _) = VsfHeader::decode(vsf_bytes)?;
let stored_hash = match &header.rolling_hash {
Some(VsfType::hb(hash_bytes)) if hash_bytes.len() == 32 => hash_bytes,
Some(VsfType::hb(hash_bytes)) => {
return Err(format!(
"Invalid rolling hash size: expected 32 bytes, found {}",
hash_bytes.len()
))
}
_ => return Err("Missing rolling hash (hb) in header".to_string()),
};
if computed_hash.as_slice() == stored_hash.as_slice() {
Ok(())
} else {
Err("Rolling hash verification failed: file has been modified".to_string())
}
}
#[cfg(feature = "crypto")]
pub fn sign_file(mut vsf_bytes: Vec<u8>, signing_key: &[u8; 32]) -> Result<Vec<u8>, String> {
use ed25519_dalek::{Signer, SigningKey};
let signing_key = SigningKey::from_bytes(signing_key);
let hp_info = find_header_hp(&vsf_bytes)?;
if hp_info.is_placeholder {
let hp_hash = compute_provenance_hash(&vsf_bytes)?;
vsf_bytes[hp_info.value_start..hp_info.value_start + 32].copy_from_slice(&hp_hash);
}
let ge_info = find_header_ge(&vsf_bytes)?;
if !ge_info.is_placeholder {
return Err("ge is already filled - file may already be signed".to_string());
}
let file_hash = blake3::hash(&vsf_bytes);
let signature = signing_key.sign(file_hash.as_bytes());
let sig_bytes = signature.to_bytes();
vsf_bytes[ge_info.value_start..ge_info.value_start + 64].copy_from_slice(&sig_bytes);
Ok(vsf_bytes)
}
#[cfg(feature = "crypto")]
pub fn verify_file_signature(vsf_bytes: &[u8]) -> Result<bool, String> {
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
let ke_info = find_header_ke(vsf_bytes)?;
if ke_info.value_len != 32 {
return Err(format!(
"Expected 32-byte Ed25519 pubkey, got {}",
ke_info.value_len
));
}
let pubkey_bytes: [u8; 32] = vsf_bytes[ke_info.value_start..ke_info.value_start + 32]
.try_into()
.map_err(|_| "Failed to extract pubkey bytes")?;
let ge_info = find_header_ge(vsf_bytes)?;
if ge_info.value_len != 64 {
return Err(format!(
"Expected 64-byte Ed25519 signature, got {}",
ge_info.value_len
));
}
let sig_bytes: [u8; 64] = vsf_bytes[ge_info.value_start..ge_info.value_start + 64]
.try_into()
.map_err(|_| "Failed to extract signature bytes")?;
if sig_bytes.iter().all(|&b| b == 0) {
return Err("Signature is all zeros - file not signed".to_string());
}
let mut temp_bytes = vsf_bytes.to_vec();
for i in 0..64 {
temp_bytes[ge_info.value_start + i] = 0;
}
let file_hash = blake3::hash(&temp_bytes);
let verifying_key =
VerifyingKey::from_bytes(&pubkey_bytes).map_err(|e| format!("Invalid pubkey: {}", e))?;
let signature = Signature::from_bytes(&sig_bytes);
match verifying_key.verify(file_hash.as_bytes(), &signature) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
#[cfg(feature = "crypto")]
pub fn extract_signer_pubkey(vsf_bytes: &[u8]) -> Result<[u8; 32], String> {
let ke_info = find_header_ke(vsf_bytes)?;
if ke_info.value_len != 32 {
return Err(format!(
"Expected 32-byte Ed25519 pubkey, got {}",
ke_info.value_len
));
}
vsf_bytes[ke_info.value_start..ke_info.value_start + 32]
.try_into()
.map_err(|_| "Failed to extract pubkey bytes".to_string())
}
#[allow(dead_code)]
struct HeaderFieldInfo {
#[allow(dead_code)]
marker_pos: usize, value_start: usize, value_len: usize, is_placeholder: bool, }
#[allow(dead_code)]
fn find_header_hp(data: &[u8]) -> Result<HeaderFieldInfo, String> {
if data.len() < 4 || &data[0..3] != "RÃ…".as_bytes() || data[3] != b'<' {
return Err("Invalid VSF magic".to_string());
}
let mut ptr = 4;
let _ = parse(data, &mut ptr).map_err(|e| format!("version: {}", e))?;
let _ = parse(data, &mut ptr).map_err(|e| format!("backward: {}", e))?;
let _ = parse(data, &mut ptr).map_err(|e| format!("header_len: {}", e))?;
if ptr < data.len() && data[ptr] == b'L' {
let _ = parse(data, &mut ptr).map_err(|e| format!("file_len: {}", e))?;
}
let _ = parse(data, &mut ptr).map_err(|e| format!("creation_time: {}", e))?;
let marker_pos = ptr;
if ptr >= data.len() || data[ptr] != b'h' {
return Err(format!("Expected hp at position {}", ptr));
}
let hp_type = parse(data, &mut ptr).map_err(|e| format!("hp: {}", e))?;
match hp_type {
VsfType::hp(bytes) => {
let value_start = ptr - bytes.len();
let is_placeholder = bytes.iter().all(|&b| b == 0);
Ok(HeaderFieldInfo {
marker_pos,
value_start,
value_len: bytes.len(),
is_placeholder,
})
}
_ => Err("Expected hp type".to_string()),
}
}
#[allow(dead_code)]
fn find_header_ke(data: &[u8]) -> Result<HeaderFieldInfo, String> {
if data.len() < 4 || &data[0..3] != "RÃ…".as_bytes() || data[3] != b'<' {
return Err("Invalid VSF magic".to_string());
}
let mut ptr = 4;
let _ = parse(data, &mut ptr).map_err(|e| format!("version: {}", e))?;
let _ = parse(data, &mut ptr).map_err(|e| format!("backward: {}", e))?;
let _ = parse(data, &mut ptr).map_err(|e| format!("header_len: {}", e))?;
if ptr < data.len() && data[ptr] == b'L' {
let _ = parse(data, &mut ptr).map_err(|e| format!("file_len: {}", e))?;
}
let _ = parse(data, &mut ptr).map_err(|e| format!("creation_time: {}", e))?;
let _ = parse(data, &mut ptr).map_err(|e| format!("hp: {}", e))?;
let marker_pos = ptr;
if ptr >= data.len() || data[ptr] != b'k' {
return Err("No ke found in header".to_string());
}
let ke_type = parse(data, &mut ptr).map_err(|e| format!("ke: {}", e))?;
match ke_type {
VsfType::ke(bytes) => {
let value_start = ptr - bytes.len();
let is_placeholder = bytes.iter().all(|&b| b == 0);
Ok(HeaderFieldInfo {
marker_pos,
value_start,
value_len: bytes.len(),
is_placeholder,
})
}
_ => Err(format!("Expected ke type, got {:?}", ke_type)),
}
}
#[allow(dead_code)]
fn find_header_ge(data: &[u8]) -> Result<HeaderFieldInfo, String> {
if data.len() < 4 || &data[0..3] != "RÃ…".as_bytes() || data[3] != b'<' {
return Err("Invalid VSF magic".to_string());
}
let mut ptr = 4;
let _ = parse(data, &mut ptr).map_err(|e| format!("version: {}", e))?;
let _ = parse(data, &mut ptr).map_err(|e| format!("backward: {}", e))?;
let _ = parse(data, &mut ptr).map_err(|e| format!("header_len: {}", e))?;
if ptr < data.len() && data[ptr] == b'L' {
let _ = parse(data, &mut ptr).map_err(|e| format!("file_len: {}", e))?;
}
let _ = parse(data, &mut ptr).map_err(|e| format!("creation_time: {}", e))?;
let _ = parse(data, &mut ptr).map_err(|e| format!("hp: {}", e))?;
if ptr >= data.len() || data[ptr] != b'k' {
return Err("No ke found before ge".to_string());
}
let _ = parse(data, &mut ptr).map_err(|e| format!("ke: {}", e))?;
let marker_pos = ptr;
if ptr >= data.len() || data[ptr] != b'g' {
return Err("No ge found in header".to_string());
}
let ge_type = parse(data, &mut ptr).map_err(|e| format!("ge: {}", e))?;
match ge_type {
VsfType::ge(bytes) => {
let value_start = ptr - bytes.len();
let is_placeholder = bytes.iter().all(|&b| b == 0);
Ok(HeaderFieldInfo {
marker_pos,
value_start,
value_len: bytes.len(),
is_placeholder,
})
}
_ => Err(format!("Expected ge type, got {:?}", ge_type)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builders::RawImageBuilder;
use crate::types::BitPackedTensor;
#[cfg(feature = "crypto")]
#[test]
fn test_sign_section_with_metadata() {
use crate::vsf_builder::{SectionMeta, VsfBuilder};
let meta = SectionMeta::new(
VsfType::ke(vec![0u8; 32]), );
let encrypted = vec![0u8; 100];
let unsigned_bytes = VsfBuilder::new()
.add_section_with_meta(
"announce",
vec![("payload".to_string(), VsfType::v(b'e', encrypted))],
meta,
)
.build()
.expect("Failed to build VSF");
println!("Built unsigned VSF: {} bytes", unsigned_bytes.len());
println!("Hex dump:");
for (i, b) in unsigned_bytes.iter().enumerate() {
if i % 32 == 0 {
println!();
print!("{:04x}: ", i);
}
print!("{:02x} ", b);
}
println!();
let signing_key = [1u8; 32]; match sign_section(unsigned_bytes.clone(), "announce", &signing_key) {
Ok(signed) => {
println!("\nSigned VSF: {} bytes", signed.len());
assert!(
signed.len() > unsigned_bytes.len(),
"Signed should be larger"
);
}
Err(e) => {
panic!("Sign error: {}", e);
}
}
}
#[test]
fn test_add_and_verify_file_hash() {
use crate::file_format::VsfSection;
use crate::vsf_builder::VsfBuilder;
let mut section = VsfSection::new("test");
section.add_field("value", VsfType::u(42, false));
let builder = VsfBuilder::new()
.add_section("test", vec![("value".to_string(), VsfType::u(42, false))]);
let verified_bytes = builder.build().unwrap();
assert!(verified_bytes.len() > 50);
let result = verify_file_hash(&verified_bytes);
assert!(result.is_ok(), "Hash verification should succeed");
}
#[test]
fn test_automatic_hash_inclusion() {
let samples: Vec<u64> = (0..16).collect();
let image = BitPackedTensor::pack(8, vec![4, 4], &samples);
let raw = RawImageBuilder::new(image);
let bytes = raw.build().unwrap();
let result = verify_file_hash(&bytes);
assert!(
result.is_ok(),
"All VSF files should have valid hash automatically"
);
}
#[test]
fn test_verify_hash_integrity() {
let samples: Vec<u64> = (0..16).collect();
let image = BitPackedTensor::pack(8, vec![4, 4], &samples);
let raw = RawImageBuilder::new(image);
let mut bytes = raw.build().unwrap();
let corruption_index = bytes.len() - 10;
bytes[corruption_index] ^= 0xFF;
let result = verify_file_hash(&bytes);
assert!(
result.is_err(),
"Corrupted file should fail hash verification"
);
}
#[cfg(feature = "crypto")]
#[test]
fn test_sign_and_verify_file() {
use crate::vsf_builder::VsfBuilder;
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
let signing_key = SigningKey::generate(&mut OsRng);
let pubkey = signing_key.verifying_key().to_bytes();
let unsigned = VsfBuilder::new()
.signature_ed25519(pubkey, [0u8; 64]) .add_section("test", vec![("value".to_string(), VsfType::u(42, false))])
.build()
.expect("Failed to build unsigned VSF");
println!("Unsigned VSF: {} bytes", unsigned.len());
let signed =
sign_file(unsigned.clone(), signing_key.as_bytes()).expect("Failed to sign file");
println!("Signed VSF: {} bytes", signed.len());
let valid = verify_file_signature(&signed).expect("Failed to verify signature");
assert!(valid, "Signature should be valid");
let mut corrupted = signed.clone();
corrupted[signed.len() - 10] ^= 0xFF;
let valid = verify_file_signature(&corrupted).expect("Should parse corrupted file");
assert!(!valid, "Corrupted file should fail verification");
let extracted = extract_signer_pubkey(&signed).expect("Failed to extract pubkey");
assert_eq!(extracted, pubkey, "Extracted pubkey should match");
}
#[cfg(feature = "crypto")]
#[test]
fn test_sign_file_with_custom_provenance() {
use crate::vsf_builder::VsfBuilder;
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
let signing_key = SigningKey::generate(&mut OsRng);
let pubkey = signing_key.verifying_key().to_bytes();
let ceremony_hp = *blake3::hash(b"test_ceremony").as_bytes();
let unsigned = VsfBuilder::new()
.provenance_hash(ceremony_hp)
.signature_ed25519(pubkey, [0u8; 64])
.add_section(
"clutch_full_offer",
vec![
("lower".to_string(), VsfType::hb(vec![1u8; 32])),
("higher".to_string(), VsfType::hb(vec![2u8; 32])),
],
)
.build()
.expect("Failed to build unsigned VSF");
let signed = sign_file(unsigned, signing_key.as_bytes()).expect("Failed to sign file");
let valid = verify_file_signature(&signed).expect("Failed to verify signature");
assert!(valid, "Signature should be valid with custom provenance");
}
}