nordisk/heimdall
guests/verify-bundle/src/refusal.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! Why a bundle was refused.
//!
//! The bundle-pipeline refusals mirror Holger's `BundleError` one for one and
//! carry the SAME frozen `holger-update-<n>` codes (from the `holger-errcode`
//! registry), so a Holger importer that reads Heimdall's verdict branches on the
//! code exactly as it would on its own verifier's. Refusals that only this guest
//! can make (the container codec, the parameters, DSSE) carry a
//! `heimdall-bundle-<n>` code of their own.
use core::fmt;
use holger_errcode::{self as ec, ErrCode};
/// A guest-local refusal code. Never reused, never renumbered.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GuestCode(pub u16);
impl GuestCode {
/// The parameters the host passed could not be read.
pub const PARAMS: GuestCode = GuestCode(1);
/// The input is not a znippy v0.7 archive, or its index is malformed.
pub const ARCHIVE: GuestCode = GuestCode(2);
/// A member is stored compressed in a way the guest does not decode (built
/// without the `openzl` feature, or a znippy delta map).
pub const CODEC: GuestCode = GuestCode(3);
/// A DSSE envelope was malformed, unsigned, signed by an unpinned key, or
/// its signature did not verify.
pub const DSSE: GuestCode = GuestCode(4);
/// The in-toto statement inside a VERIFIED DSSE envelope does not bind to
/// the bundle's content.
pub const DSSE_BINDING: GuestCode = GuestCode(5);
/// A parser inside the guest panicked; the input is refused, never trusted.
pub const PARSER_PANIC: GuestCode = GuestCode(6);
pub fn tag(self) -> String {
format!("heimdall-bundle-{}", self.0)
}
}
/// A refusal. `Display` is the human half; [`Refusal::tagged`] adds the code.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Refusal {
// ---- Holger's BundleError, mirrored (same codes, same prose) ----
UnsupportedSchema {
found: String,
expected: &'static str,
},
UnsupportedPolicySchema {
found: String,
expected: &'static str,
},
UnsafePath(String),
LimitExceeded(String),
MissingMember(String),
UnreferencedMember(String),
DigestMismatch {
path: String,
which: &'static str,
},
CandidateDigestMismatch,
EnvelopeMismatch(String),
InvalidReleaseSignature,
InvalidApprovalSignature {
role: String,
fingerprint: String,
},
UntrustedSigner {
fingerprint: String,
role: String,
},
UntrustedReleaseSigner(String),
TwoPersonRule(String),
MissingRole(String),
Expired(String),
TooOld,
MissingExpiry,
MissingEvidence(&'static str),
TargetMismatch(String),
SequenceRollback {
found: u64,
accepted: u64,
},
SequenceReplay(u64),
BundleIdReuse(String),
Certificate(String),
VerdictNotPass(String),
TrustModeUnset,
TrustRootsEmpty,
CertificateExpired {
fingerprint: String,
not_after: String,
},
CertificateNotYetValid {
fingerprint: String,
not_before: String,
},
Policy(String),
Malformed(String),
Container(String),
Serde(String),
// ---- the guest's own ----
Params(String),
Archive(String),
Codec(String),
Dsse(String),
DsseBinding(String),
ParserPanic(String),
}
/// The machine half of a refusal.
pub enum Code {
Holger(&'static ErrCode),
Guest(GuestCode),
}
impl Code {
pub fn as_string(&self) -> String {
match self {
Code::Holger(c) => c.code.to_string(),
Code::Guest(g) => g.tag(),
}
}
}
impl Refusal {
/// The frozen code for this refusal.
pub fn code(&self) -> Code {
use Refusal as R;
match self {
R::UnsupportedSchema { .. } => Code::Holger(&ec::UPDATE_SCHEMA_UNSUPPORTED),
R::UnsupportedPolicySchema { .. } => {
Code::Holger(&ec::UPDATE_POLICY_SCHEMA_UNSUPPORTED)
}
R::UnsafePath(_) => Code::Holger(&ec::UPDATE_UNSAFE_PATH),
R::LimitExceeded(_) => Code::Holger(&ec::UPDATE_LIMIT_EXCEEDED),
R::MissingMember(_) => Code::Holger(&ec::UPDATE_MEMBER_MISSING),
R::UnreferencedMember(_) => Code::Holger(&ec::UPDATE_MEMBER_UNREFERENCED),
R::DigestMismatch { .. } => Code::Holger(&ec::UPDATE_DIGEST_MISMATCH),
R::CandidateDigestMismatch => Code::Holger(&ec::UPDATE_CANDIDATE_DIGEST_MISMATCH),
R::EnvelopeMismatch(_) => Code::Holger(&ec::UPDATE_ENVELOPE_MISMATCH),
R::InvalidReleaseSignature => Code::Holger(&ec::UPDATE_RELEASE_SIGNATURE_INVALID),
R::InvalidApprovalSignature { .. } => {
Code::Holger(&ec::UPDATE_APPROVAL_SIGNATURE_INVALID)
}
R::UntrustedSigner { .. } => Code::Holger(&ec::UPDATE_UNTRUSTED_SIGNER),
R::UntrustedReleaseSigner(_) => Code::Holger(&ec::UPDATE_UNTRUSTED_RELEASE_SIGNER),
R::TwoPersonRule(_) => Code::Holger(&ec::UPDATE_TWO_PERSON_RULE),
R::MissingRole(_) => Code::Holger(&ec::UPDATE_ROLE_MISSING),
R::Expired(_) => Code::Holger(&ec::UPDATE_EXPIRED),
R::TooOld => Code::Holger(&ec::UPDATE_TOO_OLD),
R::MissingExpiry => Code::Holger(&ec::UPDATE_EXPIRY_MISSING),
R::MissingEvidence(_) => Code::Holger(&ec::UPDATE_EVIDENCE_MISSING),
R::TargetMismatch(_) => Code::Holger(&ec::UPDATE_TARGET_MISMATCH),
R::SequenceRollback { .. } => Code::Holger(&ec::UPDATE_SEQUENCE_ROLLBACK),
R::SequenceReplay(_) => Code::Holger(&ec::UPDATE_SEQUENCE_REPLAY),
R::BundleIdReuse(_) => Code::Holger(&ec::UPDATE_BUNDLE_ID_REUSE),
R::Certificate(_) => Code::Holger(&ec::UPDATE_CERTIFICATE),
R::VerdictNotPass(_) => Code::Holger(&ec::UPDATE_VERDICT_NOT_PASS),
R::TrustModeUnset => Code::Holger(&ec::UPDATE_TRUST_MODE_UNSET),
R::TrustRootsEmpty => Code::Holger(&ec::UPDATE_TRUST_ROOTS_EMPTY),
R::CertificateExpired { .. } => Code::Holger(&ec::UPDATE_CERTIFICATE_EXPIRED),
R::CertificateNotYetValid { .. } => Code::Holger(&ec::UPDATE_CERTIFICATE_NOT_YET_VALID),
R::Policy(_) => Code::Holger(&ec::UPDATE_POLICY),
R::Malformed(_) => Code::Holger(&ec::UPDATE_MALFORMED),
R::Container(_) => Code::Holger(&ec::UPDATE_CONTAINER),
R::Serde(_) => Code::Holger(&ec::UPDATE_SERDE),
R::Params(_) => Code::Guest(GuestCode::PARAMS),
R::Archive(_) => Code::Guest(GuestCode::ARCHIVE),
R::Codec(_) => Code::Guest(GuestCode::CODEC),
R::Dsse(_) => Code::Guest(GuestCode::DSSE),
R::DsseBinding(_) => Code::Guest(GuestCode::DSSE_BINDING),
R::ParserPanic(_) => Code::Guest(GuestCode::PARSER_PANIC),
}
}
/// Both channels in one string: `[holger-update-22] sequence rollback ...`.
pub fn tagged(&self) -> String {
format!("[{}] {}", self.code().as_string(), self)
}
}
impl fmt::Display for Refusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Refusal as R;
match self {
R::UnsupportedSchema { found, expected } => write!(
f,
"bundle schema version {found:?} is not supported (expected {expected})"
),
R::UnsupportedPolicySchema { found, expected } => write!(
f,
"trust-policy schema version {found:?} is not supported (expected {expected})"
),
R::UnsafePath(s) => write!(f, "unsafe member path rejected: {s}"),
R::LimitExceeded(s) => write!(f, "bundle exceeds a resource limit: {s}"),
R::MissingMember(s) => write!(f, "bundle member {s:?} is missing"),
R::UnreferencedMember(s) => write!(f, "bundle contains an unreferenced member {s:?}"),
R::DigestMismatch { path, which } => {
write!(f, "{which} digest mismatch for member {path:?}")
}
R::CandidateDigestMismatch => write!(
f,
"candidate digest mismatch: core.json does not match the approved candidate"
),
R::EnvelopeMismatch(s) => write!(f, "envelope inconsistent with core.json: {s}"),
R::InvalidReleaseSignature => write!(f, "release signature is invalid"),
R::InvalidApprovalSignature { role, fingerprint } => write!(
f,
"approval signature invalid for role {role} signer {fingerprint}"
),
R::UntrustedSigner { fingerprint, role } => write!(
f,
"untrusted signer: fingerprint {fingerprint} is not authorized for role {role}"
),
R::UntrustedReleaseSigner(s) => write!(f, "untrusted release signer: fingerprint {s}"),
R::TwoPersonRule(s) => write!(f, "two-person rule not satisfied: {s}"),
R::MissingRole(s) => write!(f, "required approval role missing: {s}"),
R::Expired(s) => write!(f, "bundle has expired (expires_at {s})"),
R::TooOld => write!(f, "bundle exceeds the policy maximum age"),
R::MissingExpiry => write!(f, "bundle is missing a required expiry timestamp"),
R::MissingEvidence(s) => write!(f, "required evidence missing: {s}"),
R::TargetMismatch(s) => write!(f, "target mismatch: {s}"),
R::SequenceRollback { found, accepted } => write!(
f,
"sequence rollback: bundle sequence {found} is not newer than the accepted {accepted}"
),
R::SequenceReplay(n) => write!(
f,
"sequence replay: sequence {n} was already accepted with different content"
),
R::BundleIdReuse(s) => write!(
f,
"bundle-id reuse: {s} was already accepted with different content"
),
R::Certificate(s) => write!(f, "certificate error: {s}"),
R::VerdictNotPass(v) => write!(
f,
"policy verdict is {v:?}, not \"pass\": the reviewers did not clear this bundle. \
Refusing it, although its signatures may be genuine."
),
R::TrustModeUnset => write!(
f,
"trust policy states no trust_mode — it must declare RootedChain or PinnedSigners"
),
R::TrustRootsEmpty => write!(
f,
"trust policy is RootedChain but lists no trusted root — nothing can be anchored"
),
R::CertificateExpired {
fingerprint,
not_after,
} => write!(f, "signer certificate {fingerprint} expired at {not_after}"),
R::CertificateNotYetValid {
fingerprint,
not_before,
} => write!(
f,
"signer certificate {fingerprint} is not valid until {not_before}"
),
R::Policy(s) => write!(f, "policy violation: {s}"),
R::Malformed(s) => write!(f, "malformed bundle: {s}"),
R::Container(s) => write!(f, "container error: {s}"),
R::Serde(s) => write!(f, "serialization error: {s}"),
R::Params(s) => write!(f, "the verification parameters are unusable: {s}"),
R::Archive(s) => write!(f, "not a readable znippy v0.7 archive: {s}"),
R::Codec(s) => write!(f, "cannot decode in the guest: {s}"),
R::Dsse(s) => write!(f, "DSSE envelope refused: {s}"),
R::DsseBinding(s) => write!(f, "verified DSSE statement does not bind: {s}"),
R::ParserPanic(s) => write!(f, "a parser panicked on this input: {s}"),
}
}
}
impl From<serde_json::Error> for Refusal {
fn from(e: serde_json::Error) -> Self {
Refusal::Serde(format!("json: {e}"))
}
}
pub type Result<T> = core::result::Result<T, Refusal>;