nordisk/heimdall
guests/verify-bundle/src/verify.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! The verification pipeline: Holger's `container::load_bundle_verified`
//! followed by `verify::verify_member_map_inner`, over the archive bytes the
//! guest was handed, and then the DSSE step.
//!
//! Order (fail-closed, cheapest and most fundamental first):
//! 0. the parameters: a coherent policy (`trust_mode`), a `now`
//! 1. the archive: size cap, footer, manifest, index, chunk geometry, every
//! member name through the path guard, the member count, the bomb SHAPE from
//! the declared sizes — before one blob byte is read
//! 2. the control members only (`core.json`, `manifest.json`, `signatures/**`,
//! `approvals/**`), under their own per-member and total caps
//! 3. the control plane: the release manifest binds the present members, the
//! release signature over its exact bytes, the release signer named and
//! anchored, the candidate digest, the verdict, the approvals, two persons
//! 4. every other member: named by the SIGNED manifest before it is read, the
//! signed sizes budgeted, each read held to its signed size
//! 5. the whole bundle again, Holger's full order (content list both ways,
//! envelope, approvals, two-person, expiry, evidence, target, sequence)
//! 6. DSSE envelopes: signature before statement, statement bound to a payload
use std::collections::{BTreeMap, BTreeSet};
use chrono::{DateTime, Utc};
use serde::Serialize;
use crate::cert::{chains_to_root, SignerCert};
use crate::digest::{blake3_hex, sha256_hex};
use crate::dsse::{self, DsseTrust, VerifiedEnvelope};
use crate::limits::*;
use crate::model::{
check_admissible, ApprovalStatement, BundleCore, EnvelopeManifest, EvidenceKind,
ReleaseManifest, TrustMode, TrustPolicy,
};
use crate::params::VerifyParams;
use crate::refusal::{Refusal, Result};
use crate::znippy::{Archive, Source};
/// Path -> exact bytes, sorted.
pub type MemberMap = BTreeMap<String, Vec<u8>>;
/// Path -> (SHA-256, BLAKE3-256) of the member, lowercase hex. Computed ONCE
/// per member and used by every step that compares a digest (Holger's order
/// checks each member against the manifest and core.json and reports it).
pub type Digests = BTreeMap<String, (String, String)>;
/// Every member's digests, computed here.
fn digests_here(members: &MemberMap) -> Digests {
members
.iter()
.map(|(p, b)| (p.clone(), (sha256_hex(b), blake3_hex(b))))
.collect()
}
/// Every member's digests: from the split guest's host (natively, in
/// parallel) when the decoder offers them, else computed here.
fn member_digests(members: &MemberMap, src: &mut Source<'_>) -> Result<Digests> {
let Source::Split(dec) = src else {
return Ok(digests_here(members));
};
let blobs: Vec<&[u8]> = members.values().map(Vec::as_slice).collect();
let Some(got) = dec.member_digests(&blobs) else {
return Ok(digests_here(members));
};
if got.len() != members.len() {
return Err(Refusal::Decoder(format!(
"{} member digests for {} members",
got.len(),
members.len()
)));
}
let mut out = Digests::new();
for (p, d) in members.keys().zip(got) {
if d.sha256.len() != 32 || d.blake3.len() != 32 {
return Err(Refusal::Decoder(format!(
"member {p:?}: digests of {} and {} bytes, not 32",
d.sha256.len(),
d.blake3.len()
)));
}
out.insert(
p.clone(),
(
crate::digest::hex_lower(&d.sha256),
crate::digest::hex_lower(&d.blake3),
),
);
}
Ok(out)
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct ApprovalRecord {
pub role: String,
pub fingerprint: String,
pub subject: String,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct DsseRecord {
pub member: String,
pub predicate_type: String,
pub signers: Vec<String>,
pub subjects_bound: Vec<String>,
}
/// What an accepted bundle is. Field names follow Holger's `VerifyReport`.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct VerifyReport {
pub schema: &'static str,
pub bundle_id: String,
pub sequence: u64,
pub supplier: String,
pub product: String,
pub release_version: String,
pub candidate_digest: String,
pub release_signer_fingerprint: String,
pub release_signer_subject: String,
pub approvals: Vec<ApprovalRecord>,
pub non_production_evidence: bool,
pub members: Vec<MemberRecord>,
pub dsse: Vec<DsseRecord>,
pub checks: Vec<String>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct MemberRecord {
pub path: String,
/// The file name the member is emitted under ([`crate::output_name`]).
pub output_name: String,
pub size: u64,
pub sha256: String,
pub blake3: String,
}
pub const REPORT_SCHEMA: &str = "heimdall.verify-bundle.report/v1";
/// The whole pipeline. On success: the report and the verified members.
pub fn verify(input: &[u8], params: &VerifyParams) -> Result<(VerifyReport, MemberMap)> {
verify_with(input, params, &mut Source::InProcess)
}
/// [`verify`] with compressed chunks decoded by `src` (the split guest hands
/// them to isolated decoder instances). Same verdicts as [`verify`].
pub fn verify_with(
input: &[u8],
params: &VerifyParams,
src: &mut Source<'_>,
) -> Result<(VerifyReport, MemberMap)> {
let policy = ¶ms.policy;
let now = params.now;
let mode = policy.trust_mode()?;
// ---- 1. the archive, names and shape, no blob read ----
if input.len() as u64 > MAX_BUNDLE_FILE_BYTES {
return Err(Refusal::LimitExceeded(format!(
"bundle file size {} exceeds {MAX_BUNDLE_FILE_BYTES}",
input.len()
)));
}
let archive = Archive::parse(input)?;
if archive.len() > MAX_MEMBERS {
return Err(Refusal::LimitExceeded(format!(
"member count {} exceeds {MAX_MEMBERS}",
archive.len()
)));
}
archive.check_expansion_ratio()?;
// ---- 2. control members, and nothing else ----
// Read in order; the total cap fires after the member that crosses it,
// exactly where a member-by-member loop would stop.
let names: Vec<&str> = archive.names().filter(|n| is_control_member(n)).collect();
let mut control_total: u64 = 0;
let mut upto = names.len();
let mut over = false;
for (i, name) in names.iter().enumerate() {
let size = archive.entry(name).map_or(0, |e| e.size);
control_total = control_total.saturating_add(size);
if control_total > MAX_CONTROL_TOTAL_BYTES {
upto = i + 1;
over = true;
break;
}
}
let reqs: Vec<(&str, u64)> = names[..upto]
.iter()
.map(|n| (*n, MAX_CONTROL_BYTES))
.collect();
let read = archive.read_many(&reqs, src)?;
if over {
return Err(Refusal::LimitExceeded(format!(
"control members exceed {MAX_CONTROL_TOTAL_BYTES} before anything is authenticated"
)));
}
let mut control = MemberMap::new();
for (name, bytes) in names.iter().zip(read) {
control.insert(name.to_string(), bytes);
}
// ---- 3. the gate ----
let roots = collect_roots(policy)?;
let gate = verify_control_plane(&control, policy, &roots, mode, now)?;
let signed = gate.manifest.index()?;
// ---- 4. budget from SIGNED sizes, then read ----
let mut budget = control_total;
let content: Vec<&str> = archive.names().filter(|n| !is_control_member(n)).collect();
for name in &content {
let entry = signed
.get(name)
.ok_or_else(|| Refusal::UnreferencedMember(name.to_string()))?;
if entry.size_bytes > MAX_MEMBER_BYTES {
return Err(Refusal::LimitExceeded(format!(
"member {name:?} signed size {} exceeds {MAX_MEMBER_BYTES}",
entry.size_bytes
)));
}
budget = budget.saturating_add(entry.size_bytes);
if budget > MAX_TOTAL_BYTES {
return Err(Refusal::LimitExceeded(format!(
"signed member sizes total {budget}, over {MAX_TOTAL_BYTES} — refusing before extraction"
)));
}
if archive.entry(name).map(|e| e.size) != Some(entry.size_bytes) {
return Err(Refusal::DigestMismatch {
path: name.to_string(),
which: "size",
});
}
}
let mut members = control;
let reqs: Vec<(&str, u64)> = content.iter().map(|n| (*n, signed[n].size_bytes)).collect();
for (name, bytes) in content.iter().zip(archive.read_many(&reqs, src)?) {
members.insert(name.to_string(), bytes);
}
drop(signed);
// ---- 5. the full pipeline over the assembled bundle ----
let digests = member_digests(&members, src)?;
let mut report = verify_member_map(&members, &digests, params, &roots, mode, now)?;
// ---- 6. DSSE ----
let core = BundleCore::parse(&members[CORE_JSON])?;
report.dsse = verify_dsse(&members, &core, params)?;
if !report.dsse.is_empty() {
report.checks.push(format!(
"{} DSSE envelope(s) verified before their statements were read, each bound to a payload",
report.dsse.len()
));
}
Ok((report, members))
}
fn collect_roots(policy: &TrustPolicy) -> Result<Vec<SignerCert>> {
policy
.root_certs_der()?
.iter()
.map(|d| SignerCert::parse(d))
.collect()
}
fn member<'a>(members: &'a MemberMap, path: &str) -> Result<&'a Vec<u8>> {
members
.get(path)
.ok_or_else(|| Refusal::MissingMember(path.to_string()))
}
fn parse_release_manifest(members: &MemberMap) -> Result<ReleaseManifest> {
let m = ReleaseManifest::parse(member(members, RELEASE_MANIFEST_JSON)?)?;
if m.schema_version != BUNDLE_SCHEMA {
return Err(Refusal::UnsupportedSchema {
found: m.schema_version,
expected: BUNDLE_SCHEMA,
});
}
Ok(m)
}
fn authenticate_release_signature(
members: &MemberMap,
policy: &TrustPolicy,
roots: &[SignerCert],
mode: TrustMode,
now: DateTime<Utc>,
) -> Result<SignerCert> {
let manifest_bytes = member(members, RELEASE_MANIFEST_JSON)?;
let sig = member(members, RELEASE_SIG)?;
let cert = SignerCert::parse(member(members, RELEASE_CRT)?)?;
cert.ensure_valid_under(now, policy.certificate_validity)?;
cert.verify(manifest_bytes, sig)
.map_err(|_| Refusal::InvalidReleaseSignature)?;
if !policy.is_release_signer(cert.fingerprint()) || !chains_to_root(&cert, roots, mode) {
return Err(Refusal::UntrustedReleaseSigner(
cert.fingerprint().to_string(),
));
}
Ok(cert)
}
/// Every PRESENT member against the manifest (Holger's
/// `verify_present_members_against`). A member present and not referenced is
/// a stowaway.
fn verify_present_members(
manifest: &ReleaseManifest,
present: &MemberMap,
digests: &Digests,
) -> Result<()> {
let index = manifest.index()?;
for (path, bytes) in present {
if is_self_referential(path) {
continue;
}
let e = index
.get(path.as_str())
.ok_or_else(|| Refusal::UnreferencedMember(path.clone()))?;
check_digest(path, bytes, digests, e.size_bytes, &e.sha256, &e.blake3)?;
}
Ok(())
}
fn check_digest(
path: &str,
bytes: &[u8],
digests: &Digests,
size: u64,
sha: &str,
b3: &str,
) -> Result<()> {
if size != bytes.len() as u64 {
return Err(Refusal::DigestMismatch {
path: path.to_string(),
which: "size",
});
}
let (have_sha, have_b3) = match digests.get(path) {
Some((s, b)) => (s.clone(), b.clone()),
None => (sha256_hex(bytes), blake3_hex(bytes)),
};
if !sha.eq_ignore_ascii_case(&have_sha) {
return Err(Refusal::DigestMismatch {
path: path.to_string(),
which: "sha256",
});
}
if !b3.eq_ignore_ascii_case(&have_b3) {
return Err(Refusal::DigestMismatch {
path: path.to_string(),
which: "blake3",
});
}
Ok(())
}
struct ControlVerdict {
manifest: ReleaseManifest,
}
fn verify_control_plane(
control: &MemberMap,
policy: &TrustPolicy,
roots: &[SignerCert],
mode: TrustMode,
now: DateTime<Utc>,
) -> Result<ControlVerdict> {
let manifest = parse_release_manifest(control)?;
verify_present_members(&manifest, control, &digests_here(control))?;
let release = authenticate_release_signature(control, policy, roots, mode, now)?;
let core_bytes = member(control, CORE_JSON)?;
let candidate_digest = sha256_hex(core_bytes);
if candidate_digest != manifest.candidate_digest {
return Err(Refusal::CandidateDigestMismatch);
}
BundleCore::parse(core_bytes)?.require_pass_verdict()?;
let approvals = verify_approvals(control, policy, &candidate_digest, roots, mode, now)?;
enforce_two_person(policy, &approvals, release.fingerprint())?;
Ok(ControlVerdict { manifest })
}
fn cert_path_for(sig_path: &str) -> Result<String> {
sig_path
.strip_suffix(".sig")
.map(|b| format!("{b}.crt"))
.ok_or_else(|| {
Refusal::Malformed(format!(
"approval signature_path {sig_path:?} must end with .sig"
))
})
}
fn verify_approvals(
members: &MemberMap,
policy: &TrustPolicy,
candidate_digest: &str,
roots: &[SignerCert],
mode: TrustMode,
now: DateTime<Utc>,
) -> Result<Vec<ApprovalRecord>> {
let paths: Vec<&String> = members
.keys()
.filter(|k| k.starts_with(APPROVALS_PREFIX) && k.ends_with(".approval.json"))
.collect();
if paths.len() > MAX_APPROVALS {
return Err(Refusal::LimitExceeded(
"too many approval statements".into(),
));
}
let mut out = Vec::new();
for ap in paths {
let stmt_bytes = member(members, ap)?;
let stmt = ApprovalStatement::parse(stmt_bytes)?;
let sig = member(members, &stmt.signature_path)?;
let cert = SignerCert::parse(member(members, &cert_path_for(&stmt.signature_path)?)?)?;
cert.ensure_valid_under(now, policy.certificate_validity)?;
let invalid = || Refusal::InvalidApprovalSignature {
role: stmt.approval_role.clone(),
fingerprint: cert.fingerprint().to_string(),
};
cert.verify(stmt_bytes, sig).map_err(|_| invalid())?;
if !stmt
.signer_certificate_fingerprint
.eq_ignore_ascii_case(cert.fingerprint())
{
return Err(invalid());
}
if !stmt.candidate_digest.eq_ignore_ascii_case(candidate_digest) {
return Err(Refusal::Policy(format!(
"approval by {} is for a different candidate digest",
cert.fingerprint()
)));
}
if !policy.authorizes_role(cert.fingerprint(), &stmt.approval_role)
|| !chains_to_root(&cert, roots, mode)
{
return Err(Refusal::UntrustedSigner {
fingerprint: cert.fingerprint().to_string(),
role: stmt.approval_role.clone(),
});
}
if stmt.is_approve() {
out.push(ApprovalRecord {
role: stmt.approval_role,
fingerprint: cert.fingerprint().to_string(),
subject: cert.subject().to_string(),
});
}
}
Ok(out)
}
/// Holger's `enforce_two_person`: distinct approvers, the release signer
/// distinct from them, and every required role held by a DISTINCT certificate
/// (a bipartite matching, so one certificate never satisfies two roles).
fn enforce_two_person(
policy: &TrustPolicy,
approvals: &[ApprovalRecord],
release_fp: &str,
) -> Result<()> {
let mut distinct: Vec<&str> = approvals.iter().map(|a| a.fingerprint.as_str()).collect();
distinct.sort_unstable();
distinct.dedup();
if distinct.len() < policy.minimum_distinct_approvers {
return Err(Refusal::TwoPersonRule(format!(
"{} distinct approver(s), need {}",
distinct.len(),
policy.minimum_distinct_approvers
)));
}
if policy.release_signer_must_be_distinct
&& approvals
.iter()
.any(|a| a.fingerprint.eq_ignore_ascii_case(release_fp))
{
return Err(Refusal::TwoPersonRule(
"release signer must be distinct from every approver".into(),
));
}
let mut required: Vec<&str> = policy
.required_approval_roles
.iter()
.map(String::as_str)
.collect();
required.sort_unstable();
required.dedup();
let candidates: Vec<Vec<usize>> = required
.iter()
.map(|role| {
distinct
.iter()
.enumerate()
.filter(|(_, fp)| {
approvals
.iter()
.any(|a| a.fingerprint == **fp && a.role == *role)
})
.map(|(i, _)| i)
.collect()
})
.collect();
let mut match_fp: Vec<Option<usize>> = vec![None; distinct.len()];
for (ri, role) in required.iter().enumerate() {
let mut seen = vec![false; distinct.len()];
if !try_assign(ri, &candidates, &mut match_fp, &mut seen) {
return Err(Refusal::MissingRole((*role).to_string()));
}
}
Ok(())
}
fn try_assign(
ri: usize,
candidates: &[Vec<usize>],
match_fp: &mut [Option<usize>],
seen: &mut [bool],
) -> bool {
for &fp in &candidates[ri] {
if seen[fp] {
continue;
}
seen[fp] = true;
let free = match match_fp[fp] {
None => true,
Some(other) => try_assign(other, candidates, match_fp, seen),
};
if free {
match_fp[fp] = Some(ri);
return true;
}
}
false
}
/// Holger's `verify_member_map_inner`, step for step.
fn verify_member_map(
members: &MemberMap,
digests: &Digests,
params: &VerifyParams,
roots: &[SignerCert],
mode: TrustMode,
now: DateTime<Utc>,
) -> Result<VerifyReport> {
let policy = ¶ms.policy;
let target = ¶ms.target;
let mut checks = vec![
format!(
"time source: the caller's `now` ({}), not a clock the guest can read",
now.to_rfc3339()
),
format!("trust posture stated by the policy: {mode:?}"),
];
let manifest = parse_release_manifest(members)?;
verify_present_members(&manifest, members, digests)?;
for m in &manifest.members {
if !members.contains_key(&m.relative_path) {
return Err(Refusal::MissingMember(m.relative_path.clone()));
}
}
checks.push("every member digest matches the release manifest".into());
let release = authenticate_release_signature(members, policy, roots, mode, now)?;
checks.push("release signature valid and signer authorized".into());
let core_bytes = member(members, CORE_JSON)?;
let candidate_digest = sha256_hex(core_bytes);
if candidate_digest != manifest.candidate_digest {
return Err(Refusal::CandidateDigestMismatch);
}
let core = BundleCore::parse(core_bytes)?;
if !core.schema_ok() {
return Err(Refusal::UnsupportedSchema {
found: core.schema_version.clone(),
expected: BUNDLE_SCHEMA,
});
}
checks.push("candidate digest matches core.json".into());
core.require_pass_verdict()?;
checks.push(format!(
"policy verdict is {:?}",
core.policy_result.verdict
));
for p in &core.payloads {
check_digest(
&p.relative_path,
member(members, &p.relative_path)?,
digests,
p.size_bytes,
&p.sha256,
&p.blake3,
)?;
}
for e in &core.evidence {
check_digest(
&e.relative_path,
member(members, &e.relative_path)?,
digests,
e.size_bytes,
&e.sha256,
&e.blake3,
)?;
}
let declared: BTreeSet<&str> = core
.payloads
.iter()
.map(|p| p.relative_path.as_str())
.chain(core.evidence.iter().map(|e| e.relative_path.as_str()))
.collect();
for path in members.keys() {
let is_content = path.starts_with(PAYLOAD_PREFIX) || path.starts_with(EVIDENCE_PREFIX);
if is_content && !declared.contains(path.as_str()) {
return Err(Refusal::UnreferencedMember(path.clone()));
}
}
checks.push("core.json's content list is complete and exact".into());
if let Some(env_bytes) = members.get(MANIFEST_JSON) {
let env: EnvelopeManifest = serde_json::from_slice(env_bytes)?;
if env.bundle_id != core.bundle_id
|| env.sequence != core.sequence
|| env.core_digest != candidate_digest
|| env.supplier != core.supplier
|| env.product != core.product
{
return Err(Refusal::EnvelopeMismatch(
"manifest.json does not agree with core.json".into(),
));
}
}
let approvals = verify_approvals(members, policy, &candidate_digest, roots, mode, now)?;
checks.push(format!(
"{} approval signature(s) valid and authorized",
approvals.len()
));
enforce_two_person(policy, &approvals, release.fingerprint())?;
checks.push("two-person rule satisfied".into());
if policy.require_expiry && core.expires_at.is_none() {
return Err(Refusal::MissingExpiry);
}
if let Some(exp) = core.expires_at {
if now > exp {
return Err(Refusal::Expired(exp.to_rfc3339()));
}
}
if let Some(max_age) = policy.maximum_bundle_age_secs {
if now.signed_duration_since(core.created_at).num_seconds() > max_age as i64 {
return Err(Refusal::TooOld);
}
}
checks.push("bundle is within its validity window".into());
if policy.require_sbom && !core.has_evidence_kind(EvidenceKind::Sbom) {
return Err(Refusal::MissingEvidence("sbom"));
}
if policy.require_vex && !core.has_evidence_kind(EvidenceKind::Vex) {
return Err(Refusal::MissingEvidence("vex"));
}
if policy.require_scan && !core.has_evidence_kind(EvidenceKind::Scan) {
return Err(Refusal::MissingEvidence("scan"));
}
if let Some(p) = &target.product {
if &core.product != p {
return Err(Refusal::TargetMismatch(format!(
"product {p:?} != bundle product {:?}",
core.product
)));
}
}
if !policy.allowed_products.is_empty() && !policy.allowed_products.contains(&core.product) {
return Err(Refusal::TargetMismatch(format!(
"product {:?} not in policy allow-list",
core.product
)));
}
if let Some(m) = &target.model {
if !core.supported_hardware_models.iter().any(|x| x == m) {
return Err(Refusal::TargetMismatch(format!(
"model {m:?} not supported by this bundle"
)));
}
}
if !policy.allowed_models.is_empty()
&& !core
.supported_hardware_models
.iter()
.any(|m| policy.allowed_models.contains(m))
{
return Err(Refusal::TargetMismatch(
"no supported model is in the policy allow-list".into(),
));
}
if let Some(r) = &target.revision {
if !core.supported_hardware_revisions.iter().any(|x| x == r) {
return Err(Refusal::TargetMismatch(format!(
"revision {r:?} not supported by this bundle"
)));
}
}
checks.push("target product / model / revision accepted".into());
if let Some(accepted) = ¶ms.accepted {
check_admissible(accepted, &core, &candidate_digest)?;
checks.push("sequence is newer than the accepted state".into());
}
Ok(VerifyReport {
schema: REPORT_SCHEMA,
bundle_id: core.bundle_id,
sequence: core.sequence,
supplier: core.supplier,
product: core.product,
release_version: core.release_version,
candidate_digest,
release_signer_fingerprint: release.fingerprint().to_string(),
release_signer_subject: release.subject().to_string(),
approvals,
non_production_evidence: core.policy_result.non_production,
members: members
.iter()
.map(|(p, b)| {
let (sha256, blake3) = digests
.get(p)
.cloned()
.unwrap_or_else(|| (sha256_hex(b), blake3_hex(b)));
MemberRecord {
path: p.clone(),
output_name: crate::output_name(p),
size: b.len() as u64,
sha256,
blake3,
}
})
.collect(),
dsse: Vec::new(),
checks,
})
}
fn verify_dsse(
members: &MemberMap,
core: &BundleCore,
params: &VerifyParams,
) -> Result<Vec<DsseRecord>> {
let media: BTreeMap<&str, &str> = core
.payloads
.iter()
.map(|p| (p.relative_path.as_str(), p.media_type.as_str()))
.chain(
core.evidence
.iter()
.map(|e| (e.relative_path.as_str(), e.media_type.as_str())),
)
.collect();
let extra = dsse::decode_extra_certs(¶ms.dsse.certificates)?;
let trust = DsseTrust {
trusted: ¶ms.dsse.trusted_signers,
keys: dsse::decode_pinned_keys(¶ms.dsse.keys)?,
certificates: members
.values()
.map(Vec::as_slice)
.chain(extra.iter().map(Vec::as_slice))
.collect(),
now: params.now,
validity: params.policy.certificate_validity,
};
let payload_shas: Vec<&str> = core.payloads.iter().map(|p| p.sha256.as_str()).collect();
let mut out = Vec::new();
for (path, bytes) in members {
if !(path.starts_with(PAYLOAD_PREFIX) || path.starts_with(EVIDENCE_PREFIX)) {
continue;
}
if !dsse::is_envelope_member(path, media.get(path.as_str()).copied()) {
continue;
}
for v in dsse::verify_member(path, bytes, &trust, &payload_shas)? {
let VerifiedEnvelope {
member,
predicate_type,
signers,
subjects_bound,
} = v;
out.push(DsseRecord {
member,
predicate_type,
signers,
subjects_bound,
});
}
}
if params.dsse.require_envelope && out.is_empty() {
return Err(Refusal::Dsse(
"the caller requires a DSSE envelope and the bundle carries none".into(),
));
}
Ok(out)
}