use core::fmt;
use crate::release_quarantine::{
CANONICAL_INPUT_LIMIT_BYTES as A_CANONICAL_INPUT_LIMIT_BYTES, MutationSink, ORDERED_CONTEXTS,
ORDERED_SINKS, ProviderObservation, QuarantineContext, QuarantineWorkflowInventory,
Sha256Digest, SinkReachability, WorkflowIdentity, canonical_inventory_bytes,
event_is_ambient_publish_trigger, permission_is_mutation_capable,
process_is_publication_capable, quarantine_workflow_inventory, sha256_bounded,
};
pub const CANONICAL_DIGEST_DOMAIN: &[u8] = b"fastmcp-rel-quar-00-b-v1\0";
pub const CANONICAL_INPUT_LIMIT_BYTES: usize = 65_536;
pub const DIAGNOSTIC_SLUG: &str = "mutation-reachability-denial";
pub const RESTORATION_RECEIPT_IDENTIFIER: &str = "REL-02";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rel02ReceiptState {
Absent,
Present,
}
impl Rel02ReceiptState {
#[must_use]
pub const fn tag(self) -> &'static str {
match self {
Self::Absent => "rel-02-absent",
Self::Present => "rel-02-present",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefInputProvenance {
ProviderSupplied,
NotApplicable,
}
impl RefInputProvenance {
#[must_use]
pub const fn tag(self) -> &'static str {
match self {
Self::ProviderSupplied => "provider-supplied",
Self::NotApplicable => "not-applicable",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReachabilityDenialCell {
pub context: QuarantineContext,
pub sink: MutationSink,
pub result: SinkReachability,
pub workflow_identity: &'static str,
pub action_identities: usize,
pub ref_input_provenance: RefInputProvenance,
pub permission_set: usize,
pub secret_set: usize,
pub process_set: usize,
pub state_digest: Sha256Digest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ZeroAuthorityCounters {
pub ambient_publish_triggers: usize,
pub mutation_permissions: usize,
pub secret_access: usize,
pub publication_processes: usize,
pub registry_requests: usize,
pub release_tag_asset_mutations: usize,
pub provider_grants: usize,
}
impl ZeroAuthorityCounters {
#[must_use]
pub const fn first_non_zero(&self) -> Option<&'static str> {
if self.ambient_publish_triggers != 0 {
return Some("ambient_publish_triggers");
}
if self.mutation_permissions != 0 {
return Some("mutation_permissions");
}
if self.secret_access != 0 {
return Some("secret_access");
}
if self.publication_processes != 0 {
return Some("publication_processes");
}
if self.registry_requests != 0 {
return Some("registry_requests");
}
if self.release_tag_asset_mutations != 0 {
return Some("release_tag_asset_mutations");
}
if self.provider_grants != 0 {
return Some("provider_grants");
}
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MutationReachabilityRecord {
pub a_digest: Sha256Digest,
pub cells: Vec<ReachabilityDenialCell>,
pub rel_02_receipt: Rel02ReceiptState,
pub provider_disabled: ProviderObservation,
pub unresolved_historical_runs: usize,
pub counters: ZeroAuthorityCounters,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReachabilityDiagnostic {
pub code: &'static str,
pub field: String,
}
impl fmt::Display for ReachabilityDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"RELQUAR00B|Error|{}|{}|{}",
self.code, DIAGNOSTIC_SLUG, self.field
)
}
}
impl core::error::Error for ReachabilityDiagnostic {}
fn reject(code: &'static str, field: impl Into<String>) -> ReachabilityDiagnostic {
ReachabilityDiagnostic {
code,
field: field.into(),
}
}
fn put_bytes(buffer: &mut Vec<u8>, bytes: &[u8]) {
buffer.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
buffer.extend_from_slice(bytes);
}
fn put_str(buffer: &mut Vec<u8>, value: &str) {
put_bytes(buffer, value.as_bytes());
}
fn put_usize(buffer: &mut Vec<u8>, value: usize) {
put_bytes(buffer, &(value as u64).to_be_bytes());
}
#[must_use]
pub fn canonical_cell_bytes(cell: &ReachabilityDenialCell) -> Vec<u8> {
let mut buffer = Vec::with_capacity(256);
put_str(&mut buffer, cell.context.tag());
put_str(&mut buffer, cell.sink.tag());
put_str(&mut buffer, cell.result.tag());
put_str(&mut buffer, cell.workflow_identity);
put_usize(&mut buffer, cell.action_identities);
put_str(&mut buffer, cell.ref_input_provenance.tag());
put_usize(&mut buffer, cell.permission_set);
put_usize(&mut buffer, cell.secret_set);
put_usize(&mut buffer, cell.process_set);
buffer
}
#[must_use]
pub fn canonical_reachability_bytes(record: &MutationReachabilityRecord) -> Vec<u8> {
let mut buffer = Vec::with_capacity(16_384);
buffer.extend_from_slice(CANONICAL_DIGEST_DOMAIN);
put_bytes(&mut buffer, record.a_digest.as_bytes());
put_usize(&mut buffer, record.cells.len());
for cell in &record.cells {
let cell_bytes = canonical_cell_bytes(cell);
put_bytes(&mut buffer, &cell_bytes);
put_bytes(&mut buffer, cell.state_digest.as_bytes());
}
put_str(&mut buffer, record.rel_02_receipt.tag());
put_str(&mut buffer, record.provider_disabled.tag());
put_usize(&mut buffer, record.unresolved_historical_runs);
put_usize(&mut buffer, record.counters.ambient_publish_triggers);
put_usize(&mut buffer, record.counters.mutation_permissions);
put_usize(&mut buffer, record.counters.secret_access);
put_usize(&mut buffer, record.counters.publication_processes);
put_usize(&mut buffer, record.counters.registry_requests);
put_usize(&mut buffer, record.counters.release_tag_asset_mutations);
put_usize(&mut buffer, record.counters.provider_grants);
buffer
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReachabilityDenialReceipt {
pub denial_cells: usize,
pub rel_02_receipt: Rel02ReceiptState,
pub unresolved_historical_runs: usize,
pub canonical_digest: Sha256Digest,
}
fn digest_of(bytes: &[u8], field: &'static str) -> Result<Sha256Digest, ReachabilityDiagnostic> {
sha256_bounded(bytes, CANONICAL_INPUT_LIMIT_BYTES)
.map_err(|_| reject("E_CANONICAL_INPUT", field))
}
#[must_use]
pub fn process_is_registry_request(process: &str) -> bool {
process.contains("cargo publish")
|| process.contains("crates.io")
|| process.contains("registry")
}
#[must_use]
pub fn process_is_release_tag_or_asset_mutation(process: &str) -> bool {
process.contains("gh release")
|| process.contains("git tag")
|| process.contains("git push --tags")
|| process.contains("upload-release-asset")
|| process.contains("softprops/action-gh-release")
}
#[must_use]
pub fn permission_is_provider_grant(permission: &str) -> bool {
!permission.ends_with(": read")
&& !permission.ends_with(":read")
&& !permission.ends_with("none")
}
fn counters_for(identity: &WorkflowIdentity) -> ZeroAuthorityCounters {
ZeroAuthorityCounters {
ambient_publish_triggers: identity
.events
.iter()
.filter(|event| event_is_ambient_publish_trigger(event))
.count(),
mutation_permissions: identity
.declared_permissions
.iter()
.filter(|permission| permission_is_mutation_capable(permission))
.count(),
secret_access: identity.secret_references.len(),
publication_processes: identity
.process_invocations
.iter()
.filter(|process| process_is_publication_capable(process))
.count(),
registry_requests: identity
.process_invocations
.iter()
.filter(|process| process_is_registry_request(process))
.count(),
release_tag_asset_mutations: identity
.process_invocations
.iter()
.filter(|process| process_is_release_tag_or_asset_mutation(process))
.count(),
provider_grants: identity
.declared_permissions
.iter()
.filter(|permission| permission_is_provider_grant(permission))
.count(),
}
}
const fn provenance_for(context: QuarantineContext) -> RefInputProvenance {
match context {
QuarantineContext::TokenPresent | QuarantineContext::TokenAbsent => {
RefInputProvenance::NotApplicable
}
_ => RefInputProvenance::ProviderSupplied,
}
}
pub fn quarantine_mutation_reachability(
inventory: &QuarantineWorkflowInventory,
) -> Result<MutationReachabilityRecord, ReachabilityDiagnostic> {
if inventory.ordered_contexts != ORDERED_CONTEXTS {
return Err(reject("E_CONTEXT_SET", "ordered_contexts"));
}
if inventory.ordered_sinks != ORDERED_SINKS {
return Err(reject("E_SINK_SET", "ordered_sinks"));
}
let expected = ORDERED_CONTEXTS.len() * ORDERED_SINKS.len();
if inventory.reachability_cells.len() != expected {
return Err(reject("E_CONTEXT_SET", "reachability_cells.len"));
}
let identity = &inventory.quarantine;
let a_bytes = canonical_inventory_bytes(inventory);
let a_digest = sha256_bounded(&a_bytes, A_CANONICAL_INPUT_LIMIT_BYTES)
.map_err(|_| reject("E_CANONICAL_INPUT", "canonical_inventory_bytes"))?;
let mut cells = Vec::with_capacity(expected);
for (index, source) in inventory.reachability_cells.iter().enumerate() {
let context = ORDERED_CONTEXTS[index / ORDERED_SINKS.len()];
let sink = ORDERED_SINKS[index % ORDERED_SINKS.len()];
if source.context != context || source.sink != sink {
return Err(reject(
"E_CONTEXT_SET",
format!("cell[context={},sink={}].order", context.tag(), sink.tag()),
));
}
let mut cell = ReachabilityDenialCell {
context,
sink,
result: source.result,
workflow_identity: identity.role.tag(),
action_identities: identity.actions.len(),
ref_input_provenance: provenance_for(context),
permission_set: identity.declared_permissions.len(),
secret_set: identity.secret_references.len(),
process_set: identity.process_invocations.len(),
state_digest: a_digest,
};
let cell_bytes = canonical_cell_bytes(&cell);
cell.state_digest = digest_of(&cell_bytes, "cell.state_digest")?;
cells.push(cell);
}
Ok(MutationReachabilityRecord {
a_digest,
cells,
rel_02_receipt: Rel02ReceiptState::Absent,
provider_disabled: identity.provider_disablement,
unresolved_historical_runs: inventory.unresolved_provider_observations.len(),
counters: counters_for(identity),
})
}
pub fn frozen_mutation_reachability() -> Result<MutationReachabilityRecord, ReachabilityDiagnostic>
{
quarantine_mutation_reachability(&quarantine_workflow_inventory())
}
pub fn rel_quar_00_b_reachability_denial(
record: &MutationReachabilityRecord,
) -> Result<ReachabilityDenialReceipt, ReachabilityDiagnostic> {
let expected = ORDERED_CONTEXTS.len() * ORDERED_SINKS.len();
if record.cells.len() != expected {
return Err(reject("E_CELL_COUNT", "cells.len"));
}
for (index, cell) in record.cells.iter().enumerate() {
let context = ORDERED_CONTEXTS[index / ORDERED_SINKS.len()];
let sink = ORDERED_SINKS[index % ORDERED_SINKS.len()];
let field = format!("cell[context={},sink={}]", context.tag(), sink.tag());
if cell.context != context || cell.sink != sink {
return Err(reject("E_CELL_ORDER", field));
}
if cell.result != SinkReachability::ExternallyInert {
return Err(reject("E_MUTATION_REACHABLE", field));
}
if cell.ref_input_provenance != provenance_for(context) {
return Err(reject("E_REF_PROVENANCE", field));
}
let recomputed = digest_of(&canonical_cell_bytes(cell), "cell.state_digest")?;
if recomputed != cell.state_digest {
return Err(reject("E_CELL_DIGEST", field));
}
}
if let Some(counter) = record.counters.first_non_zero() {
return Err(reject("E_NONZERO_AUTHORITY", counter));
}
if record.rel_02_receipt != Rel02ReceiptState::Absent {
return Err(reject("E_RESTORATION_BOUNDARY", "rel_02_receipt"));
}
if record.provider_disabled == ProviderObservation::VerifiedBySeparateAuthority {
return Err(reject("E_PROVIDER_INFERENCE", "provider_disabled"));
}
let canonical_digest = digest_of(
&canonical_reachability_bytes(record),
"canonical_reachability_bytes",
)?;
Ok(ReachabilityDenialReceipt {
denial_cells: record.cells.len(),
rel_02_receipt: record.rel_02_receipt,
unresolved_historical_runs: record.unresolved_historical_runs,
canonical_digest,
})
}