use image::GenericImageView;
use sha2::{Digest, Sha256};
use crate::detached::manifest::DetachedManifest;
use crate::resource_limits::ResourceLimits;
use crate::verification::report::{FieldSource, SignatureVerification, VerificationReport};
pub type TrustCallbackFn = dyn Fn(&[u8]) -> bool + Send + Sync;
#[cfg(feature = "signatures")]
#[derive(Clone)]
pub struct TrustedVerifyingKey {
pub key_id: Vec<u8>,
pub key: crate::signing::VerifyingKey,
}
#[cfg(feature = "signatures")]
impl std::fmt::Debug for TrustedVerifyingKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TrustedVerifyingKey")
.field("key_id", &hex::encode(&self.key_id))
.finish_non_exhaustive()
}
}
pub enum TrustPolicy {
TrustNone,
TrustKeys(Vec<Vec<u8>>),
TrustCallback(Box<TrustCallbackFn>),
}
impl std::fmt::Debug for TrustPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TrustPolicy::TrustNone => write!(f, "TrustNone"),
TrustPolicy::TrustKeys(keys) => f.debug_tuple("TrustKeys").field(keys).finish(),
TrustPolicy::TrustCallback(_) => write!(f, "TrustCallback(<function>)"),
}
}
}
#[derive(Debug, Default)]
pub struct DetachedVerificationOptions<'a> {
pub trust_policy: Option<&'a TrustPolicy>,
#[cfg(feature = "signatures")]
pub caller_verifying_keys: &'a [TrustedVerifyingKey],
pub payload_mac_key: Option<&'a [u8]>,
pub limits: Option<&'a ResourceLimits>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmbeddedReferenceStatus {
NotProvided,
Stripped,
VersionMismatch,
DigestMismatch,
Malformed,
#[deprecated(note = "use PresentValid")]
Present,
PresentValid,
AuthenticationKeyMissing,
AuthenticationFailed,
UnsupportedVersion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetachedOverallStatus {
VerifiedTrusted,
VerifiedUntrusted,
InvalidConfiguration,
BindingFailure,
SignatureFailure,
EmbeddedReferenceFailure,
KeyMaterialMismatch,
}
impl DetachedOverallStatus {
#[must_use]
pub fn exit_code(&self) -> i32 {
match self {
Self::VerifiedTrusted => 0,
Self::VerifiedUntrusted => 4,
Self::InvalidConfiguration => 2,
Self::BindingFailure
| Self::SignatureFailure
| Self::EmbeddedReferenceFailure
| Self::KeyMaterialMismatch => 3,
}
}
}
#[derive(Debug, Clone)]
pub struct ManifestVerification {
pub report: VerificationReport,
pub instance_digest_match: bool,
pub manifest_valid: bool,
pub embedded_reference_status: EmbeddedReferenceStatus,
}
impl ManifestVerification {
#[must_use]
pub fn overall_status(&self) -> DetachedOverallStatus {
if !self.manifest_valid {
return DetachedOverallStatus::InvalidConfiguration;
}
if !self.instance_digest_match {
return DetachedOverallStatus::BindingFailure;
}
let has_key_material_mismatch = self
.report
.signatures()
.iter()
.any(|s| s.key_id_matched() && !s.key_material_matched());
if has_key_material_mismatch {
return DetachedOverallStatus::KeyMaterialMismatch;
}
if !self
.report
.signatures()
.iter()
.any(|s| s.cryptographically_valid())
{
return DetachedOverallStatus::SignatureFailure;
}
match self.embedded_reference_status {
#[allow(deprecated)]
EmbeddedReferenceStatus::NotProvided
| EmbeddedReferenceStatus::Present
| EmbeddedReferenceStatus::PresentValid => {
if self.report.trust().trusted() {
DetachedOverallStatus::VerifiedTrusted
} else {
DetachedOverallStatus::VerifiedUntrusted
}
}
_ => DetachedOverallStatus::EmbeddedReferenceFailure,
}
}
}
#[must_use]
pub fn verify_detached_manifest(
image_bytes: &[u8],
manifest: &DetachedManifest,
trust: &TrustPolicy,
) -> ManifestVerification {
let limits = ResourceLimits::default();
verify_detached_manifest_with_limits(image_bytes, manifest, trust, Some(&limits))
}
#[must_use]
pub fn verify_detached_manifest_with_limits(
image_bytes: &[u8],
manifest: &DetachedManifest,
trust: &TrustPolicy,
limits: Option<&ResourceLimits>,
) -> ManifestVerification {
verify_detached_manifest_with_limits_and_mac(image_bytes, manifest, trust, limits, None)
}
#[must_use]
pub fn verify_detached_manifest_with_limits_and_mac(
image_bytes: &[u8],
manifest: &DetachedManifest,
trust: &TrustPolicy,
limits: Option<&ResourceLimits>,
payload_mac_key: Option<&[u8]>,
) -> ManifestVerification {
if let Some(limits) = limits {
if limits.check_input_size(image_bytes.len()).is_err() {
let mut builder = VerificationReport::builder();
builder = builder.with_bindings(
crate::verification::report::BindingVerification::builder()
.instance_digest_present(false)
.instance_digest_valid(false)
.build(),
);
return ManifestVerification {
report: builder.build(),
instance_digest_match: false,
manifest_valid: false,
embedded_reference_status: EmbeddedReferenceStatus::NotProvided,
};
}
}
verify_detached_manifest_inner(image_bytes, manifest, trust, payload_mac_key, &[])
}
#[allow(unused_variables)]
fn verify_detached_manifest_inner(
image_bytes: &[u8],
manifest: &DetachedManifest,
trust: &TrustPolicy,
payload_mac_key: Option<&[u8]>,
#[cfg(feature = "signatures")] caller_verifying_keys: &[TrustedVerifyingKey],
#[cfg(not(feature = "signatures"))] caller_verifying_keys: &[()],
) -> ManifestVerification {
let mut builder = VerificationReport::builder();
let manifest_valid = manifest.validate().is_ok();
if !manifest_valid {
let report = builder.build();
return ManifestVerification {
report,
instance_digest_match: false,
manifest_valid: false,
embedded_reference_status: EmbeddedReferenceStatus::NotProvided,
};
}
let mut hasher = Sha256::new();
hasher.update(image_bytes);
let image_hash = hasher.finalize();
let image_digest = format!("sha256:{}", hex::encode(image_hash));
let instance_digest_match = image_digest == manifest.claim.instance_digest;
let mut _any_signature_valid = false;
let mut _any_signature_trusted = false;
for sig_record in &manifest.signatures {
if sig_record.algorithm != "ed25519" {
builder = builder.add_signature(
SignatureVerification::builder()
.present(true)
.structurally_valid(false)
.source(FieldSource::DetachedManifest)
.build(),
);
continue;
}
let sig_bytes = match hex::decode(&sig_record.signature) {
Ok(b) => b,
Err(_) => {
builder = builder.add_signature(
SignatureVerification::builder()
.present(true)
.structurally_valid(false)
.source(FieldSource::DetachedManifest)
.build(),
);
continue;
}
};
let matching_key = manifest
.public_keys
.iter()
.find(|k| k.key_id == sig_record.key_id);
#[cfg(feature = "signatures")]
let trusted_vk: Option<&crate::signing::VerifyingKey> = caller_verifying_keys
.iter()
.find(|t| t.key_id == sig_record.key_id)
.map(|t| &t.key);
#[cfg(not(feature = "signatures"))]
let trusted_vk: Option<()> = None;
#[cfg(feature = "signatures")]
{
let (verify_key_bytes, key_material_matched): (Option<[u8; 32]>, bool) =
if let Some(vk) = trusted_vk {
let caller_bytes = *vk.as_bytes();
let matched = if let Some(entry) = matching_key {
if entry.algorithm != "ed25519" {
false
} else if let Ok(manifest_bytes) = hex::decode(&entry.key_bytes) {
manifest_bytes.as_slice() == caller_bytes.as_slice()
} else {
false
}
} else {
true
};
(Some(caller_bytes), matched)
} else if let Some(entry) = matching_key {
if entry.algorithm != "ed25519" {
(None, true)
} else if let Ok(pub_bytes_vec) = hex::decode(&entry.key_bytes) {
if pub_bytes_vec.len() == 32 {
let mut raw = [0u8; 32];
raw.copy_from_slice(&pub_bytes_vec);
(Some(raw), true)
} else {
(None, true)
}
} else {
(None, true)
}
} else {
(None, true)
};
if let Some(raw_pub) = verify_key_bytes {
let vk =
crate::signing::VerifyingKey::from_bytes(raw_pub, sig_record.key_id.clone());
let claim_bytes = manifest.claim.canonical_bytes();
let result = vk.verify(&claim_bytes, &sig_bytes);
let is_valid = result == crate::signing::SignatureResult::Valid;
_any_signature_valid = _any_signature_valid || is_valid;
let key_id_matched = match trust {
TrustPolicy::TrustNone => false,
TrustPolicy::TrustKeys(keys) => keys.iter().any(|t| t == &sig_record.key_id),
TrustPolicy::TrustCallback(f) => f(&sig_record.key_id),
} || {
#[cfg(feature = "signatures")]
{
caller_verifying_keys
.iter()
.any(|t| t.key_id == sig_record.key_id)
}
#[cfg(not(feature = "signatures"))]
{
false
}
};
let sig_trusted = key_id_matched && is_valid && key_material_matched;
_any_signature_trusted = _any_signature_trusted || sig_trusted;
builder = builder.add_signature(
SignatureVerification::builder()
.present(true)
.structurally_valid(true)
.cryptographically_valid(is_valid)
.public_key_id(sig_record.key_id.clone())
.key_id_matched(key_id_matched)
.key_material_matched(key_material_matched)
.trusted(sig_trusted)
.source(FieldSource::DetachedManifest)
.build(),
);
} else {
builder = builder.add_signature(
SignatureVerification::builder()
.present(true)
.structurally_valid(false)
.key_material_matched(key_material_matched)
.source(FieldSource::DetachedManifest)
.build(),
);
}
}
#[cfg(not(feature = "signatures"))]
{
builder = builder.add_signature(
SignatureVerification::builder()
.present(true)
.structurally_valid(true)
.cryptographically_valid(false)
.source(FieldSource::DetachedManifest)
.build(),
);
}
}
let overall_trusted = _any_signature_trusted;
if let Some(ref trust) = manifest.trust_metadata {
builder = builder.with_trust(
crate::verification::report::TrustEvaluation::builder()
.trust_model(&trust.trust_model)
.trusted(overall_trusted)
.reason(if overall_trusted {
"caller-trusted key produced valid signature"
} else {
"trust_metadata from manifest is informational only; no caller-trusted key produced a valid signature"
})
.build(),
);
} else if overall_trusted {
builder = builder.with_trust(
crate::verification::report::TrustEvaluation::builder()
.trust_model("caller")
.trusted(true)
.reason("caller-trusted key produced valid signature")
.build(),
);
}
let actual_format = crate::types::ImageOutputFormat::from_magic_bytes(image_bytes)
.map(|f| format!("{:?}", f).to_lowercase())
.unwrap_or_default();
let format_valid = actual_format == manifest.claim.format;
let (actual_width, actual_height) = match crate::util::image::load_image_from_bytes(image_bytes)
{
Ok(img) => img.dimensions(),
Err(_) => (0, 0),
};
let dimensions_valid =
actual_width == manifest.claim.width && actual_height == manifest.claim.height;
let file_size_valid = (image_bytes.len() as u64) == manifest.claim.file_size;
builder = builder.with_bindings(
crate::verification::report::BindingVerification::builder()
.instance_digest_present(!manifest.claim.instance_digest.is_empty())
.instance_digest_valid(instance_digest_match)
.format_valid(format_valid)
.dimensions_valid(dimensions_valid)
.file_size_valid(file_size_valid)
.build(),
);
let report = builder.build();
let embedded_reference_status = match &manifest.embedded_reference {
None => EmbeddedReferenceStatus::NotProvided,
Some(reference) => {
let extractor = crate::protected::steganography::SteganographyProtector::new();
let mac_key = payload_mac_key.unwrap_or(&[]);
let (status, raw_bytes) =
extractor.verify_and_extract_raw_from_bytes(image_bytes, mac_key);
match status {
crate::VerificationStatus::Verified => {
if let Some(payload) =
extractor.extract_payload_from_bytes_with_key(image_bytes, mac_key)
{
if payload.version() != reference.payload_version {
return ManifestVerification {
report,
instance_digest_match,
manifest_valid: true,
embedded_reference_status: EmbeddedReferenceStatus::VersionMismatch,
};
}
match payload.raw_payload() {
Some(raw) => {
let mut hasher = Sha256::new();
hasher.update(raw);
let actual_digest =
format!("sha256:{}", hex::encode(hasher.finalize()));
if actual_digest != reference.payload_digest {
EmbeddedReferenceStatus::DigestMismatch
} else {
EmbeddedReferenceStatus::PresentValid
}
}
None => EmbeddedReferenceStatus::Malformed,
}
} else {
EmbeddedReferenceStatus::Malformed
}
}
crate::VerificationStatus::Invalid => {
if let Some(raw) = raw_bytes {
if raw.len() > 30 && raw[0] == 0x53 && raw[1] == 0x45 {
let auth_algo = raw[29];
if auth_algo == 2 {
if mac_key.is_empty() {
EmbeddedReferenceStatus::AuthenticationKeyMissing
} else {
EmbeddedReferenceStatus::AuthenticationFailed
}
} else {
EmbeddedReferenceStatus::Malformed
}
} else {
EmbeddedReferenceStatus::Malformed
}
} else {
EmbeddedReferenceStatus::Stripped
}
}
crate::VerificationStatus::NotFound => EmbeddedReferenceStatus::Stripped,
}
}
};
ManifestVerification {
report,
instance_digest_match,
manifest_valid,
embedded_reference_status,
}
}
#[must_use]
pub fn verify_detached_manifest_with_keys(
image_bytes: &[u8],
manifest: &DetachedManifest,
expected_keys: Option<&[Vec<u8>]>,
) -> ManifestVerification {
verify_detached_manifest_with_keys_and_mac(image_bytes, manifest, expected_keys, None)
}
#[must_use]
pub fn verify_detached_manifest_with_keys_and_mac(
image_bytes: &[u8],
manifest: &DetachedManifest,
expected_keys: Option<&[Vec<u8>]>,
payload_mac_key: Option<&[u8]>,
) -> ManifestVerification {
let policy = match expected_keys {
Some(keys) => TrustPolicy::TrustKeys(keys.to_vec()),
None => TrustPolicy::TrustNone,
};
let limits = ResourceLimits::default();
verify_detached_manifest_with_limits_and_mac(
image_bytes,
manifest,
&policy,
Some(&limits),
payload_mac_key,
)
}
#[must_use]
pub fn verify_detached_manifest_with_options(
image_bytes: &[u8],
manifest: &DetachedManifest,
options: &DetachedVerificationOptions<'_>,
) -> ManifestVerification {
let default_limits = ResourceLimits::default();
let limits = options.limits.unwrap_or(&default_limits);
if limits.check_input_size(image_bytes.len()).is_err() {
let mut builder = VerificationReport::builder();
builder = builder.with_bindings(
crate::verification::report::BindingVerification::builder()
.instance_digest_present(false)
.instance_digest_valid(false)
.build(),
);
return ManifestVerification {
report: builder.build(),
instance_digest_match: false,
manifest_valid: false,
embedded_reference_status: EmbeddedReferenceStatus::NotProvided,
};
}
let default_policy = TrustPolicy::TrustNone;
let trust = options.trust_policy.unwrap_or(&default_policy);
#[cfg(feature = "signatures")]
{
verify_detached_manifest_inner(
image_bytes,
manifest,
trust,
options.payload_mac_key,
options.caller_verifying_keys,
)
}
#[cfg(not(feature = "signatures"))]
{
verify_detached_manifest_inner(image_bytes, manifest, trust, options.payload_mac_key, &[])
}
}