mod received_refs;
mod ref_cluster;
mod root_authority;
mod stage_containment;
mod trust;
mod wal_cluster;
use prikk_error::{PrikkError, Result};
use prikk_object::{
BlobKind, BlobPayload, BlockKind, BlockPayload, CanonicalEncode, CreateFile, MerkleRoot,
NodeId, ObjectEnvelope, ObjectId, ObjectType, Operation, OperationKind, PatchPayload,
PatchPurpose,
};
use crate::maintainer_signing::MaintainerSigner;
use crate::wal::{WalRecord, encode_record_for_test};
use crate::{
ActiveWalMetadataStatus, AuthorSignatureVerification, AuthorSigner, BlockStateStatus,
Ed25519AuthorSigner, FileObjectStore, ObjectItemStatus, ObjectVerification, ObjectWriter,
RepositoryLayout, RepositoryVerification, StageStatus, VerificationStage, Wal,
author_signature, derive_next_state_root, verify_repository, write_active_ref_metadata,
};
use crate::test_support::{
dummy_signature, maintainer_signature, rollback_patch_envelope, sample_object_id,
signed_patch_envelope, signed_ref_state_envelope, unique_temp_dir,
};
pub(super) fn assert_stage_failed(
report: &RepositoryVerification,
stage: VerificationStage,
expected_substring: &str,
) {
assert!(
report.has_stage_failure(),
"expected at least one stage to fail, got: {report:?}"
);
let Some(outcome) = report
.stage_outcomes
.iter()
.find(|outcome| outcome.stage == stage)
else {
panic!("expected a StageOutcome for stage {stage}, found none in: {report:?}");
};
match &outcome.status {
StageStatus::Failed { message } => assert!(
message.contains(expected_substring),
"stage {stage} failed, but message {message:?} does not contain {expected_substring:?}"
),
other => panic!("expected stage {stage} to be Failed, got: {other:?}"),
}
}
pub(super) fn assert_object_item_failed(report: &RepositoryVerification, expected_substring: &str) {
assert!(
report.has_item_failure(),
"expected at least one item to fail, got: {report:?}"
);
let found = report.object_outcomes.iter().any(|outcome| {
matches!(&outcome.status, ObjectItemStatus::Failed { message } if message.contains(expected_substring))
});
assert!(
found,
"expected an object item Failed with a message containing {expected_substring:?}, got: {:?}",
report.object_outcomes
);
}
pub(super) fn assert_block_state_failed(report: &RepositoryVerification, expected_substring: &str) {
assert!(
report.has_item_failure(),
"expected at least one item to fail, got: {report:?}"
);
let found = report.block_state_outcomes.iter().any(|outcome| {
matches!(&outcome.status, BlockStateStatus::Failed { message } if message.contains(expected_substring))
});
assert!(
found,
"expected a block-state outcome Failed with a message containing {expected_substring:?}, got: {:?}",
report.block_state_outcomes
);
}
pub(super) fn assert_ref_failed(report: &RepositoryVerification, expected_substring: &str) {
assert!(
report.has_item_failure(),
"expected at least one item to fail, got: {report:?}"
);
let found = report
.pointer_outcomes
.iter()
.chain(&report.log_outcomes)
.any(|outcome| {
matches!(&outcome.status, crate::refs::RefFileStatus::Failed { message } if message.contains(expected_substring))
})
|| report.ref_item_outcomes.iter().any(|outcome| {
matches!(&outcome.status, crate::refs::RefItemStatus::Failed { message } if message.contains(expected_substring))
})
|| report.received_ref_item_outcomes.iter().any(|outcome| {
matches!(&outcome.status, crate::refs::RefItemStatus::Failed { message } if message.contains(expected_substring))
});
assert!(
found,
"expected a ref-related outcome Failed with a message containing {expected_substring:?}, got: pointer_outcomes={:?} log_outcomes={:?} ref_item_outcomes={:?} received_ref_item_outcomes={:?}",
report.pointer_outcomes,
report.log_outcomes,
report.ref_item_outcomes,
report.received_ref_item_outcomes
);
}
pub(super) fn assert_wal_item_failed(report: &RepositoryVerification, expected_substring: &str) {
assert!(
report.has_item_failure(),
"expected at least one item to fail, got: {report:?}"
);
let found = report.wal_record_outcomes.iter().any(|outcome| {
matches!(&outcome.status, crate::wal::WalRecordStatus::Failed { message } if message.contains(expected_substring))
});
assert!(
found,
"expected a wal_record_outcomes entry Failed with a message containing {expected_substring:?}, got: {:?}",
report.wal_record_outcomes
);
}
#[test]
fn verify_repository_detects_every_missing_referenced_object() -> Result<()> {
type CaseFn = fn(&FileObjectStore, ObjectId) -> Result<(BlockPayload, &'static str)>;
let cases: Vec<(&str, CaseFn)> = vec![
("missing-parent", |_store, missing| {
Ok((
BlockPayload {
parent_block_ids: vec![missing],
kind: BlockKind::Normal,
patch_ids: Vec::new(),
state_merkle_root: MerkleRoot([0xC0_u8; 32]),
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
"references missing parent block",
))
}),
("missing-patch", |_store, missing| {
Ok((
BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: vec![missing],
state_merkle_root: MerkleRoot([0xC1_u8; 32]),
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
"references missing block patch",
))
}),
("missing-snapshot-blob", |store, missing| {
let state_merkle_root = derive_next_state_root(store, None, &[])?;
Ok((
BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root,
snapshot_blob_ref: Some(missing),
mainline_parent_id: None,
merge_baseline_block_id: None,
},
"references missing snapshot blob",
))
}),
];
for (name, case_fn) in cases {
let root = unique_temp_dir(&format!("missing-referenced-{name}"));
let layout = RepositoryLayout::init(root.clone())?;
let mut store = FileObjectStore::new(layout.clone());
let missing = sample_object_id(&format!("{name}-target"));
let (payload, expected_substring) = case_fn(&store, missing)?;
write_signed_block(&mut store, &payload)?;
let report = verify_repository(&layout)?;
assert_object_item_failed(&report, expected_substring);
let _ = std::fs::remove_dir_all(root);
}
Ok(())
}
#[test]
fn verify_repository_detects_block_with_state_root_mismatch() -> Result<()> {
let root = unique_temp_dir("block-state-root-mismatch");
let layout = RepositoryLayout::init(root.clone())?;
let mut store = FileObjectStore::new(layout.clone());
write_signed_block(
&mut store,
&BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root: MerkleRoot([0xEE_u8; 32]),
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
)?;
let report = verify_repository(&layout)?;
assert_block_state_failed(&report, "state root does not match authoritative replay");
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_every_block_shape_violation() -> Result<()> {
type CaseFn =
fn(&FileObjectStore, ObjectId, ObjectId, ObjectId) -> Result<(BlockPayload, &'static str)>;
let cases: Vec<(&str, CaseFn)> = vec![
("root-with-parent", |store, genesis, _a, _b| {
let state_merkle_root = derive_next_state_root(store, Some(genesis), &[])?;
Ok((
BlockPayload {
parent_block_ids: vec![genesis],
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
"Root Block must have zero parents",
))
}),
("normal-with-zero-parents", |store, _genesis, _a, _b| {
let state_merkle_root = derive_next_state_root(store, None, &[])?;
Ok((
BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Normal,
patch_ids: Vec::new(),
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
"Normal Block must have exactly one parent",
))
}),
("merge-with-one-parent", |store, genesis, _a, _b| {
let state_merkle_root = derive_next_state_root(store, Some(genesis), &[])?;
Ok((
BlockPayload {
parent_block_ids: vec![genesis],
kind: BlockKind::Merge,
patch_ids: Vec::new(),
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: Some(genesis),
merge_baseline_block_id: Some(genesis),
},
"Merge Block must have exactly two parents",
))
}),
("repair-kind-unauthorized", |store, _genesis, _a, _b| {
let state_merkle_root = derive_next_state_root(store, None, &[])?;
Ok((
BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Repair,
patch_ids: Vec::new(),
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
"Block kind is not authorized",
))
}),
("root-with-mainline-field", |store, genesis, _a, _b| {
let state_merkle_root = derive_next_state_root(store, None, &[])?;
Ok((
BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: Some(genesis),
merge_baseline_block_id: None,
},
"must not carry a mainline parent or merge baseline",
))
}),
("merge-without-mainline", |store, genesis, a, b| {
let mut parents = vec![a, b];
parents.sort();
let state_merkle_root = derive_next_state_root(store, None, &[])?;
Ok((
BlockPayload {
parent_block_ids: parents,
kind: BlockKind::Merge,
patch_ids: Vec::new(),
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: Some(genesis),
},
"Merge Block must name a mainline parent",
))
}),
("merge-mainline-not-a-parent", |store, genesis, a, b| {
let mut parents = vec![a, b];
parents.sort();
let state_merkle_root = derive_next_state_root(store, Some(genesis), &[])?;
Ok((
BlockPayload {
parent_block_ids: parents,
kind: BlockKind::Merge,
patch_ids: Vec::new(),
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: Some(genesis),
merge_baseline_block_id: Some(genesis),
},
"mainline parent must be one of its own parents",
))
}),
("merge-without-baseline", |store, _genesis, a, b| {
let mut parents = vec![a, b];
parents.sort();
let state_merkle_root = derive_next_state_root(store, Some(a), &[])?;
Ok((
BlockPayload {
parent_block_ids: parents,
kind: BlockKind::Merge,
patch_ids: Vec::new(),
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: Some(a),
merge_baseline_block_id: None,
},
"must record the baseline confluence was proven against",
))
}),
];
for (name, case_fn) in cases {
let root = unique_temp_dir(&format!("block-shape-{name}"));
let layout = RepositoryLayout::init(root.clone())?;
let mut store = FileObjectStore::new(layout.clone());
let genesis_root = derive_next_state_root(&store, None, &[])?;
let genesis = write_signed_block(
&mut store,
&BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root: genesis_root,
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
)?;
let parent_a = write_create_child(&mut store, genesis, "a.txt", 0x51)?;
let parent_b = write_create_child(&mut store, genesis, "b.txt", 0x52)?;
let (payload, expected_substring) = case_fn(&store, genesis, parent_a, parent_b)?;
write_signed_block(&mut store, &payload)?;
let report = verify_repository(&layout)?;
assert_block_state_failed(&report, expected_substring);
let _ = std::fs::remove_dir_all(root);
}
Ok(())
}
fn write_signed_block(store: &mut FileObjectStore, payload: &BlockPayload) -> Result<ObjectId> {
let payload_bytes = payload.to_canonical_bytes()?;
let mut block = ObjectEnvelope::unsigned(ObjectType::Block, 2, payload_bytes);
block.add_signature(maintainer_signature())?;
store.write_object(&block)
}
fn write_create_child(
store: &mut FileObjectStore,
parent: ObjectId,
path: &str,
node_byte: u8,
) -> Result<ObjectId> {
let blob = BlobPayload::new(BlobKind::Text, format!("{path}\n").into_bytes());
let mut blob_env = ObjectEnvelope::unsigned(ObjectType::Blob, 1, blob.to_canonical_bytes()?);
blob_env.add_signature(maintainer_signature())?;
let blob_id = store.write_object(&blob_env)?;
let patch = PatchPayload {
operations: vec![Operation {
op_seq: 1,
op_id: None,
preconditions: Vec::new(),
kind: OperationKind::CreateFile(CreateFile {
path: path.to_string(),
node_id: NodeId::from_bytes([node_byte; 32]),
blob_id,
mode: 0o100_644,
}),
}],
parent_patch_ids: Vec::new(),
intent: None,
preconditions: Vec::new(),
purpose: PatchPurpose::Normal,
};
let mut patch_env = ObjectEnvelope::unsigned(ObjectType::Patch, 1, patch.to_canonical_bytes()?);
patch_env.add_signature(maintainer_signature())?;
let patch_id = store.write_object(&patch_env)?;
let state_merkle_root = derive_next_state_root(store, Some(parent), &[patch_id])?;
write_signed_block(
store,
&BlockPayload {
parent_block_ids: vec![parent],
kind: BlockKind::Normal,
patch_ids: vec![patch_id],
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
)
}
#[test]
fn verify_repository_flags_merge_block_with_baseline_not_a_common_ancestor() -> Result<()> {
let root = unique_temp_dir("merge-baseline-forged");
let layout = RepositoryLayout::init(root.clone())?;
let mut store = FileObjectStore::new(layout.clone());
let genesis_root = derive_next_state_root(&store, None, &[])?;
let genesis = write_signed_block(
&mut store,
&BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root: genesis_root,
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
)?;
let mainline_parent = write_create_child(&mut store, genesis, "mainline.txt", 0x10)?;
let secondary_parent = write_create_child(&mut store, genesis, "secondary.txt", 0x20)?;
let mut parents = vec![mainline_parent, secondary_parent];
parents.sort();
let forged_baseline = sample_object_id("forged-baseline-not-an-ancestor");
let merge_root = derive_next_state_root(&store, Some(mainline_parent), &[])?;
let merge_block = write_signed_block(
&mut store,
&BlockPayload {
parent_block_ids: parents,
kind: BlockKind::Merge,
patch_ids: Vec::new(),
state_merkle_root: merge_root,
snapshot_blob_ref: None,
mainline_parent_id: Some(mainline_parent),
merge_baseline_block_id: Some(forged_baseline),
},
)?;
let report = verify_repository(&layout)?;
assert!(report.has_merge_baseline_divergence());
assert_eq!(report.merge_baseline_divergences.len(), 1);
let Some(divergence) = report.merge_baseline_divergences.first() else {
panic!("expected exactly one merge-baseline divergence");
};
assert_eq!(divergence.block_id, merge_block);
assert_eq!(divergence.recorded_baseline, forged_baseline);
assert_eq!(divergence.mainline_parent_id, mainline_parent);
assert_eq!(divergence.secondary_parent_id, secondary_parent);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_accepts_merge_block_with_genuine_common_ancestor_baseline() -> Result<()> {
let root = unique_temp_dir("merge-baseline-genuine");
let layout = RepositoryLayout::init(root.clone())?;
let mut store = FileObjectStore::new(layout.clone());
let genesis_root = derive_next_state_root(&store, None, &[])?;
let genesis = write_signed_block(
&mut store,
&BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root: genesis_root,
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
)?;
let mainline_parent = write_create_child(&mut store, genesis, "mainline.txt", 0x30)?;
let secondary_parent = write_create_child(&mut store, genesis, "secondary.txt", 0x40)?;
let mut parents = vec![mainline_parent, secondary_parent];
parents.sort();
let merge_root = derive_next_state_root(&store, Some(mainline_parent), &[])?;
write_signed_block(
&mut store,
&BlockPayload {
parent_block_ids: parents,
kind: BlockKind::Merge,
patch_ids: Vec::new(),
state_merkle_root: merge_root,
snapshot_blob_ref: None,
mainline_parent_id: Some(mainline_parent),
merge_baseline_block_id: Some(genesis),
},
)?;
let report = verify_repository(&layout)?;
assert!(!report.has_merge_baseline_divergence());
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_counts_objects_and_wal_records() {
let root = unique_temp_dir("verify");
let layout = RepositoryLayout::init(root.clone());
assert!(layout.is_ok());
if let Ok(layout) = layout {
let mut store = FileObjectStore::new(layout.clone());
let mut blob = ObjectEnvelope::unsigned(ObjectType::Blob, 1, b"payload".to_vec());
assert!(blob.add_signature(dummy_signature()).is_ok());
assert!(store.write_object(&blob).is_ok());
let wal = Wal::for_layout(&layout);
assert!(write_active_ref_metadata(&layout, "heads/main").is_ok());
assert!(wal.append_patch(&signed_patch_envelope()).is_ok());
let report = verify_repository(&layout);
assert!(report.is_ok());
if let Ok(report) = report {
assert_eq!(report.checked_objects, Some(1));
assert_eq!(report.checked_blocks, Some(0));
assert_eq!(report.checked_wal_records, Some(1));
assert_eq!(report.persisted_wal_patches, Some(0));
assert_eq!(report.checked_refs, Some(0));
assert_eq!(report.checked_ref_log_records, Some(0));
assert_eq!(report.trailing_partial_wal_bytes, Some(0));
assert_eq!(
report.active_wal_metadata_status,
Some(ActiveWalMetadataStatus::ValidForNonEmptyWal {
ref_name: "heads/main".to_string()
})
);
}
}
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn verify_repository_reports_active_wal_ordering_violation() {
let root = unique_temp_dir("verify-wal-ordering");
let layout = RepositoryLayout::init(root.clone());
assert!(layout.is_ok());
if let Ok(layout) = layout {
let wal = Wal::for_layout(&layout);
assert!(wal.append_patch(&signed_patch_envelope()).is_ok());
let first = WalRecord {
seq: 1,
envelope: signed_patch_envelope(),
};
let second = WalRecord {
seq: 1,
envelope: rollback_patch_envelope(),
};
let mut bytes = Vec::new();
let first_encoded = encode_record_for_test(&first);
assert!(first_encoded.is_ok());
if let Ok(first_encoded) = first_encoded {
bytes.extend(first_encoded);
}
let second_encoded = encode_record_for_test(&second);
assert!(second_encoded.is_ok());
if let Ok(second_encoded) = second_encoded {
bytes.extend(second_encoded);
}
assert!(std::fs::write(wal.path(), &bytes).is_ok());
assert!(write_active_ref_metadata(&layout, "heads/main").is_ok());
let report = verify_repository(&layout);
assert!(report.is_ok());
if let Ok(report) = report {
assert_eq!(report.checked_wal_records, Some(2));
assert!(report.has_active_wal_ordering_issue());
assert_eq!(
report.active_wal_ordering_issues,
vec![crate::ActiveWalOrderingIssue {
index: 1,
previous_seq: 1,
seq: 1,
}]
);
}
}
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn verify_repository_reports_missing_active_metadata_for_non_empty_wal() {
let root = unique_temp_dir("verify-active-metadata-missing");
let layout = RepositoryLayout::init(root.clone());
assert!(layout.is_ok());
if let Ok(layout) = layout {
let wal = Wal::for_layout(&layout);
assert!(wal.append_patch(&signed_patch_envelope()).is_ok());
let report = verify_repository(&layout);
assert!(report.is_ok());
if let Ok(report) = report {
assert_eq!(
report.active_wal_metadata_status,
Some(ActiveWalMetadataStatus::MissingForNonEmptyWal)
);
assert!(report.has_active_wal_metadata_integrity_issue());
}
}
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn verify_repository_reports_malformed_empty_active_metadata_as_warning_state() {
let root = unique_temp_dir("verify-active-metadata-debris");
let layout = RepositoryLayout::init(root.clone());
assert!(layout.is_ok());
if let Ok(layout) = layout {
assert!(std::fs::write(layout.default_active_ref_name_path(), b"tags/v1").is_ok());
let report = verify_repository(&layout);
assert!(report.is_ok());
if let Ok(report) = report {
assert!(matches!(
report.active_wal_metadata_status,
Some(ActiveWalMetadataStatus::InvalidForEmptyWal { .. })
));
assert!(report.has_active_wal_metadata_warning());
assert!(!report.has_active_wal_metadata_integrity_issue());
}
}
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn verify_repository_reports_malformed_active_metadata_for_non_empty_wal_as_integrity_issue() {
let root = unique_temp_dir("verify-active-metadata-invalid-nonempty");
let layout = RepositoryLayout::init(root.clone());
assert!(layout.is_ok());
if let Ok(layout) = layout {
let wal = Wal::for_layout(&layout);
assert!(wal.append_patch(&signed_patch_envelope()).is_ok());
assert!(std::fs::write(layout.default_active_ref_name_path(), b"tags/v1").is_ok());
let report = verify_repository(&layout);
assert!(report.is_ok());
if let Ok(report) = report {
assert!(matches!(
report.active_wal_metadata_status,
Some(ActiveWalMetadataStatus::InvalidForNonEmptyWal { .. })
));
assert!(report.has_active_wal_metadata_integrity_issue());
assert!(!report.has_active_wal_metadata_warning());
}
}
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn verify_repository_detects_index_entry_resolving_to_a_different_object() -> Result<()> {
let root = unique_temp_dir("verify-index-wrong-location");
let layout = RepositoryLayout::init(root.clone())?;
let real_envelope = signed_patch_envelope();
let real_id = real_envelope.object_id();
let record_bytes =
crate::container::encode_container_record_for_test(ObjectType::Patch, &real_envelope)?;
std::fs::write(
layout.container_slot_path(ObjectType::Patch, crate::layout::ContainerSlot::A),
&record_bytes,
)?;
let wrong_id = sample_object_id("not-this-patch-s-real-id");
let bad_entry = crate::index::IndexEntry {
object_id: wrong_id,
object_type: ObjectType::Patch,
slot: crate::layout::ContainerSlot::A,
offset: 0,
length: record_bytes.len() as u64,
container_checksum: [0_u8; 32],
};
std::fs::write(
layout.container_index_path(),
crate::index::encode_index_record(&bad_entry)?,
)?;
assert_ne!(wrong_id, real_id);
let report = verify_repository(&layout)?;
assert_stage_failed(
&report,
VerificationStage::Objects,
"resolves to an envelope with computed id",
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_envelope_type_mismatch() -> Result<()> {
let root = unique_temp_dir("verify-envelope-type-mismatch");
let layout = RepositoryLayout::init(root.clone())?;
let patch_envelope = signed_patch_envelope();
assert_eq!(patch_envelope.object_type, ObjectType::Patch);
let record_bytes =
crate::container::encode_container_record(ObjectType::Blob, &patch_envelope)?;
std::fs::write(
layout.container_slot_path(ObjectType::Blob, crate::layout::ContainerSlot::A),
&record_bytes,
)?;
let report = verify_repository(&layout)?;
assert_object_item_failed(&report, "is under type");
let _ = std::fs::remove_dir_all(root);
Ok(())
}
fn author_verification_test_patch_payload(discriminant: u8) -> PatchPayload {
PatchPayload {
operations: vec![Operation {
op_seq: 1,
op_id: None,
preconditions: Vec::new(),
kind: OperationKind::CreateFile(CreateFile {
path: format!("dc53-author-verify-{discriminant}.txt"),
node_id: NodeId::from_bytes([discriminant; 32]),
blob_id: ObjectId::from_bytes([discriminant.wrapping_add(1); 32]),
mode: 0o100_644,
}),
}],
parent_patch_ids: Vec::new(),
intent: None,
preconditions: Vec::new(),
purpose: PatchPurpose::Normal,
}
}
fn write_author_signed_patch(
layout: &RepositoryLayout,
discriminant: u8,
key_id: &str,
corrupt_signature: bool,
) -> Result<(String, ObjectId)> {
let payload = author_verification_test_patch_payload(discriminant);
let canonical = payload.to_canonical_bytes()?;
let mut envelope = ObjectEnvelope::unsigned(ObjectType::Patch, 1, canonical);
let object_id = envelope.object_id();
let signer = Ed25519AuthorSigner::from_seed(key_id, &[discriminant; 32])?;
let mut signature = author_signature(&signer, object_id)?;
if corrupt_signature {
if let Some(byte) = signature.signature_bytes.get_mut(0) {
*byte ^= 1;
}
}
envelope.add_signature(signature)?;
let record_bytes = crate::container::encode_container_record(ObjectType::Patch, &envelope)?;
std::fs::write(
layout.container_slot_path(ObjectType::Patch, crate::layout::ContainerSlot::A),
&record_bytes,
)?;
Ok((signer.key_id().to_string(), object_id))
}
fn find_object_verification(
report: &RepositoryVerification,
object_id: ObjectId,
) -> &ObjectVerification {
report
.object_outcomes
.iter()
.find_map(|outcome| match &outcome.status {
ObjectItemStatus::Evaluated(verification)
| ObjectItemStatus::Unindexed(verification)
if verification.object_id == object_id =>
{
Some(verification)
}
_ => None,
})
.unwrap_or_else(|| panic!("expected an evaluated outcome for {object_id}, got: {report:?}"))
}
#[test]
fn verify_repository_fails_a_tampered_author_signature_against_recorded_key_material() -> Result<()>
{
let root = unique_temp_dir("verify-author-signature-fails");
let layout = RepositoryLayout::init(root.clone())?;
let signer = Ed25519AuthorSigner::from_seed("fails-author", &[0x61; 32])?;
let active_lock = crate::lock::ActiveLock::acquire(&layout)?;
crate::author_key_index::record_author_key_material(
&layout,
signer.key_id(),
signer.public_key_bytes(),
&active_lock,
)?;
write_author_signed_patch(&layout, 0x61, "fails-author", true)?;
let report = verify_repository(&layout)?;
assert_object_item_failed(
&report,
"AUTHOR signature does not verify against recorded key material",
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_fails_for_a_key_id_with_conflicting_recorded_keys() -> Result<()> {
let root = unique_temp_dir("verify-author-signature-conflict");
let layout = RepositoryLayout::init(root.clone())?;
let signer = Ed25519AuthorSigner::from_seed("conflict-author", &[0x91; 32])?;
let active_lock = crate::lock::ActiveLock::acquire(&layout)?;
crate::author_key_index::record_author_key_material(
&layout,
signer.key_id(),
signer.public_key_bytes(),
&active_lock,
)?;
crate::author_key_index::force_conflicting_author_key_entry_for_test(
&layout,
signer.key_id(),
[0xee; 32],
)?;
write_author_signed_patch(&layout, 0x91, "conflict-author", false)?;
let report = verify_repository(&layout)?;
assert_object_item_failed(&report, "has more than one distinct recorded public key");
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_reports_sound_for_a_signature_verifying_against_recorded_material()
-> Result<()> {
let root = unique_temp_dir("verify-author-signature-sound");
let layout = RepositoryLayout::init(root.clone())?;
let signer = Ed25519AuthorSigner::from_seed("sound-author", &[0x71; 32])?;
let active_lock = crate::lock::ActiveLock::acquire(&layout)?;
crate::author_key_index::record_author_key_material(
&layout,
signer.key_id(),
signer.public_key_bytes(),
&active_lock,
)?;
let (key_id, object_id) = write_author_signed_patch(&layout, 0x71, "sound-author", false)?;
let report = verify_repository(&layout)?;
assert!(
!report.has_item_failure(),
"expected no item failure, got: {report:?}"
);
let verification = find_object_verification(&report, object_id);
assert_eq!(
verification.author_verification,
Some(AuthorSignatureVerification::Sound { key_id })
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_reports_unverifiable_for_a_key_id_with_no_recorded_material() -> Result<()> {
let root = unique_temp_dir("verify-author-signature-unverifiable");
let layout = RepositoryLayout::init(root.clone())?;
let (key_id, object_id) =
write_author_signed_patch(&layout, 0x81, "unverifiable-author", false)?;
let report = verify_repository(&layout)?;
assert!(
!report.has_item_failure(),
"expected no item failure, got: {report:?}"
);
let verification = find_object_verification(&report, object_id);
assert_eq!(
verification.author_verification,
Some(AuthorSignatureVerification::Unverifiable { key_id })
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_every_directory_shape_violation() -> Result<()> {
let non_directory_root = unique_temp_dir("verify-non-directory-in-type-dir");
let layout = RepositoryLayout::init(non_directory_root.clone())?;
let mut store = FileObjectStore::new(layout.clone());
store.write_object(&ObjectEnvelope::unsigned(
ObjectType::Blob,
1,
b"payload".to_vec(),
))?;
let object_type_dir = layout.object_type_dir(ObjectType::Blob);
std::fs::create_dir_all(&object_type_dir)?;
let stray_file = object_type_dir.join("zz");
std::fs::write(&stray_file, b"not a prefix directory")?;
let report = verify_repository(&layout)?;
assert_stage_failed(
&report,
VerificationStage::Objects,
"unexpected non-directory in object type directory",
);
let _ = std::fs::remove_dir_all(non_directory_root);
let non_file_root = unique_temp_dir("verify-non-file-in-prefix-dir");
let layout = RepositoryLayout::init(non_file_root.clone())?;
let mut store = FileObjectStore::new(layout.clone());
let id = store.write_object(&ObjectEnvelope::unsigned(
ObjectType::Blob,
1,
b"payload".to_vec(),
))?;
let prefix_dir = layout
.object_path(ObjectType::Blob, id)
.parent()
.ok_or_else(|| PrikkError::Io("object path has no parent".to_string()))?
.to_path_buf();
std::fs::create_dir_all(prefix_dir.join("stray-directory"))?;
let report = verify_repository(&layout)?;
assert_stage_failed(
&report,
VerificationStage::Objects,
"unexpected non-file in object prefix directory",
);
let _ = std::fs::remove_dir_all(non_file_root);
Ok(())
}
#[test]
fn verify_repository_flags_untrusted_block_signer_and_clears_once_trusted() -> Result<()> {
let root = unique_temp_dir("verify-untrusted-block-signer");
let layout = RepositoryLayout::init(root.clone())?;
let mut store = FileObjectStore::new(layout.clone());
let trusted_signer =
crate::Ed25519MaintainerSigner::from_seed("verify-trust-maintainer", &[0x63; 32])?;
let trusted_public_key_hex: String = trusted_signer
.public_key_bytes()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
crate::add_trusted_maintainer(&layout, trusted_signer.key_id(), &trusted_public_key_hex)?;
let untrusted_payload = BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root: derive_next_state_root(&store, None, &[])?,
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
};
let untrusted_block_id = write_signed_block(&mut store, &untrusted_payload)?;
let snapshot_blob = BlobPayload::new(BlobKind::Text, b"trusted-block-snapshot".to_vec());
let mut snapshot_envelope =
ObjectEnvelope::unsigned(ObjectType::Blob, 1, snapshot_blob.to_canonical_bytes()?);
snapshot_envelope.add_signature(maintainer_signature())?;
let snapshot_blob_id = snapshot_envelope.object_id();
store.write_object(&snapshot_envelope)?;
let trusted_payload = BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root: derive_next_state_root(&store, None, &[])?,
snapshot_blob_ref: Some(snapshot_blob_id),
mainline_parent_id: None,
merge_baseline_block_id: None,
};
let trusted_payload_bytes = trusted_payload.to_canonical_bytes()?;
let mut trusted_envelope =
ObjectEnvelope::unsigned(ObjectType::Block, 2, trusted_payload_bytes);
let trusted_id = trusted_envelope.object_id();
trusted_envelope.add_signature(crate::maintainer_signature(
&trusted_signer,
ObjectType::Block,
trusted_id,
)?)?;
store.write_object(&trusted_envelope)?;
let report = verify_repository(&layout)?;
assert!(report.has_publication_trust_issues());
assert!(report.publication_trust_issues.iter().any(|issue| {
issue.code == "PRIKK-TRUST-PUBLICATION-UNTRUSTED"
&& issue.message.contains(&untrusted_block_id.to_string())
}));
assert!(
!report
.publication_trust_issues
.iter()
.any(|issue| issue.message.contains(&trusted_id.to_string())),
"the trusted block must carry no publication-trust issue of its own: {:?}",
report.publication_trust_issues
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_flags_untrusted_ref_state_signer_and_clears_once_trusted() -> Result<()> {
let root = unique_temp_dir("verify-untrusted-refstate-signer");
let layout = RepositoryLayout::init(root.clone())?;
let mut store = FileObjectStore::new(layout.clone());
let trusted_signer =
crate::Ed25519MaintainerSigner::from_seed("verify-trust-maintainer-rs", &[0x64; 32])?;
let trusted_public_key_hex: String = trusted_signer
.public_key_bytes()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
crate::add_trusted_maintainer(&layout, trusted_signer.key_id(), &trusted_public_key_hex)?;
let untrusted_envelope = signed_ref_state_envelope(
"heads/untrusted",
None,
sample_object_id("untrusted-target"),
1,
);
let untrusted_id = store.write_object(&untrusted_envelope)?;
let trusted_payload = prikk_object::RefStatePayload {
ref_name: "heads/trusted".to_string(),
kind: prikk_object::RefKind::Branch,
target_object_id: sample_object_id("trusted-target"),
update_seq: 1,
previous_ref_state_id: None,
required_attestation_ids: Vec::new(),
closed: false,
};
let trusted_bytes = trusted_payload.to_canonical_bytes()?;
let mut trusted_envelope = ObjectEnvelope::unsigned(ObjectType::RefState, 1, trusted_bytes);
let trusted_id = trusted_envelope.object_id();
trusted_envelope.add_signature(crate::maintainer_signature(
&trusted_signer,
ObjectType::RefState,
trusted_id,
)?)?;
store.write_object(&trusted_envelope)?;
let report = verify_repository(&layout)?;
assert!(report.has_publication_trust_issues());
assert!(report.publication_trust_issues.iter().any(|issue| {
issue.code == "PRIKK-TRUST-PUBLICATION-UNTRUSTED"
&& issue.message.contains(&untrusted_id.to_string())
}));
assert!(
!report
.publication_trust_issues
.iter()
.any(|issue| issue.message.contains(&trusted_id.to_string())),
"the trusted ref-state must carry no publication-trust issue of its own: {:?}",
report.publication_trust_issues
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_rejects_malformed_signature_shape() -> Result<()> {
let root = unique_temp_dir("verify-malformed-signature-shape");
let layout = RepositoryLayout::init(root.clone())?;
let blob = BlobPayload::new(BlobKind::Text, b"strict-shape fixture\n".to_vec());
let mut envelope = ObjectEnvelope::unsigned(ObjectType::Blob, 1, blob.to_canonical_bytes()?);
envelope.signatures = vec![prikk_object::Signature {
algorithm: prikk_object::SignatureAlgorithm::Ed25519,
key_id: "maintainer-key".to_string(),
signature_bytes: vec![5_u8; 63],
created_at: 8,
signer_role: prikk_object::SignerRole::Maintainer,
}];
let record_bytes =
crate::container::encode_container_record_for_test(ObjectType::Blob, &envelope)?;
std::fs::write(
layout.container_slot_path(ObjectType::Blob, crate::layout::ContainerSlot::A),
&record_bytes,
)?;
let report = verify_repository(&layout)?;
assert_object_item_failed(&report, "malformed algorithm shape");
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn verify_repository_detects_invalid_trust_policy() -> Result<()> {
let missing_root = unique_temp_dir("verify-trust-policy-missing");
let layout = RepositoryLayout::init(missing_root.clone())?;
let mut store = FileObjectStore::new(layout.clone());
let state_merkle_root = derive_next_state_root(&store, None, &[])?;
write_signed_block(
&mut store,
&BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
)?;
let report = verify_repository(&layout)?;
assert!(report.has_publication_trust_issues());
assert!(
report
.publication_trust_issues
.iter()
.any(|issue| issue.code == "PRIKK-TRUST-POLICY-INVALID"),
"expected a missing-policy PRIKK-TRUST-POLICY-INVALID issue, got: {:?}",
report.publication_trust_issues
);
let _ = std::fs::remove_dir_all(missing_root);
let malformed_root = unique_temp_dir("verify-trust-policy-malformed");
let layout = RepositoryLayout::init(malformed_root.clone())?;
std::fs::write(
layout.trust_policy_container_slot_path(crate::layout::ContainerSlot::A),
b"not a valid trust policy container at all",
)?;
let mut store = FileObjectStore::new(layout.clone());
let state_merkle_root = derive_next_state_root(&store, None, &[])?;
write_signed_block(
&mut store,
&BlockPayload {
parent_block_ids: Vec::new(),
kind: BlockKind::Root,
patch_ids: Vec::new(),
state_merkle_root,
snapshot_blob_ref: None,
mainline_parent_id: None,
merge_baseline_block_id: None,
},
)?;
let report = verify_repository(&layout)?;
assert!(report.has_publication_trust_issues());
assert!(
report
.publication_trust_issues
.iter()
.any(|issue| issue.code == "PRIKK-TRUST-POLICY-INVALID"),
"expected a malformed-policy PRIKK-TRUST-POLICY-INVALID issue, got: {:?}",
report.publication_trust_issues
);
let _ = std::fs::remove_dir_all(malformed_root);
Ok(())
}