use super::protection::BlobMeta;
use crate::store::{Object, ObjectParams, Store};
use sha2::{Digest, Sha256};
pub const SIG_TRAILER_LEN: usize = 65;
pub(super) fn append_trailer(store: &mut Store, trailer: &[u8]) -> anyhow::Result<()> {
let last_idx = find_last_chunk_idx(store);
let last_obj = &store.objects[last_idx as usize];
let last_max_payload = if last_obj.chunk_pos == 0 {
Object::head_payload_capacity(store.object_size, last_obj.blob_name.len())
} else {
Object::continuation_payload_capacity(store.object_size)
};
let last_payload_len = last_obj.payload.len();
let space_in_last = last_max_payload.saturating_sub(last_payload_len);
if space_in_last >= trailer.len() {
store.objects[last_idx as usize]
.payload
.extend_from_slice(trailer);
store.objects[last_idx as usize].dirty = true;
} else {
if store.free_count() == 0 {
anyhow::bail!("no room for the blob's trailer; nothing was stored");
}
store.objects[last_idx as usize]
.payload
.extend_from_slice(&trailer[..space_in_last]);
store.objects[last_idx as usize].dirty = true;
let spill_idx = store.alloc_free().unwrap();
let age = store.next_age();
let chunk_pos = store.objects[last_idx as usize].chunk_pos + 1;
let mut spill_obj = store.make_object(ObjectParams {
index: spill_idx,
age,
chunk_pos,
next_chunk: spill_idx, });
spill_obj.payload = trailer[space_in_last..].to_vec();
store.objects[spill_idx as usize] = spill_obj;
store.objects[last_idx as usize].next_chunk = spill_idx;
store.objects[last_idx as usize].dirty = true;
}
Ok(())
}
pub(super) fn find_last_chunk_idx(store: &Store) -> u8 {
let head = store
.objects
.iter()
.filter(|o| o.is_head())
.max_by_key(|o| o.age)
.expect("store must have at least one head after store_blob");
let chain = store.chunk_chain(head.index);
*chain.last().expect("chain must be non-empty")
}
pub fn collect_blob_chain(
head: &crate::store::Object,
store: &crate::store::Store,
) -> (Vec<u8>, Vec<u8>, bool) {
let blob_size = head.blob_size as usize;
let chain = store.chunk_chain(head.index);
let mut all_bytes: Vec<u8> = Vec::new();
let mut combined_without_last = 0usize;
for (i, &idx) in chain.iter().enumerate() {
let slot_bytes = &store.objects[idx as usize].payload;
if i + 1 < chain.len() {
combined_without_last += slot_bytes.len();
}
all_bytes.extend_from_slice(slot_bytes);
}
let has_supernumerary_slot = combined_without_last >= blob_size;
let payload = all_bytes[..blob_size.min(all_bytes.len())].to_vec();
let trailing = if all_bytes.len() > blob_size {
all_bytes[blob_size..].to_vec()
} else {
vec![]
};
(payload, trailing, has_supernumerary_slot)
}
const SIG_V1: u8 = 0x01;
pub(super) const SIG_V2: u8 = 0x02;
const SIG_V2_TAG: &[u8] = b"yb-blob-sig-v2\0";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SigVerdict {
Verified,
Unsigned,
Unverifiable,
NewerVersion,
Corrupted,
ChecksumOk,
}
impl std::fmt::Display for SigVerdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
SigVerdict::Verified => "VERIFIED",
SigVerdict::Unsigned => "UNSIGNED",
SigVerdict::Unverifiable | SigVerdict::NewerVersion => "UNVERIFIABLE",
SigVerdict::Corrupted => "CORRUPTED",
SigVerdict::ChecksumOk => "CHECKSUM OK",
})
}
}
pub(super) fn signed_digest_v2(meta: &BlobMeta, stored: &[u8]) -> [u8; 32] {
let name = meta.name.as_bytes();
let mut h = Sha256::new();
h.update(SIG_V2_TAG);
h.update([name.len() as u8]); h.update(name);
h.update([meta.key_byte, u8::from(meta.compressed)]);
h.update(meta.plain_size.to_le_bytes());
h.update(meta.stored_size.to_le_bytes());
h.update(meta.mtime.to_le_bytes());
h.update(stored);
h.finalize().into()
}
pub fn check_blob_signature(
head: &Object,
store: &Store,
verifying_key: Option<&p256::ecdsa::VerifyingKey>,
) -> SigVerdict {
use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature};
let blob_size = head.blob_size as usize;
let (payload, trailing, has_supernumerary) = collect_blob_chain(head, store);
if payload.len() + trailing.len() < blob_size {
return SigVerdict::Corrupted;
}
let zero_padding = trailing.iter().all(|&b| b == 0);
if trailing.is_empty() || trailing.len() != SIG_TRAILER_LEN || trailing[0] == 0x00 {
return if trailing.is_empty() || (zero_padding && !has_supernumerary) {
SigVerdict::Unsigned
} else {
SigVerdict::Corrupted
};
}
let digest: [u8; 32] = match trailing[0] {
SIG_V1 => Sha256::digest(&payload).into(),
SIG_V2 => signed_digest_v2(&BlobMeta::from_head(head), &payload),
_ => return SigVerdict::NewerVersion,
};
let Some(vk) = verifying_key else {
return SigVerdict::Unverifiable;
};
let mut raw = [0u8; 64];
raw.copy_from_slice(&trailing[1..SIG_TRAILER_LEN]);
let Ok(sig) = Signature::from_der(&crate::crypto::raw_ecdsa_to_der(&raw)) else {
return SigVerdict::Corrupted;
};
match vk.verify_prehash(&digest, &sig) {
Ok(()) => SigVerdict::Verified,
Err(_) => SigVerdict::Corrupted,
}
}