use std::path::Path;
use sigstore_trust_root::TrustedRoot;
use crate::model::{InvalidDocument, ReleaseListStatement, Scheme, Statement};
use crate::sigstore::{self, SignedBy, Trust};
#[derive(Debug, Clone, Copy)]
pub struct Options<'a> {
pub require_log: bool,
pub trusted_root: &'a TrustedRoot,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct Verified {
pub project: String,
pub version: String,
pub published_at: String,
pub scheme: Scheme,
pub key_id: String,
pub attested_by: crate::model::Attestor,
pub prerelease: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logged_at: Option<String>,
pub provenance_linked: bool,
pub checked_artifacts: Vec<String>,
pub artifact_count: usize,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub resources: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub requires: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedList {
pub list: ReleaseListStatement,
pub scheme: Scheme,
pub key_id: String,
pub issuer: Option<String>,
pub logged_at: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("statement is not valid JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("statement is invalid: {0}")]
Invalid(#[from] InvalidDocument),
#[error("statement declares scheme {declared} but the bundle was signed by {actual}")]
SchemeMismatch {
declared: Scheme,
actual: &'static str,
},
#[error("statement declares signer {declared:?}, the bundle was signed by {actual:?}")]
DeclaredSignerMismatch { declared: String, actual: String },
#[error("statement declares issuer {declared:?}, the certificate says {actual:?}")]
DeclaredIssuerMismatch { declared: String, actual: String },
#[error("{0}")]
Sigstore(#[from] sigstore::Error),
#[error("artifact {name}: {why}")]
Artifact { name: String, why: String },
}
fn check_declared(
identity: &crate::model::Identity,
signed_by: &SignedBy,
) -> Result<(Scheme, String, Option<String>), Error> {
match (identity.scheme, signed_by) {
(
Scheme::SigstoreOidc,
SignedBy::Identity {
identity: actual,
issuer,
},
) => {
if identity.key_id != *actual {
return Err(Error::DeclaredSignerMismatch {
declared: identity.key_id.clone(),
actual: actual.clone(),
});
}
if let Some(declared) = &identity.issuer
&& declared != issuer
{
return Err(Error::DeclaredIssuerMismatch {
declared: declared.clone(),
actual: issuer.clone(),
});
}
Ok((Scheme::SigstoreOidc, actual.clone(), Some(issuer.clone())))
}
(Scheme::SigstoreKey, SignedBy::Key { key_id }) => {
if !identity.key_id.eq_ignore_ascii_case(key_id) {
return Err(Error::DeclaredSignerMismatch {
declared: identity.key_id.clone(),
actual: key_id.clone(),
});
}
Ok((Scheme::SigstoreKey, key_id.clone(), None))
}
(declared, SignedBy::Identity { .. }) => Err(Error::SchemeMismatch {
declared,
actual: "an OIDC identity",
}),
(declared, SignedBy::Key { .. }) => Err(Error::SchemeMismatch {
declared,
actual: "a key",
}),
}
}
fn logged_at(integrated_time: Option<i64>) -> Option<String> {
integrated_time
.and_then(|t| jiff::Timestamp::from_second(t).ok())
.map(|t| t.to_string())
}
pub fn verify(
bundle: &str,
trust: &Trust<'_>,
options: Options<'_>,
artifacts: &[&Path],
) -> Result<Verified, Error> {
let verified = sigstore::verify(bundle, trust, options.require_log, options.trusted_root)?;
let statement: Statement = serde_json::from_slice(&verified.statement)?;
statement.validate()?;
let version = crate::model::parse_version(&statement.predicate.version)?;
let (scheme, key_id, issuer) =
check_declared(&statement.predicate.identity, &verified.signed_by)?;
let mut checked = Vec::new();
for path in artifacts {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.to_string();
let Some(expected) = statement.digest_of(&name) else {
return Err(Error::Artifact {
name,
why: "not listed in the document".into(),
});
};
let (actual, size) = crate::digest_file(path).map_err(|e| Error::Artifact {
name: name.clone(),
why: e.to_string(),
})?;
if actual != expected {
return Err(Error::Artifact {
name,
why: format!("sha256 is {actual}, document says {expected}"),
});
}
let declared_size = statement
.predicate
.artifacts
.iter()
.find(|a| a.name == name)
.map(|a| a.size);
if let Some(declared) = declared_size
&& declared != size
{
return Err(Error::Artifact {
name,
why: format!("size is {size}, document says {declared}"),
});
}
checked.push(name);
}
Ok(Verified {
project: statement.predicate.project.clone(),
version: statement.predicate.version.clone(),
published_at: statement.predicate.published_at.clone(),
scheme,
key_id,
attested_by: statement.predicate.attested_by,
prerelease: !version.pre.is_empty(),
channel: crate::model::channel(&version).map(str::to_string),
issuer,
logged_at: logged_at(verified.integrated_time),
provenance_linked: statement.provenance_linked(),
checked_artifacts: checked,
artifact_count: statement.predicate.artifacts.len(),
resources: statement
.predicate
.resources
.iter()
.map(|r| {
format!(
"{} ({})",
r.label(),
r.source().map_or("?".to_string(), |s| s.to_string())
)
})
.collect(),
requires: statement
.predicate
.artifacts
.iter()
.filter_map(|a| {
let requires = a.requires.as_ref().filter(|r| !r.is_empty())?;
Some(format!("{}: {}", a.name, requires.summary()))
})
.collect(),
})
}
pub fn verify_release_list(
bundle: &str,
trust: &Trust<'_>,
options: Options<'_>,
) -> Result<VerifiedList, Error> {
let verified = sigstore::verify(bundle, trust, options.require_log, options.trusted_root)?;
let list: ReleaseListStatement = serde_json::from_slice(&verified.statement)?;
list.validate()?;
let (scheme, key_id, issuer) = check_declared(&list.predicate.identity, &verified.signed_by)?;
Ok(VerifiedList {
list,
scheme,
key_id,
issuer,
logged_at: logged_at(verified.integrated_time),
})
}