use std::collections::BTreeSet;
use prikk_error::{PrikkError, Result};
use prikk_object::{
CanonicalEncode, ObjectEnvelope, ObjectId, ObjectType, RefKind, RefStatePayload,
RefUpdatePayload, Signature, SignatureAlgorithm, SignerRole, TagPayload,
};
use crate::container::decode_container_records;
use crate::fsutil::read_file_if_exists;
use crate::layout::{ContainerSlot, RepositoryLayout, persisted_object_types};
use crate::maintainer_signing::{MaintainerSigner, maintainer_signature};
use crate::object_store::{ObjectReadSnapshot, ObjectReader, ObjectWriteSession, ObjectWriter};
use crate::patch_set_digest::{PatchSetDigest, PatchSetResolution, resolve_patch_set_digest};
use crate::refs::{RefPublication, RefStore, validate_local_tag_ref};
use crate::trust::{MaintainerTrustPolicy, verify_signer_trusted};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TagSignatureVerification {
Sound {
key_id: String,
},
Unverifiable {
key_id: String,
},
}
pub(crate) fn verify_tag_signature(
envelope: &ObjectEnvelope,
trust_policy: &MaintainerTrustPolicy,
) -> Result<TagSignatureVerification> {
let tag_id = envelope.object_id();
let Some(signature) = envelope
.signatures
.iter()
.find(|signature| signature.signer_role == SignerRole::Maintainer)
else {
return Err(PrikkError::Integrity(format!(
"tag {tag_id} carries no MAINTAINER signature -- a tag is, by definition, signed by \
its author's maintainer key"
)));
};
if signature.algorithm != SignatureAlgorithm::Ed25519 {
return Err(PrikkError::InvalidSignature(format!(
"tag {tag_id} MAINTAINER signature is not Ed25519"
)));
}
match trust_policy
.keys
.iter()
.find(|adopted| adopted.key_id == signature.key_id)
{
None => Ok(TagSignatureVerification::Unverifiable {
key_id: signature.key_id.clone(),
}),
Some(adopted) => {
let preimage = Signature::signed_bytes(
SignatureAlgorithm::Ed25519,
envelope.object_type,
tag_id,
SignerRole::Maintainer,
&signature.key_id,
)?;
if prikk_crypto::verify_ed25519(
&adopted.public_key,
&preimage,
&signature.signature_bytes,
)
.is_err()
{
return Err(PrikkError::InvalidSignature(format!(
"tag {tag_id} MAINTAINER signature does not verify against adopted key {}",
signature.key_id
)));
}
Ok(TagSignatureVerification::Sound {
key_id: signature.key_id.clone(),
})
}
}
}
pub fn received_tag_ids(layout: &RepositoryLayout) -> Result<Vec<ObjectId>> {
debug_assert!(
persisted_object_types().contains(&ObjectType::Tag),
"Tag must remain a persisted, containerized object type"
);
let container_path = layout.container_slot_path(ObjectType::Tag, ContainerSlot::A);
let relative = layout.repository_relative(&container_path)?;
let mut all_tag_ids: BTreeSet<ObjectId> = BTreeSet::new();
if let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)? {
let replay = decode_container_records(ObjectType::Tag, &bytes)?;
for record in replay.records {
all_tag_ids.insert(record.envelope.object_id());
}
}
let object_store = ObjectReadSnapshot::open(layout)?;
let ref_store = RefStore::new(layout.clone());
let mut locally_targeted: BTreeSet<ObjectId> = BTreeSet::new();
for pointer in ref_store.list_ref_pointers()? {
let ref_state_envelope = object_store
.read_typed(pointer.ref_state_id, ObjectType::RefState)?
.ok_or_else(|| {
PrikkError::Integrity(format!(
"ref {} names missing RefState {}",
pointer.ref_name, pointer.ref_state_id
))
})?;
let ref_state_payload = RefStatePayload::decode_canonical(
&ref_state_envelope.canonical_payload,
ref_state_envelope.schema_version,
)?;
if ref_state_payload.kind == RefKind::Tag {
locally_targeted.insert(ref_state_payload.target_object_id);
}
}
Ok(all_tag_ids.difference(&locally_targeted).copied().collect())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceivedTagSummary {
pub tag_id: ObjectId,
pub name: String,
pub signature_outcome: TagSignatureVerification,
pub resolution: ReceivedTagResolution,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReceivedTagResolution {
Resolved(ObjectId),
NotHeld,
Ambiguous {
detail: String,
},
}
pub fn list_received_tags(layout: &RepositoryLayout) -> Result<Vec<ReceivedTagSummary>> {
let object_store = ObjectReadSnapshot::open(layout)?;
let trust_policy = crate::recognition_claim::maintainer_trust_policy_or_empty(layout)?;
let mut summaries = Vec::new();
for tag_id in received_tag_ids(layout)? {
let envelope = object_store
.read_typed(tag_id, ObjectType::Tag)?
.ok_or_else(|| PrikkError::Integrity(format!("missing Tag object: {tag_id}")))?;
let payload = TagPayload::decode_canonical(&envelope.canonical_payload)?;
let signature_outcome = verify_tag_signature(&envelope, &trust_policy)?;
let resolution =
match resolve_patch_set_digest(layout, payload.patch_set_digest, payload.patch_count) {
Ok(PatchSetResolution::Resolved(block_id)) => {
ReceivedTagResolution::Resolved(block_id)
}
Ok(PatchSetResolution::NotHeld) => ReceivedTagResolution::NotHeld,
Err(err) => ReceivedTagResolution::Ambiguous {
detail: err.to_string(),
},
};
summaries.push(ReceivedTagSummary {
tag_id,
name: payload.name,
signature_outcome,
resolution,
});
}
Ok(summaries)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LocalTagCreation {
pub tag_object_id: ObjectId,
pub ref_state_id: ObjectId,
}
#[allow(clippy::too_many_arguments)]
pub fn create_local_tag(
layout: &RepositoryLayout,
object_store: &mut ObjectWriteSession,
requested_ref_name: &str,
target_block_id: ObjectId,
message: Option<String>,
patch_set_digest: PatchSetDigest,
patch_count: u64,
signer: &impl MaintainerSigner,
) -> Result<LocalTagCreation> {
layout.require_current_format()?;
let canonical = validate_local_tag_ref(requested_ref_name)?;
let ref_store = RefStore::new(layout.clone());
if ref_store.read_current_ref_state_id(&canonical)?.is_some() {
return Err(PrikkError::Integrity(format!(
"tag {canonical} already exists"
)));
}
let tag_payload = TagPayload {
name: canonical.clone(),
target_block_id,
message,
created_at: 0,
author_key_id: signer.key_id().to_string(),
patch_set_digest,
patch_count,
};
let tag_envelope = signed_envelope(
ObjectType::Tag,
1,
tag_payload.to_canonical_bytes()?,
signer,
)?;
let tag_object_id = object_store.write_object(&tag_envelope)?;
let ref_state_payload = RefStatePayload {
ref_name: canonical.clone(),
kind: RefKind::Tag,
target_object_id: tag_object_id,
update_seq: 1,
previous_ref_state_id: None,
required_attestation_ids: Vec::new(),
closed: false,
};
let ref_state_envelope = signed_envelope(
ObjectType::RefState,
1,
ref_state_payload.to_canonical_bytes()?,
signer,
)?;
let ref_state_id = ref_state_envelope.object_id();
let ref_update_payload = RefUpdatePayload {
ref_name: canonical.clone(),
old_ref_state_id: None,
new_ref_state_id: ref_state_id,
new_target_object_id: tag_object_id,
update_seq: 1,
created_at: 0,
author_key_id: signer.key_id().to_string(),
};
let ref_update_envelope = signed_envelope(
ObjectType::RefUpdate,
1,
ref_update_payload.to_canonical_bytes()?,
signer,
)?;
let publication = RefPublication {
ref_name: canonical,
expected_previous_ref_state_id: None,
ref_state: ref_state_envelope,
ref_update: ref_update_envelope,
};
let published_ref_state_id = ref_store.publish_with_object_store(object_store, &publication)?;
Ok(LocalTagCreation {
tag_object_id,
ref_state_id: published_ref_state_id,
})
}
fn signed_envelope(
object_type: ObjectType,
schema_version: u32,
canonical_payload: Vec<u8>,
signer: &impl MaintainerSigner,
) -> Result<ObjectEnvelope> {
let mut envelope = ObjectEnvelope::unsigned(object_type, schema_version, canonical_payload);
let object_id = envelope.object_id();
envelope.add_signature(maintainer_signature(signer, object_type, object_id)?)?;
Ok(envelope)
}
pub fn adopt_tag(
layout: &RepositoryLayout,
requested_name: &str,
signer: &impl MaintainerSigner,
) -> Result<LocalTagCreation> {
let canonical = validate_local_tag_ref(requested_name)?;
let object_store = ObjectReadSnapshot::open(layout)?;
let mut matching: Vec<(ObjectId, TagPayload)> = Vec::new();
for tag_id in received_tag_ids(layout)? {
let envelope = object_store
.read_typed(tag_id, ObjectType::Tag)?
.ok_or_else(|| PrikkError::Integrity(format!("missing Tag object: {tag_id}")))?;
let payload = TagPayload::decode_canonical(&envelope.canonical_payload)?;
if payload.name == canonical {
matching.push((tag_id, payload));
}
}
let (source_tag_id, source_payload) = match matching.len() {
0 => {
return Err(PrikkError::Integrity(format!(
"no received tag named {canonical} -- nothing to adopt"
)));
}
1 => matching.pop().ok_or_else(|| {
PrikkError::Integrity(
"adopt_tag: exactly one match reported but none present -- internal inconsistency"
.to_string(),
)
})?,
_ => {
let ids = matching
.iter()
.map(|(id, _)| id.to_string())
.collect::<Vec<_>>()
.join(", ");
return Err(PrikkError::Integrity(format!(
"{} received tags are named {canonical}, refusing to pick: {ids}",
matching.len()
)));
}
};
let local_block_id = match resolve_patch_set_digest(
layout,
source_payload.patch_set_digest,
source_payload.patch_count,
)? {
PatchSetResolution::NotHeld => {
return Err(PrikkError::Integrity(format!(
"received tag {canonical} ({source_tag_id})'s patch set is not held locally yet -- \
not enough of this repository's history has been synced to adopt it"
)));
}
PatchSetResolution::Resolved(block_id) => block_id,
};
verify_signer_trusted(layout, signer)?;
let mut write_session = ObjectWriteSession::open(layout)?;
create_local_tag(
layout,
&mut write_session,
&canonical,
local_block_id,
source_payload.message,
source_payload.patch_set_digest,
source_payload.patch_count,
signer,
)
}
#[cfg(test)]
mod tests;