use prikk_error::Result;
use prikk_object::{
BlockKind, BlockPayload, CanonicalEncode, ObjectEnvelope, ObjectId, ObjectType, RefKind,
RefStatePayload, RefUpdatePayload,
};
use super::{assert_ref_failed, assert_stage_failed};
use crate::maintainer_signing::MaintainerSigner;
use crate::test_support::{
signed_empty_block_envelope, signed_ref_state_envelope, signed_ref_update_envelope,
unique_temp_dir,
};
use crate::{
Ed25519MaintainerSigner, FileObjectStore, ObjectWriter, RefPublication, RefStore,
RepositoryLayout, VerificationStage, add_trusted_maintainer, derive_next_state_root,
maintainer_signature, verify_repository,
};
fn trusted_signer(seed_label: &str, byte: u8) -> Result<Ed25519MaintainerSigner> {
let signer = Ed25519MaintainerSigner::from_seed(seed_label, &[byte; 32])?;
Ok(signer)
}
fn adopt(layout: &RepositoryLayout, signer: &Ed25519MaintainerSigner) -> Result<()> {
let public_key_hex: String = signer
.public_key_bytes()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
add_trusted_maintainer(layout, signer.key_id(), &public_key_hex)?;
Ok(())
}
fn write_trusted_block(
objects: &mut FileObjectStore,
signer: &Ed25519MaintainerSigner,
distinguishing_snapshot: Option<ObjectId>,
) -> Result<ObjectId> {
let payload = BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root: derive_next_state_root(objects, None, &[])?,
snapshot_blob_ref: distinguishing_snapshot,
mainline_parent_id: None,
merge_baseline_block_id: None,
};
let mut envelope =
ObjectEnvelope::unsigned(ObjectType::Block, 2, payload.to_canonical_bytes()?);
let id = envelope.object_id();
envelope.add_signature(maintainer_signature(signer, ObjectType::Block, id)?)?;
objects.write_object(&envelope)
}
fn write_trusted_blob(
objects: &mut FileObjectStore,
signer: &Ed25519MaintainerSigner,
content: &[u8],
) -> Result<ObjectId> {
let payload = prikk_object::BlobPayload::new(prikk_object::BlobKind::Text, content.to_vec());
let mut envelope = ObjectEnvelope::unsigned(ObjectType::Blob, 1, payload.to_canonical_bytes()?);
let id = envelope.object_id();
envelope.add_signature(maintainer_signature(signer, ObjectType::Blob, id)?)?;
objects.write_object(&envelope)
}
fn write_trusted_ref_state(
objects: &mut FileObjectStore,
ref_name: &str,
target_object_id: ObjectId,
update_seq: u64,
previous_ref_state_id: Option<ObjectId>,
signer: &Ed25519MaintainerSigner,
sign: bool,
) -> Result<ObjectId> {
let payload = RefStatePayload {
ref_name: ref_name.to_string(),
kind: RefKind::Branch,
target_object_id,
update_seq,
previous_ref_state_id,
required_attestation_ids: Vec::new(),
closed: false,
};
let mut envelope =
ObjectEnvelope::unsigned(ObjectType::RefState, 1, payload.to_canonical_bytes()?);
let id = envelope.object_id();
if sign {
envelope.add_signature(maintainer_signature(signer, ObjectType::RefState, id)?)?;
}
objects.write_object(&envelope)
}
fn publish_ref_to_new_block_fake_signed_confounds_probes(
layout: &RepositoryLayout,
objects: &mut FileObjectStore,
ref_name: &str,
) -> Result<(prikk_object::ObjectId, prikk_object::ObjectId)> {
let block = signed_empty_block_envelope();
let block_id = objects.write_object(&block)?;
let ref_state = signed_ref_state_envelope(ref_name, None, block_id, 1);
let ref_state_id = ref_state.object_id();
let ref_update = signed_ref_update_envelope(ref_name, None, ref_state_id, block_id, 1);
RefStore::new(layout.clone()).publish(&RefPublication {
ref_name: ref_name.to_string(),
expected_previous_ref_state_id: None,
ref_state,
ref_update,
})?;
Ok((ref_state_id, block_id))
}
#[test]
fn verify_repository_detects_dangling_ref_target() -> Result<()> {
let root = unique_temp_dir("verify-dangling-ref-target");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let (_, block_id) =
publish_ref_to_new_block_fake_signed_confounds_probes(&layout, &mut objects, "heads/main")?;
crate::index::remove_index_entry_for_test(&layout, block_id)?;
let report = verify_repository(&layout)?;
assert_ref_failed(&report, "targets missing block");
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_pointer_index_entry_key_mismatch() -> Result<()> {
let root = unique_temp_dir("verify-pointer-index-key-mismatch");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let signer = trusted_signer("verify-pointer-index-key-mismatch", 0x17)?;
adopt(&layout, &signer)?;
let target_block = write_trusted_block(&mut objects, &signer, None)?;
let ref_state_id = write_trusted_ref_state(
&mut objects,
"heads/aux",
target_block,
1,
None,
&signer,
true,
)?;
crate::refs::write_ref_pointer_entry_with_explicit_key_for_test(
&layout,
crate::layout::ref_name_key_bytes("heads/aux"),
"heads/not-aux",
ref_state_id,
)?;
let report = verify_repository(&layout)?;
assert_ref_failed(
&report,
"pointer index entry ref_name_key does not match sha256",
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_ref_container_record_key_mismatch() -> Result<()> {
let root = unique_temp_dir("verify-ref-container-key-mismatch");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let signer = trusted_signer("verify-ref-container-key-mismatch", 0x18)?;
adopt(&layout, &signer)?;
let target_block = write_trusted_block(&mut objects, &signer, None)?;
let ref_state_id = write_trusted_ref_state(
&mut objects,
"heads/aux",
target_block,
1,
None,
&signer,
true,
)?;
let mismatched_update = build_signed_ref_update(
"heads/not-aux",
None,
ref_state_id,
target_block,
1,
&signer,
)?;
let framed = crate::refs::encode_ref_container_record_for_test(
crate::layout::ref_name_key_bytes("heads/aux"),
&mismatched_update,
)?;
std::fs::write(
layout.ref_log_container_slot_path(crate::layout::ContainerSlot::A),
framed,
)?;
let report = verify_repository(&layout)?;
assert_ref_failed(
&report,
"ref container record header ref_name_key does not match its own envelope",
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_ref_state_name_pointer_mismatch() -> Result<()> {
let root = unique_temp_dir("verify-ref-state-name-mismatch");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let (main_ref_state_id, _) =
publish_ref_to_new_block_fake_signed_confounds_probes(&layout, &mut objects, "heads/main")?;
crate::refs::write_ref_pointer_candidate(&layout, "heads/other", main_ref_state_id)?;
let report = verify_repository(&layout)?;
assert_ref_failed(&report, "name differs from pointer ref");
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_fails_closed_on_a_damaged_pointer_index_entry() -> Result<()> {
let root = unique_temp_dir("verify-pointer-index-fail-closed");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let signer = trusted_signer("verify-pointer-index-fail-closed", 0x19)?;
adopt(&layout, &signer)?;
let target_block = write_trusted_block(&mut objects, &signer, None)?;
let ref_state_id = write_trusted_ref_state(
&mut objects,
"heads/main",
target_block,
1,
None,
&signer,
true,
)?;
crate::refs::write_ref_pointer_candidate(&layout, "heads/main", ref_state_id)?;
let path = layout.ref_pointer_index_slot_path(crate::layout::ContainerSlot::A);
let mut bytes = std::fs::read(&path)?;
let last = bytes.last_mut().ok_or_else(|| {
prikk_error::PrikkError::Integrity("expected a pointer entry".to_string())
})?;
*last ^= 0x01;
std::fs::write(&path, bytes)?;
let report = verify_repository(&layout)?;
assert_stage_failed(
&report,
VerificationStage::Refs,
"ref pointer index has a damaged entry",
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_ref_update_ref_state_mismatch() -> Result<()> {
let root = unique_temp_dir("verify-ref-update-mismatch");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let signer = trusted_signer("verify-ref-update-mismatch", 0x11)?;
adopt(&layout, &signer)?;
let target_block = write_trusted_block(&mut objects, &signer, None)?;
let distinguishing_blob = write_trusted_blob(&mut objects, &signer, b"wrong-target-marker")?;
let wrong_target_block = write_trusted_block(&mut objects, &signer, Some(distinguishing_blob))?;
let ref_state_id = write_trusted_ref_state(
&mut objects,
"heads/main",
target_block,
1,
None,
&signer,
true,
)?;
let update_payload = RefUpdatePayload {
ref_name: "heads/main".to_string(),
old_ref_state_id: None,
new_ref_state_id: ref_state_id,
new_target_object_id: wrong_target_block,
update_seq: 1,
created_at: 0,
author_key_id: signer.key_id().to_string(),
};
let mut update_envelope = ObjectEnvelope::unsigned(
ObjectType::RefUpdate,
1,
update_payload.to_canonical_bytes()?,
);
let update_id = update_envelope.object_id();
update_envelope.add_signature(maintainer_signature(
&signer,
ObjectType::RefUpdate,
update_id,
)?)?;
crate::refs::append_log_record_for_signature_test(&layout, "heads/main", &update_envelope)?;
crate::refs::write_ref_pointer_candidate(&layout, "heads/main", ref_state_id)?;
let report = verify_repository(&layout)?;
assert_ref_failed(&report, "RefState disagrees with RefUpdate");
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_unsigned_ref_state() -> Result<()> {
let root = unique_temp_dir("verify-unsigned-ref-state");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let signer = trusted_signer("verify-unsigned-ref-state", 0x12)?;
adopt(&layout, &signer)?;
let target_block = write_trusted_block(&mut objects, &signer, None)?;
let ref_state_id = write_trusted_ref_state(
&mut objects,
"heads/main",
target_block,
1,
None,
&signer,
false,
)?;
let update_payload = RefUpdatePayload {
ref_name: "heads/main".to_string(),
old_ref_state_id: None,
new_ref_state_id: ref_state_id,
new_target_object_id: target_block,
update_seq: 1,
created_at: 0,
author_key_id: signer.key_id().to_string(),
};
let mut update_envelope = ObjectEnvelope::unsigned(
ObjectType::RefUpdate,
1,
update_payload.to_canonical_bytes()?,
);
let update_id = update_envelope.object_id();
update_envelope.add_signature(maintainer_signature(
&signer,
ObjectType::RefUpdate,
update_id,
)?)?;
crate::refs::append_log_record_for_signature_test(&layout, "heads/main", &update_envelope)?;
crate::refs::write_ref_pointer_candidate(&layout, "heads/main", ref_state_id)?;
let report = verify_repository(&layout)?;
assert_ref_failed(&report, "is unsigned");
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_incomplete_log_tail_without_pointer_lead() -> Result<()> {
let root = unique_temp_dir("verify-incomplete-log-tail");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let signer = trusted_signer("verify-incomplete-log-tail", 0x15)?;
adopt(&layout, &signer)?;
let target_block = write_trusted_block(&mut objects, &signer, None)?;
let state_id = write_trusted_ref_state(
&mut objects,
"heads/main",
target_block,
1,
None,
&signer,
true,
)?;
let update = build_signed_ref_update("heads/main", None, state_id, target_block, 1, &signer)?;
crate::refs::append_log_record_for_signature_test(&layout, "heads/main", &update)?;
crate::refs::write_ref_pointer_candidate(&layout, "heads/main", state_id)?;
crate::refs::append_torn_ref_log_tail_for_test(
&layout,
crate::layout::ref_name_key_bytes("heads/main"),
&update,
)?;
let report = verify_repository(&layout)?;
assert_ref_failed(&report, "incomplete log tail without a pointer lead");
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_nonzero_created_at_under_format2() -> Result<()> {
let root = unique_temp_dir("verify-nonzero-created-at");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let signer = trusted_signer("verify-nonzero-created-at", 0x16)?;
adopt(&layout, &signer)?;
let target_block = write_trusted_block(&mut objects, &signer, None)?;
let ref_state_id = write_trusted_ref_state(
&mut objects,
"heads/main",
target_block,
1,
None,
&signer,
true,
)?;
crate::refs::write_ref_pointer_candidate(&layout, "heads/main", ref_state_id)?;
let update_payload = RefUpdatePayload {
ref_name: "heads/main".to_string(),
old_ref_state_id: None,
new_ref_state_id: ref_state_id,
new_target_object_id: target_block,
update_seq: 1,
created_at: 7,
author_key_id: signer.key_id().to_string(),
};
let mut update_envelope = ObjectEnvelope::unsigned(
ObjectType::RefUpdate,
1,
update_payload.to_canonical_bytes()?,
);
let update_id = update_envelope.object_id();
update_envelope.add_signature(maintainer_signature(
&signer,
ObjectType::RefUpdate,
update_id,
)?)?;
std::fs::write(
layout.ref_log_container_slot_path(crate::layout::ContainerSlot::A),
crate::refs::encode_log_record_for_test(&update_envelope)?,
)?;
let report = verify_repository(&layout)?;
assert_stage_failed(
&report,
VerificationStage::Refs,
"format-2 RefUpdate requires created_at == 0",
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_ref_log_sequence_gap() -> Result<()> {
let root = unique_temp_dir("verify-ref-log-sequence-gap");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let signer = trusted_signer("verify-ref-log-sequence-gap", 0x13)?;
adopt(&layout, &signer)?;
let target_block = write_trusted_block(&mut objects, &signer, None)?;
let first_state_id = write_trusted_ref_state(
&mut objects,
"heads/main",
target_block,
1,
None,
&signer,
true,
)?;
let first_update =
build_signed_ref_update("heads/main", None, first_state_id, target_block, 1, &signer)?;
crate::refs::append_log_record_for_signature_test(&layout, "heads/main", &first_update)?;
crate::refs::write_ref_pointer_candidate(&layout, "heads/main", first_state_id)?;
let gap_state_id = write_trusted_ref_state(
&mut objects,
"heads/main",
target_block,
3,
Some(first_state_id),
&signer,
true,
)?;
let gap_update = build_signed_ref_update(
"heads/main",
Some(first_state_id),
gap_state_id,
target_block,
3,
&signer,
)?;
crate::refs::append_log_record_for_signature_test(&layout, "heads/main", &gap_update)?;
crate::refs::write_ref_pointer_candidate(&layout, "heads/main", gap_state_id)?;
let report = verify_repository(&layout)?;
assert_ref_failed(&report, "ref-log chain or sequence diverges");
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_unexplained_pointer_log_divergence() -> Result<()> {
let root = unique_temp_dir("verify-unexplained-divergence");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let signer = trusted_signer("verify-unexplained-divergence", 0x14)?;
adopt(&layout, &signer)?;
let target_block = write_trusted_block(&mut objects, &signer, None)?;
let y_state_id = write_trusted_ref_state(
&mut objects,
"heads/main",
target_block,
1,
None,
&signer,
true,
)?;
let y_update =
build_signed_ref_update("heads/main", None, y_state_id, target_block, 1, &signer)?;
crate::refs::append_log_record_for_signature_test(&layout, "heads/main", &y_update)?;
let x_state_id = write_trusted_ref_state(
&mut objects,
"heads/main",
target_block,
99,
None,
&signer,
true,
)?;
crate::refs::write_ref_pointer_candidate(&layout, "heads/main", x_state_id)?;
let report = verify_repository(&layout)?;
assert_ref_failed(&report, "unexplained pointer/log divergence");
let _ = std::fs::remove_dir_all(root);
Ok(())
}
fn build_signed_ref_update(
ref_name: &str,
old_ref_state_id: Option<ObjectId>,
new_ref_state_id: ObjectId,
new_target_object_id: ObjectId,
update_seq: u64,
signer: &Ed25519MaintainerSigner,
) -> Result<ObjectEnvelope> {
let payload = RefUpdatePayload {
ref_name: ref_name.to_string(),
old_ref_state_id,
new_ref_state_id,
new_target_object_id,
update_seq,
created_at: 0,
author_key_id: signer.key_id().to_string(),
};
let mut envelope =
ObjectEnvelope::unsigned(ObjectType::RefUpdate, 1, payload.to_canonical_bytes()?);
let id = envelope.object_id();
envelope.add_signature(maintainer_signature(signer, ObjectType::RefUpdate, id)?)?;
Ok(envelope)
}