use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const ENVELOPE_FORMAT: &str = "nautalid-image-trust-envelope-v1";
pub const ENVELOPE_FORMAT_LEGACY: &str = "artifact-envelope-v1";
pub const SIG_ALG_P256: &str = "ecdsa-p256-sha256";
#[must_use]
pub fn envelope_format_supported(format: &str) -> bool {
format == ENVELOPE_FORMAT || format == ENVELOPE_FORMAT_LEGACY
}
#[derive(Debug, Error)]
pub enum ArtifactVerifyError {
#[error("IO error on {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("JSON: {0}")]
Json(#[from] serde_json::Error),
#[error(
"missing public key: set IMAGE_TRUST_PUBLIC_KEY_PEM / IMAGE_TRUST_PUBLIC_KEY_PATH (or ARTIFACT_VERIFY_* aliases)"
)]
MissingPublicKey,
#[error("unsupported envelope format: {0}")]
UnsupportedFormat(String),
#[error("no supported ECDSA P-256 signature verified")]
NoValidSignature,
#[error("cryptographic error: {0}")]
Crypto(String),
#[error("unknown artifact name: {name}")]
UnknownArtifact { name: String },
#[error("digest mismatch for {name} at {path}: expected {expected}, got {actual}")]
DigestMismatch {
name: String,
path: String,
expected: String,
actual: String,
},
#[error(
"runtime OCI image digest mismatch: manifest expects {expected}, environment has {actual}"
)]
RuntimeImageDigestMismatch { expected: String, actual: String },
#[error(
"manifest requires runtime_image_digest_sha256 but IMAGE_TRUST_RUNTIME_DIGEST (or CONTAINER_IMAGE_DIGEST) is not set"
)]
MissingRuntimeDigestEnv,
#[error("unsafe artifact path `{path}`: {reason}")]
UnsafePath { path: String, reason: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
pub format: String,
pub payload: ArtifactPayload,
pub signatures: Vec<SignatureRecord>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ImageTrustClaims {
#[serde(skip_serializing_if = "Option::is_none")]
pub runtime_image_digest_sha256: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub runtime_oci_reference: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wasm_image_digest_sha256: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub web_dom_bundle_digest_sha256: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub policy_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactPayload {
pub manifest_version: u32,
pub artifacts: Vec<ArtifactEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_trust: Option<ImageTrustClaims>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ArtifactKind {
Native,
Wasm,
ContainerImage,
OciLayer,
File,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactEntry {
pub name: String,
#[serde(rename = "type")]
pub kind: ArtifactKind,
pub sha256: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignatureRecord {
pub algorithm: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub public_key_id: Option<String>,
pub signature_der_base64: String,
}
impl ArtifactPayload {
pub fn sort_artifacts(&mut self) {
self.artifacts.sort_by(|a, b| a.name.cmp(&b.name));
}
pub fn signing_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
let mut p = self.clone();
p.sort_artifacts();
serde_json::to_vec(&p)
}
}
pub fn normalize_sha256_hex(input: &str) -> Option<String> {
let s = input.trim();
let hex_part = s.strip_prefix("sha256:").unwrap_or(s);
if hex_part.len() != 64 || !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
Some(hex_part.to_ascii_lowercase())
}