use std::collections::{BTreeMap, BTreeSet};
use prikk_error::{PrikkError, Result};
use prikk_object::{ObjectEnvelope, ObjectId, ObjectType, RecognitionClaimPayload, SignerRole};
use crate::author_key_index::{
check_author_key_conflict, lookup_author_key_entries, record_author_key_material,
verify_author_signature_against_material,
};
use crate::layout::RepositoryLayout;
use crate::lock::ActiveLock;
use crate::object_store::{ObjectReadSnapshot, ObjectWriteSession, ObjectWriter};
use crate::patch_replay::decode::{
DecodedDeletePreimage, DecodedOperationKind, decode_patch_operations, decode_patch_parent_ids,
};
use crate::patch_set_digest::compute_patch_set_digest;
use crate::recognition_claim::{
check_recognition_claim_consistency, maintainer_trust_policy_or_empty, verify_claim_signature,
};
use crate::tag_travel::verify_tag_signature;
use crate::verify::AuthorSignatureVerification;
pub use crate::recognition_claim::ClaimSignatureVerification;
pub use crate::tag_travel::TagSignatureVerification;
use super::artifact::{
DEFAULT_EXCHANGE_ARTIFACT_MAX_OBJECT_COUNT, DEFAULT_EXCHANGE_ARTIFACT_MAX_TOTAL_BYTES,
decode_exchange_artifact,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AcceptOptions {
pub max_object_count: usize,
pub max_total_bytes: usize,
}
impl AcceptOptions {
#[must_use]
pub const fn default_limits() -> Self {
Self {
max_object_count: DEFAULT_EXCHANGE_ARTIFACT_MAX_OBJECT_COUNT,
max_total_bytes: DEFAULT_EXCHANGE_ARTIFACT_MAX_TOTAL_BYTES,
}
}
#[must_use]
pub const fn with_max_object_count(mut self, max_object_count: usize) -> Self {
self.max_object_count = max_object_count;
self
}
#[must_use]
pub const fn with_max_total_bytes(mut self, max_total_bytes: usize) -> Self {
self.max_total_bytes = max_total_bytes;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AcceptReport {
pub patch_count: usize,
pub blob_count: usize,
pub claim_count: usize,
pub tag_count: usize,
pub written_object_count: usize,
pub recorded_author_key_count: usize,
pub author_signature_outcomes: Vec<(ObjectId, AuthorSignatureVerification)>,
pub claim_signature_outcomes: Vec<(ObjectId, ClaimSignatureVerification)>,
pub tag_signature_outcomes: Vec<(ObjectId, TagSignatureVerification)>,
}
pub fn accept_exchange_artifact(
layout: &RepositoryLayout,
bytes: &[u8],
options: &AcceptOptions,
) -> Result<AcceptReport> {
if bytes.len() > options.max_total_bytes {
return Err(PrikkError::MalformedData(format!(
"patch-exchange artifact is {} bytes, over the configured limit of {} bytes",
bytes.len(),
options.max_total_bytes
)));
}
let decoded = decode_exchange_artifact(bytes, options.max_object_count)?;
let mut decoded_patch_ids: Vec<ObjectId> = decoded
.patches
.iter()
.map(ObjectEnvelope::object_id)
.collect();
decoded_patch_ids.sort_unstable();
decoded_patch_ids.dedup();
let recomputed_digest = compute_patch_set_digest(&decoded_patch_ids)?;
if recomputed_digest != decoded.declared_digest {
return Err(PrikkError::Integrity(
"patch-exchange artifact's declared patch-set digest does not match its own decoded \
patches -- refusing before any signature work"
.to_string(),
));
}
let mut artifact_key_ids: BTreeMap<&str, [u8; 32]> = BTreeMap::new();
for entry in &decoded.author_keys {
match artifact_key_ids.get(entry.key_id.as_str()) {
Some(existing) if *existing != entry.public_key => {
return Err(PrikkError::MalformedData(format!(
"patch-exchange artifact's author-key section carries two different public \
keys for key_id {} -- refusing the whole exchange",
entry.key_id
)));
}
Some(_) => {}
None => {
artifact_key_ids.insert(&entry.key_id, entry.public_key);
}
}
}
for (&key_id, &public_key) in &artifact_key_ids {
check_author_key_conflict(layout, key_id, public_key)?;
}
let read_snapshot = ObjectReadSnapshot::open(layout)?;
let artifact_blob_ids: BTreeSet<ObjectId> = decoded
.blobs
.iter()
.map(ObjectEnvelope::object_id)
.collect();
for envelope in &decoded.patches {
let parent_patch_ids = decode_patch_parent_ids(&envelope.canonical_payload)?;
if !parent_patch_ids.is_empty() {
return Err(PrikkError::Integrity(format!(
"patch {} carries a non-empty parent_patch_ids -- this field is always empty \
today and there is nothing defined to walk there yet; refusing rather than \
silently ignoring it",
envelope.object_id()
)));
}
for operation in
decode_patch_operations(&envelope.canonical_payload, envelope.schema_version)?
{
for blob_id in referenced_blob_ids(&operation.kind) {
if !artifact_blob_ids.contains(&blob_id)
&& !read_snapshot.contains_object(ObjectType::Blob, blob_id)
{
return Err(PrikkError::Integrity(format!(
"patch {} references blob {blob_id}, which is neither carried by this \
artifact nor already present in this repository -- refusing the whole \
exchange, no partial apply",
envelope.object_id()
)));
}
}
}
}
let mut author_signature_outcomes = Vec::with_capacity(decoded.patches.len());
for envelope in &decoded.patches {
let Some(signature) = envelope
.signatures
.iter()
.find(|signature| signature.signer_role == SignerRole::Author)
else {
continue;
};
let mut candidates = lookup_author_key_entries(layout, &signature.key_id)?;
candidates.extend(
decoded
.author_keys
.iter()
.filter(|entry| entry.key_id == signature.key_id)
.cloned(),
);
let Some((key_id, verifies)) =
verify_author_signature_against_material(envelope, &candidates)?
else {
continue;
};
let outcome = if verifies {
AuthorSignatureVerification::Sound { key_id }
} else {
AuthorSignatureVerification::Unverifiable { key_id }
};
author_signature_outcomes.push((envelope.object_id(), outcome));
}
let trust_policy = maintainer_trust_policy_or_empty(layout)?;
let mut claim_signature_outcomes = Vec::with_capacity(decoded.claims.len());
for envelope in &decoded.claims {
let claim_id = envelope.object_id();
let outcome = verify_claim_signature(envelope, &trust_policy)?;
claim_signature_outcomes.push((claim_id, outcome));
let payload = RecognitionClaimPayload::decode_canonical(&envelope.canonical_payload)?;
match check_recognition_claim_consistency(&read_snapshot, &payload)? {
crate::recognition_claim::RecognitionClaimConsistency::Contradicted { .. } => {
return Err(PrikkError::Integrity(format!(
"recognition claim {claim_id} contradicts a block this repository already \
holds -- refusing the whole exchange"
)));
}
crate::recognition_claim::RecognitionClaimConsistency::Consistent
| crate::recognition_claim::RecognitionClaimConsistency::BlockAbsent => {}
}
}
let mut tag_signature_outcomes = Vec::with_capacity(decoded.tags.len());
for envelope in &decoded.tags {
let tag_id = envelope.object_id();
let outcome = verify_tag_signature(envelope, &trust_policy)?;
tag_signature_outcomes.push((tag_id, outcome));
}
let mut object_store = ObjectWriteSession::open(layout)?;
let mut written_object_count = 0_usize;
for envelope in decoded.patches.iter().chain(decoded.blobs.iter()) {
let id = envelope.object_id();
if !object_store.contains_object(envelope.object_type, id)? {
written_object_count = written_object_count.checked_add(1).ok_or_else(|| {
PrikkError::Integrity("exchange accept written-object count overflow".to_string())
})?;
}
object_store.write_object(envelope)?;
}
let mut recorded_author_key_count = 0_usize;
{
let active_lock = ActiveLock::acquire(layout)?;
for (&key_id, &public_key) in &artifact_key_ids {
check_author_key_conflict(layout, key_id, public_key)?;
}
for entry in &decoded.author_keys {
record_author_key_material(layout, &entry.key_id, entry.public_key, &active_lock)?;
recorded_author_key_count =
recorded_author_key_count.checked_add(1).ok_or_else(|| {
PrikkError::Integrity(
"exchange accept recorded-author-key count overflow".to_string(),
)
})?;
}
}
for envelope in decoded.claims.iter().chain(decoded.tags.iter()) {
let id = envelope.object_id();
if !object_store.contains_object(envelope.object_type, id)? {
written_object_count = written_object_count.checked_add(1).ok_or_else(|| {
PrikkError::Integrity("exchange accept written-object count overflow".to_string())
})?;
}
object_store.write_object(envelope)?;
}
Ok(AcceptReport {
patch_count: decoded.patches.len(),
blob_count: decoded.blobs.len(),
claim_count: decoded.claims.len(),
tag_count: decoded.tags.len(),
written_object_count,
recorded_author_key_count,
author_signature_outcomes,
claim_signature_outcomes,
tag_signature_outcomes,
})
}
fn referenced_blob_ids(kind: &DecodedOperationKind) -> Vec<ObjectId> {
match kind {
DecodedOperationKind::CreateFile { blob_id, .. } => vec![*blob_id],
DecodedOperationKind::ReplaceBinary {
old_blob_id,
new_blob_id,
..
} => vec![*old_blob_id, *new_blob_id],
DecodedOperationKind::DeleteNode {
preimage: DecodedDeletePreimage::File { old_blob_id, .. },
..
} => vec![*old_blob_id],
_ => Vec::new(),
}
}
#[cfg(test)]
mod tests;