use std::path::{Path, PathBuf};
use assay_registry::supply_chain::{
verify_supply_chain, ContainerRef, PinningInput, Policy, ProvenanceInput, SlsaLevel, Subject,
SupplyChainConformance, UnsupportedProvenance, VerifyInput,
};
use assay_registry::trust::TrustStore;
use assay_registry::types::{DsseEnvelope, TrustedKey};
use serde::Deserialize;
use crate::exit_codes::EXIT_CONFIG_ERROR;
pub(super) const INPUT_SCHEMA: &str = "assay.supply_chain_conformance.input.v0";
pub(super) const DSSE_IN_TOTO_PAYLOAD_TYPE: &str = "application/vnd.in-toto+json";
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct InputDescriptor {
schema: String,
subject: SubjectInput,
#[serde(default)]
expected_artifact_digest: Option<String>,
provenance: ProvenanceDescriptor,
pinning: PinningDescriptor,
policy: PolicyDescriptor,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SubjectInput {
name: String,
version: String,
digest: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ProvenanceDescriptor {
kind: String,
#[serde(default)]
format: Option<String>,
#[serde(default)]
payload_type: Option<String>,
#[serde(default)]
envelope_path: Option<String>,
#[serde(default)]
trusted_key_path: Option<String>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct PinningDescriptor {
version_pinned: bool,
#[serde(default)]
digest_pinned: Option<bool>,
#[serde(default)]
lockfile_digest: Option<String>,
#[serde(default)]
floating_source_ref: bool,
#[serde(default)]
container_ref: Option<String>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct PolicyDescriptor {
#[serde(default)]
required_builder_id: Option<String>,
#[serde(default)]
required_slsa_build_level: u8,
#[serde(default)]
require_rekor_inclusion: bool,
#[serde(default)]
require_timestamp_freshness: bool,
#[serde(default)]
require_consistency: bool,
#[serde(default)]
require_witnessing: bool,
}
#[derive(Debug)]
pub(super) struct EmitErr {
pub(super) code: i32,
pub(super) msg: String,
}
fn contract(msg: impl Into<String>) -> EmitErr {
EmitErr {
code: EXIT_CONFIG_ERROR,
msg: format!("[config_error] {}", msg.into()),
}
}
fn resolve_under(base_dir: &Path, rel: &str, field: &str) -> Result<PathBuf, EmitErr> {
if rel.is_empty() {
return Err(contract(format!("provenance.{field} must not be empty")));
}
if rel.contains("://") {
return Err(contract(format!(
"provenance.{field} {rel:?} looks like a URL; only descriptor-relative files are allowed"
)));
}
if rel.chars().any(|c| c.is_control()) {
return Err(contract(format!(
"provenance.{field} contains control characters"
)));
}
let rel_path = Path::new(rel);
if rel_path.is_absolute() {
return Err(contract(format!(
"provenance.{field} {rel:?} must be descriptor-relative, not absolute"
)));
}
if rel_path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err(contract(format!(
"provenance.{field} {rel:?} must not contain \"..\""
)));
}
let base = base_dir
.canonicalize()
.map_err(|e| contract(format!("cannot resolve descriptor directory: {e}")))?;
let full = base
.join(rel_path)
.canonicalize()
.map_err(|e| contract(format!("cannot resolve provenance.{field} {rel:?}: {e}")))?;
if !full.starts_with(&base) {
return Err(contract(format!(
"provenance.{field} {rel:?} escapes the descriptor directory"
)));
}
if !full.is_file() {
return Err(contract(format!(
"provenance.{field} {rel:?} is not a regular file"
)));
}
Ok(full)
}
fn reject_dsse_fields(p: &ProvenanceDescriptor, kind: &str) -> Result<(), EmitErr> {
if p.payload_type.is_some() || p.envelope_path.is_some() || p.trusted_key_path.is_some() {
return Err(contract(format!(
"provenance.payload_type/envelope_path/trusted_key_path are only valid for kind \"dsse\", not {kind:?}"
)));
}
Ok(())
}
pub(super) fn build_carrier(raw: &str, base_dir: &Path) -> Result<SupplyChainConformance, EmitErr> {
let d: InputDescriptor = serde_json::from_str(raw)
.map_err(|e| contract(format!("malformed input descriptor: {e}")))?;
if d.schema != INPUT_SCHEMA {
return Err(contract(format!(
"input descriptor schema must be \"{INPUT_SCHEMA}\"; got {:?}",
d.schema
)));
}
let container_ref = match d.pinning.container_ref.as_deref() {
None => None,
Some("digest_pinned") => Some(ContainerRef::DigestPinned),
Some("tag_only") => Some(ContainerRef::TagOnly),
Some(other) => {
return Err(contract(format!(
"unknown pinning.container_ref {other:?} (expected digest_pinned | tag_only)"
)));
}
};
let pinning = PinningInput {
version_pinned: d.pinning.version_pinned,
digest_pinned: d.pinning.digest_pinned,
lockfile_digest: d.pinning.lockfile_digest,
floating_source_ref: d.pinning.floating_source_ref,
container_ref,
};
let policy = Policy {
required_builder_id: d.policy.required_builder_id,
required_slsa_build_level: SlsaLevel(d.policy.required_slsa_build_level),
require_rekor_inclusion: d.policy.require_rekor_inclusion,
require_timestamp_freshness: d.policy.require_timestamp_freshness,
require_consistency: d.policy.require_consistency,
require_witnessing: d.policy.require_witnessing,
};
let subject = Subject {
name: d.subject.name,
version: d.subject.version,
digest: d.subject.digest,
};
let store;
let provenance = match d.provenance.kind.as_str() {
"none" => {
if d.provenance.format.is_some() {
return Err(contract(
"provenance.format is only valid for kind \"unsupported\"",
));
}
reject_dsse_fields(&d.provenance, "none")?;
store = TrustStore::new();
ProvenanceInput::None
}
"unsupported" => {
let format = d.provenance.format.as_deref().ok_or_else(|| {
contract("provenance.format is required for kind \"unsupported\"")
})?;
let kind = match format {
"pep740" => UnsupportedProvenance::Pep740,
"npm_provenance" => UnsupportedProvenance::NpmProvenance,
"unknown_predicate" => UnsupportedProvenance::UnknownPredicate,
other => {
return Err(contract(format!(
"unknown provenance.format {other:?} (expected pep740 | npm_provenance | unknown_predicate)"
)));
}
};
reject_dsse_fields(&d.provenance, "unsupported")?;
store = TrustStore::new();
ProvenanceInput::Unsupported(kind)
}
"dsse" => {
if d.provenance.format.is_some() {
return Err(contract(
"provenance.format is only valid for kind \"unsupported\"",
));
}
let payload_type =
d.provenance.payload_type.as_deref().ok_or_else(|| {
contract("provenance.payload_type is required for kind \"dsse\"")
})?;
let envelope_path = d.provenance.envelope_path.as_deref().ok_or_else(|| {
contract("provenance.envelope_path is required for kind \"dsse\"")
})?;
let trusted_key_path = d.provenance.trusted_key_path.as_deref().ok_or_else(|| {
contract("provenance.trusted_key_path is required for kind \"dsse\"")
})?;
if payload_type != DSSE_IN_TOTO_PAYLOAD_TYPE {
return Err(contract(format!(
"provenance.payload_type must be {DSSE_IN_TOTO_PAYLOAD_TYPE:?} for kind \"dsse\"; got {payload_type:?}"
)));
}
let env_file = resolve_under(base_dir, envelope_path, "envelope_path")?;
let env_bytes = std::fs::read(&env_file).map_err(|e| {
contract(format!(
"cannot read provenance.envelope_path {envelope_path:?}: {e}"
))
})?;
let envelope: DsseEnvelope = serde_json::from_slice(&env_bytes)
.map_err(|e| contract(format!("malformed DSSE envelope {envelope_path:?}: {e}")))?;
if envelope.payload_type != payload_type {
return Err(contract(format!(
"provenance.payload_type {payload_type:?} does not match the envelope payloadType {:?}",
envelope.payload_type
)));
}
let key_file = resolve_under(base_dir, trusted_key_path, "trusted_key_path")?;
let key_bytes = std::fs::read(&key_file).map_err(|e| {
contract(format!(
"cannot read provenance.trusted_key_path {trusted_key_path:?}: {e}"
))
})?;
let trusted_key: TrustedKey = serde_json::from_slice(&key_bytes)
.map_err(|e| contract(format!("malformed TrustedKey {trusted_key_path:?}: {e}")))?;
store = TrustStore::from_pinned_roots(vec![trusted_key]).map_err(|e| {
contract(format!("invalid pinned key in {trusted_key_path:?}: {e}"))
})?;
ProvenanceInput::Dsse(envelope)
}
"sigstore_bundle" => {
return Err(contract(
"provenance kind \"sigstore_bundle\" is not yet wired into this emitter (supported: none | unsupported | dsse); a follow-up adds the keyless Sigstore path",
));
}
other => {
return Err(contract(format!(
"unknown provenance.kind {other:?} (expected none | unsupported | dsse | sigstore_bundle)"
)));
}
};
Ok(verify_supply_chain(VerifyInput {
subject,
expected_artifact_digest: d.expected_artifact_digest,
provenance,
pinning,
policy,
trust_store: &store,
}))
}