use crate::artifact::{
verify_canonical_artifact_envelope, ArtifactHash, ArtifactVerificationReport,
CanonicalArtifactEnvelope, SignatureRef,
};
use crate::evidence::{content_hash, sort_findings, sorted_findings};
use serde::{Deserialize, Serialize};
pub const REGISTRY_ROW_BODY_SCHEMA_VERSION: u32 = 1;
pub const REGISTRY_DRIFT_REPORT_SCHEMA_VERSION: u32 = 1;
pub const REGISTRY_VERIFICATION_REPORT_SCHEMA_VERSION: u32 = 1;
pub const REGISTRY_LIFECYCLE_ANNOUNCED: u32 = 0;
pub const REGISTRY_LIFECYCLE_LIVE: u32 = 1;
pub const REGISTRY_LIFECYCLE_DEPRECATED: u32 = 2;
pub const REGISTRY_LIFECYCLE_REMOVED: u32 = 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct RegistryRowId(pub ArtifactHash);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct NamedDigest {
pub name: String,
pub digest: ArtifactHash,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegistryRowBody {
pub schema_version: u32,
pub row_id: RegistryRowId,
pub row_kind: u64,
pub row_layout_version: u32,
pub opaque_payload: Vec<u8>,
pub named_digests: Vec<NamedDigest>,
pub lifecycle: u32,
pub supersedes: Option<RegistryRowId>,
}
#[must_use]
pub fn normalize_registry_row_body(body: &RegistryRowBody) -> RegistryRowBody {
let mut named_digests = body.named_digests.clone();
named_digests.sort();
RegistryRowBody {
named_digests,
..body.clone()
}
}
pub fn registry_row_body_bytes(
body: &RegistryRowBody,
) -> Result<Vec<u8>, rmp_serde::encode::Error> {
let normalized = normalize_registry_row_body(body);
crate::encoding::to_bytes(&normalized)
}
pub fn registry_row_body_hash(
body: &RegistryRowBody,
) -> Result<ArtifactHash, rmp_serde::encode::Error> {
let bytes = registry_row_body_bytes(body)?;
Ok(content_hash(&bytes))
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum RegistryDriftFinding {
MissingRow {
row_id: RegistryRowId,
},
ExtraRow {
row_id: RegistryRowId,
},
HashMismatch {
row_id: RegistryRowId,
expected: ArtifactHash,
observed: ArtifactHash,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegistryDriftReportBody {
pub schema_version: u32,
pub expected: Vec<(RegistryRowId, ArtifactHash)>,
pub observed: Vec<(RegistryRowId, ArtifactHash)>,
pub findings: Vec<RegistryDriftFinding>,
}
pub fn sort_registry_row_hash_pairs(pairs: &mut [(RegistryRowId, ArtifactHash)]) {
pairs.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
}
#[must_use]
pub fn registry_drift_findings_sorted(
expected: &[(RegistryRowId, ArtifactHash)],
observed: &[(RegistryRowId, ArtifactHash)],
) -> Vec<RegistryDriftFinding> {
let mut i = 0usize;
let mut j = 0usize;
let mut out = Vec::new();
while i < expected.len() && j < observed.len() {
match expected[i].0.cmp(&observed[j].0) {
std::cmp::Ordering::Less => {
out.push(RegistryDriftFinding::MissingRow {
row_id: expected[i].0,
});
i += 1;
}
std::cmp::Ordering::Greater => {
out.push(RegistryDriftFinding::ExtraRow {
row_id: observed[j].0,
});
j += 1;
}
std::cmp::Ordering::Equal => {
if expected[i].1 != observed[j].1 {
out.push(RegistryDriftFinding::HashMismatch {
row_id: expected[i].0,
expected: expected[i].1,
observed: observed[j].1,
});
}
i += 1;
j += 1;
}
}
}
while i < expected.len() {
out.push(RegistryDriftFinding::MissingRow {
row_id: expected[i].0,
});
i += 1;
}
while j < observed.len() {
out.push(RegistryDriftFinding::ExtraRow {
row_id: observed[j].0,
});
j += 1;
}
sort_findings(&mut out);
out
}
pub fn registry_drift_report_body_hash(
report: &RegistryDriftReportBody,
) -> Result<ArtifactHash, rmp_serde::encode::Error> {
let findings = sorted_findings(&report.findings);
let mut expected = report.expected.clone();
let mut observed = report.observed.clone();
sort_registry_row_hash_pairs(&mut expected);
sort_registry_row_hash_pairs(&mut observed);
let normalized = RegistryDriftReportBody {
expected,
observed,
findings,
..report.clone()
};
let bytes = crate::encoding::to_bytes(&normalized)?;
Ok(content_hash(&bytes))
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum RegistryVerificationFinding {
UnsupportedRowSchemaVersion {
row_id: RegistryRowId,
observed: u32,
expected: u32,
},
InvalidLifecycle {
row_id: RegistryRowId,
lifecycle: u32,
},
RowHashMismatch {
row_id: RegistryRowId,
claimed: ArtifactHash,
computed: ArtifactHash,
},
RowIdMismatch {
body_row_id: RegistryRowId,
claimed_row_id: RegistryRowId,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegistryVerificationReport {
pub schema_version: u32,
pub envelope_plane: ArtifactVerificationReport,
pub findings: Vec<RegistryVerificationFinding>,
}
pub fn registry_verification_report_body_hash(
report: &RegistryVerificationReport,
) -> Result<ArtifactHash, rmp_serde::encode::Error> {
let findings = sorted_findings(&report.findings);
let mut envelope_plane = report.envelope_plane.clone();
sort_findings(&mut envelope_plane.findings);
let normalized = RegistryVerificationReport {
envelope_plane,
findings,
..report.clone()
};
let bytes = crate::encoding::to_bytes(&normalized)?;
Ok(content_hash(&bytes))
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum RegistrySupersessionFinding {
DanglingSupersedes {
from: RegistryRowId,
target: RegistryRowId,
},
RemovedDeclaresSupersedes {
from: RegistryRowId,
},
DuplicateRowId {
row_id: RegistryRowId,
},
SupersedesCycle {
edge_from: RegistryRowId,
edge_to: RegistryRowId,
},
}
#[must_use]
pub fn registry_supersession_findings_sorted(
catalog: &[(RegistryRowId, RegistryRowBody)],
) -> Vec<RegistrySupersessionFinding> {
let mut out = Vec::new();
for w in catalog.windows(2) {
if w[0].0 == w[1].0 {
out.push(RegistrySupersessionFinding::DuplicateRowId { row_id: w[1].0 });
}
}
let id_set: std::collections::BTreeSet<RegistryRowId> =
catalog.iter().map(|(id, _)| *id).collect();
let by_id: std::collections::BTreeMap<RegistryRowId, &RegistryRowBody> =
catalog.iter().map(|(id, body)| (*id, body)).collect();
for (id, body) in catalog {
if let Some(target) = body.supersedes {
if !id_set.contains(&target) {
out.push(RegistrySupersessionFinding::DanglingSupersedes { from: *id, target });
}
}
if body.lifecycle == REGISTRY_LIFECYCLE_REMOVED && body.supersedes.is_some() {
out.push(RegistrySupersessionFinding::RemovedDeclaresSupersedes { from: *id });
}
}
let mut cycle_edges: std::collections::BTreeSet<(RegistryRowId, RegistryRowId)> =
std::collections::BTreeSet::new();
for &(start, _) in catalog {
let mut path: Vec<RegistryRowId> = Vec::new();
supersession_walk_for_cycles(&by_id, start, &mut path, &mut cycle_edges);
}
for edge in cycle_edges {
out.push(RegistrySupersessionFinding::SupersedesCycle {
edge_from: edge.0,
edge_to: edge.1,
});
}
out.sort();
out
}
fn supersession_walk_for_cycles(
by_id: &std::collections::BTreeMap<RegistryRowId, &RegistryRowBody>,
cur: RegistryRowId,
path: &mut Vec<RegistryRowId>,
cycle_edges: &mut std::collections::BTreeSet<(RegistryRowId, RegistryRowId)>,
) {
if path.contains(&cur) {
if let Some(&prev) = path.last() {
cycle_edges.insert((prev, cur));
}
return;
}
path.push(cur);
if let Some(body) = by_id.get(&cur) {
if let Some(next) = body.supersedes {
if by_id.contains_key(&next) {
supersession_walk_for_cycles(by_id, next, path, cycle_edges);
}
}
}
path.pop();
}
pub fn verify_registry_attested_row<F>(
envelope: &CanonicalArtifactEnvelope<RegistryRowBody>,
claimed_row_id: RegistryRowId,
claimed_row_hash: ArtifactHash,
verify_signature: F,
) -> Result<RegistryVerificationReport, rmp_serde::encode::Error>
where
F: FnMut(&SignatureRef, &[u8]) -> Result<(), String>,
{
let normalized_body = normalize_registry_row_body(&envelope.body);
let envelope_norm = CanonicalArtifactEnvelope {
body: normalized_body,
envelope_schema_version: envelope.envelope_schema_version,
generated_at_wall_ms: envelope.generated_at_wall_ms,
diagnostic_note: envelope.diagnostic_note.clone(),
signatures: envelope.signatures.clone(),
attestations: envelope.attestations.clone(),
};
let envelope_plane = verify_canonical_artifact_envelope(&envelope_norm, verify_signature)?;
let mut findings = Vec::new();
let body = &envelope_norm.body;
if body.schema_version != REGISTRY_ROW_BODY_SCHEMA_VERSION {
findings.push(RegistryVerificationFinding::UnsupportedRowSchemaVersion {
row_id: body.row_id,
observed: body.schema_version,
expected: REGISTRY_ROW_BODY_SCHEMA_VERSION,
});
}
if body.row_id != claimed_row_id {
findings.push(RegistryVerificationFinding::RowIdMismatch {
body_row_id: body.row_id,
claimed_row_id,
});
}
let computed = registry_row_body_hash(body)?;
if computed != claimed_row_hash {
findings.push(RegistryVerificationFinding::RowHashMismatch {
row_id: body.row_id,
claimed: claimed_row_hash,
computed,
});
}
if body.lifecycle != REGISTRY_LIFECYCLE_ANNOUNCED
&& body.lifecycle != REGISTRY_LIFECYCLE_LIVE
&& body.lifecycle != REGISTRY_LIFECYCLE_DEPRECATED
&& body.lifecycle != REGISTRY_LIFECYCLE_REMOVED
{
findings.push(RegistryVerificationFinding::InvalidLifecycle {
row_id: body.row_id,
lifecycle: body.lifecycle,
});
}
sort_findings(&mut findings);
Ok(RegistryVerificationReport {
schema_version: REGISTRY_VERIFICATION_REPORT_SCHEMA_VERSION,
envelope_plane,
findings,
})
}
pub fn registry_row_signing_bytes(
body: &RegistryRowBody,
) -> Result<Vec<u8>, rmp_serde::encode::Error> {
registry_row_body_bytes(body)
}
pub fn verify_registry_row_signatures_only<F>(
envelope: &CanonicalArtifactEnvelope<RegistryRowBody>,
verify_signature: F,
) -> Result<ArtifactVerificationReport, rmp_serde::encode::Error>
where
F: FnMut(&SignatureRef, &[u8]) -> Result<(), String>,
{
let normalized_body = normalize_registry_row_body(&envelope.body);
let envelope_norm = CanonicalArtifactEnvelope {
body: normalized_body,
envelope_schema_version: envelope.envelope_schema_version,
generated_at_wall_ms: envelope.generated_at_wall_ms,
diagnostic_note: envelope.diagnostic_note.clone(),
signatures: envelope.signatures.clone(),
attestations: envelope.attestations.clone(),
};
verify_canonical_artifact_envelope(&envelope_norm, verify_signature)
}
pub fn registry_row_body_hash_matches_signing_bytes(
body: &RegistryRowBody,
) -> Result<bool, rmp_serde::encode::Error> {
let n = normalize_registry_row_body(body);
let a = crate::artifact::artifact_body_bytes(&n)?;
let b = registry_row_body_bytes(body)?;
Ok(a == b)
}