use crate::artifact::{ArtifactRequirements, PreparedArtifact};
use crate::error::{Error, Result};
use crate::verify_impl::helpers::SigningCertificateChain;
use sigstore_bundle::validate_bundle_with_options;
use sigstore_bundle::ValidationOptions;
use sigstore_crypto::{parse_certificate_info, KeyAlgorithm, SigningScheme, SubjectAltName};
use sigstore_trust_root::TrustedRoot;
use sigstore_types::bundle::VerificationMaterialContent;
use sigstore_types::{Artifact, Bundle, KindVersion, SignatureContent};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CertificatePolicy {
Skip,
#[non_exhaustive]
Verify {
verify_sct: bool,
},
}
#[derive(Debug, Clone)]
pub struct PublicKeyVerificationPolicy {
public_key: sigstore_types::DerPublicKey,
verify_tlog: bool,
}
impl PublicKeyVerificationPolicy {
pub fn new(public_key: sigstore_types::DerPublicKey) -> Self {
Self {
public_key,
verify_tlog: true,
}
}
pub fn skip_tlog_unsafe(mut self) -> Self {
self.verify_tlog = false;
self
}
pub fn public_key(&self) -> &sigstore_types::DerPublicKey {
&self.public_key
}
pub fn verifies_tlog(&self) -> bool {
self.verify_tlog
}
}
pub trait Policy: sealed::Sealed {}
impl Policy for VerificationPolicy {}
impl Policy for PublicKeyVerificationPolicy {}
mod sealed {
pub trait Sealed {
fn kind(&self) -> PolicyKind<'_>;
}
pub enum PolicyKind<'a> {
Certificate(&'a super::VerificationPolicy),
PublicKey(&'a super::PublicKeyVerificationPolicy),
}
impl Sealed for super::VerificationPolicy {
fn kind(&self) -> PolicyKind<'_> {
PolicyKind::Certificate(self)
}
}
impl Sealed for super::PublicKeyVerificationPolicy {
fn kind(&self) -> PolicyKind<'_> {
PolicyKind::PublicKey(self)
}
}
}
use sealed::PolicyKind;
enum Prepared<'a> {
Certificate {
policy: &'a VerificationPolicy,
cert_info: Box<sigstore_crypto::CertificateInfo>,
},
PublicKey {
policy: &'a PublicKeyVerificationPolicy,
scheme: SigningScheme,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum IdentityMatcher {
Exact(String),
Email(String),
Uri(String),
}
impl IdentityMatcher {
pub fn matches(&self, identity: &SubjectAltName) -> bool {
match (self, identity) {
(Self::Exact(expected), actual) => actual.as_str() == expected,
(Self::Email(expected), SubjectAltName::Email(actual)) => actual == expected,
(Self::Uri(expected), SubjectAltName::Uri(actual)) => actual == expected,
_ => false,
}
}
}
impl std::fmt::Display for IdentityMatcher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Exact(value) => f.write_str(value),
Self::Email(value) => write!(f, "email {value}"),
Self::Uri(value) => write!(f, "URI {value}"),
}
}
}
impl From<String> for IdentityMatcher {
fn from(identity: String) -> Self {
Self::Exact(identity)
}
}
impl From<&str> for IdentityMatcher {
fn from(identity: &str) -> Self {
Self::Exact(identity.to_string())
}
}
impl From<&String> for IdentityMatcher {
fn from(identity: &String) -> Self {
Self::Exact(identity.clone())
}
}
impl From<SubjectAltName> for IdentityMatcher {
fn from(identity: SubjectAltName) -> Self {
match identity {
SubjectAltName::Email(email) => Self::Email(email),
SubjectAltName::Uri(uri) => Self::Uri(uri),
other => Self::Exact(other.as_str().to_string()),
}
}
}
impl From<&SubjectAltName> for IdentityMatcher {
fn from(identity: &SubjectAltName) -> Self {
identity.clone().into()
}
}
#[derive(Debug, Clone)]
pub struct VerificationPolicy {
identity: Option<IdentityMatcher>,
issuer: Option<String>,
verify_tlog: bool,
certificate: CertificatePolicy,
}
impl VerificationPolicy {
pub fn new(identity: impl Into<IdentityMatcher>, issuer: impl Into<String>) -> Self {
Self::any_identity()
.require_identity(identity)
.require_issuer(issuer)
}
pub fn any_identity() -> Self {
Self {
identity: None,
issuer: None,
verify_tlog: true,
certificate: CertificatePolicy::Verify { verify_sct: true },
}
}
pub fn require_identity(mut self, identity: impl Into<IdentityMatcher>) -> Self {
self.identity = Some(identity.into());
self
}
pub fn require_issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
pub fn skip_tlog_unsafe(mut self) -> Self {
self.verify_tlog = false;
self
}
pub fn skip_certificate_chain(mut self) -> Self {
self.certificate = CertificatePolicy::Skip;
self
}
pub fn skip_sct(mut self) -> Self {
if let CertificatePolicy::Verify { verify_sct } = &mut self.certificate {
*verify_sct = false;
}
self
}
pub fn identity(&self) -> Option<&IdentityMatcher> {
self.identity.as_ref()
}
pub fn issuer(&self) -> Option<&str> {
self.issuer.as_deref()
}
pub fn verifies_tlog(&self) -> bool {
self.verify_tlog
}
pub fn certificate(&self) -> &CertificatePolicy {
&self.certificate
}
}
#[derive(Debug)]
pub struct VerificationResult {
identity: Option<SubjectAltName>,
issuer: Option<String>,
certificate: Option<sigstore_crypto::CertificateInfo>,
integrated_time: Option<jiff::Timestamp>,
certificate_verified: bool,
sct_verified: bool,
tlog_verified: bool,
identity_policy_checked: bool,
verified_timestamps: Vec<jiff::Timestamp>,
}
impl VerificationResult {
fn new() -> Self {
Self {
identity: None,
issuer: None,
certificate: None,
integrated_time: None,
certificate_verified: false,
sct_verified: false,
tlog_verified: false,
identity_policy_checked: false,
verified_timestamps: Vec::new(),
}
}
pub fn identity(&self) -> Option<&SubjectAltName> {
self.identity.as_ref()
}
pub fn issuer(&self) -> Option<&str> {
self.issuer.as_deref()
}
pub fn certificate(&self) -> Option<&sigstore_crypto::CertificateInfo> {
self.certificate.as_ref()
}
pub fn integrated_time(&self) -> Option<jiff::Timestamp> {
self.integrated_time
}
pub fn certificate_verified(&self) -> bool {
self.certificate_verified
}
pub fn sct_verified(&self) -> bool {
self.sct_verified
}
pub fn tlog_verified(&self) -> bool {
self.tlog_verified
}
pub fn identity_policy_checked(&self) -> bool {
self.identity_policy_checked
}
pub fn verified_timestamps(&self) -> &[jiff::Timestamp] {
&self.verified_timestamps
}
}
pub struct Verifier {
trusted_root: TrustedRoot,
rekor_keys: sigstore_crypto::Keyring,
pub(crate) ct_keys: Vec<(SigningScheme, sigstore_crypto::Keyring)>,
pub(crate) fulcio_anchors: Vec<crate::verify_impl::helpers::FulcioAnchor>,
}
impl Verifier {
pub fn new(trusted_root: &TrustedRoot) -> Result<Self> {
use sigstore_crypto::{Keyring, VerificationKey};
use sigstore_types::Sha256Hash;
let mut ids = std::collections::HashSet::new();
let logs = trusted_root
.tlogs
.iter()
.map(|log| (true, &log.log_id, &log.public_key))
.chain(
trusted_root
.ctlogs
.iter()
.map(|log| (false, &log.log_id, &log.public_key)),
);
for (is_rekor, id, public_key) in logs {
validate_trust_window(public_key.valid_for)?;
let id = Sha256Hash::try_from(id.key_id.as_bytes())?;
if !ids.insert((is_rekor, id)) {
return Err(Error::InvalidTrustRoot(format!(
"duplicate trusted log ID: {}",
id.to_hex()
)));
}
if is_rekor {
VerificationKey::from_spki(&public_key.raw_bytes)?;
} else {
VerificationKey::from_spki(&public_key.raw_bytes).or_else(|_| {
VerificationKey::from_der(&public_key.raw_bytes, SigningScheme::RsaPkcs1Sha256)
})?;
}
}
let rekor_keys = trusted_root.rekor_keys()?;
let ct_keys: Vec<(SigningScheme, Keyring)> = [
SigningScheme::EcdsaP256Sha256,
SigningScheme::EcdsaP384Sha384,
SigningScheme::RsaPkcs1Sha256,
SigningScheme::RsaPkcs1Sha384,
SigningScheme::RsaPkcs1Sha512,
]
.into_iter()
.map(|scheme| Ok((scheme, trusted_root.ctfe_keys(scheme)?)))
.collect::<Result<_>>()?;
let mut fulcio_anchors = Vec::new();
let authorities = trusted_root
.certificate_authorities
.iter()
.map(|ca| (true, &ca.cert_chain, ca.valid_for))
.chain(
trusted_root
.timestamp_authorities
.iter()
.map(|tsa| (false, &tsa.cert_chain, tsa.valid_for)),
);
for (is_fulcio, chain, window) in authorities {
validate_trust_window(window)?;
if chain.certificates.is_empty() {
return Err(Error::InvalidTrustRoot(
"trusted authority has an empty certificate chain".into(),
));
}
for cert in &chain.certificates {
let der = rustls_pki_types::CertificateDer::from(cert.raw_bytes.as_bytes());
let anchor = webpki::anchor_from_trusted_cert(&der)
.map_err(|e| Error::TrustedCertificate(Box::new(e)))?;
if is_fulcio {
fulcio_anchors.push((anchor.to_owned(), window));
}
}
}
Ok(Self {
trusted_root: trusted_root.clone(),
rekor_keys,
ct_keys,
fulcio_anchors,
})
}
pub fn verify<'a>(
&self,
artifact: impl Into<Artifact<'a>>,
bundle: &Bundle,
policy: &(impl Policy + ?Sized),
) -> Result<VerificationResult> {
let (prepared, requirements) = prepare(bundle, policy.kind())?;
self.verify_prepared(
PreparedArtifact::from_artifact(artifact.into(), &requirements)?,
bundle,
prepared,
&requirements,
)
}
pub fn verify_reader(
&self,
reader: impl std::io::Read,
bundle: &Bundle,
policy: &(impl Policy + ?Sized),
) -> Result<VerificationResult> {
let (prepared, requirements) = prepare(bundle, policy.kind())?;
self.verify_prepared(
PreparedArtifact::from_reader(reader, &requirements)?,
bundle,
prepared,
&requirements,
)
}
pub async fn verify_async_reader(
&self,
reader: impl futures_io::AsyncRead + Unpin,
bundle: &Bundle,
policy: &(impl Policy + ?Sized),
) -> Result<VerificationResult> {
let (prepared, requirements) = prepare(bundle, policy.kind())?;
self.verify_prepared(
PreparedArtifact::from_async_reader(reader, &requirements).await?,
bundle,
prepared,
&requirements,
)
}
fn verify_prepared(
&self,
artifact: PreparedArtifact<'_>,
bundle: &Bundle,
prepared: Prepared<'_>,
requirements: &ArtifactRequirements,
) -> Result<VerificationResult> {
match prepared {
Prepared::Certificate { policy, cert_info } => {
self.verify_certificate(artifact, bundle, policy, &cert_info, requirements)
}
Prepared::PublicKey { policy, scheme } => {
self.verify_public_key(artifact, bundle, policy, scheme, requirements)
}
}
}
fn verify_certificate(
&self,
artifact: PreparedArtifact<'_>,
bundle: &Bundle,
policy: &VerificationPolicy,
cert_info: &sigstore_crypto::CertificateInfo,
requirements: &ArtifactRequirements,
) -> Result<VerificationResult> {
let mut result = VerificationResult::new();
let cert = bundle
.signing_certificate()
.ok_or_else(|| Error::InvalidBundle("bundle has no signing certificate".into()))?;
result.identity = cert_info.identity.clone();
result.issuer = cert_info.issuer.clone();
result.certificate = Some(cert_info.clone());
let signature = crate::verify_impl::helpers::extract_signature(&bundle.content)?;
let validation_times = crate::verify_impl::helpers::determine_validation_times(
bundle,
&signature,
&self.trusted_root,
&self.rekor_keys,
)?;
if let CertificatePolicy::Verify { verify_sct } = policy.certificate {
let chain =
SigningCertificateChain::from_material(&bundle.verification_material.content)
.ok_or_else(|| {
Error::InvalidBundle("bundle has no signing certificate".into())
})?;
let mut issuer_spki = None;
for &validation_time in &validation_times {
issuer_spki = Some(crate::verify_impl::helpers::verify_certificate_chain(
chain,
validation_time,
&self.fulcio_anchors,
)?);
crate::verify_impl::helpers::validate_certificate_time(validation_time, cert_info)?;
}
let Some(issuer_spki) = issuer_spki else {
return Err(Error::no_verified_timestamp(
"no verified timestamp to validate the signing certificate against".to_string(),
));
};
if verify_sct {
crate::verify_impl::sct::verify_sct(
cert.as_bytes(),
issuer_spki.as_bytes(),
&self.ct_keys,
)?;
result.sct_verified = true;
}
result.certificate_verified = true;
}
result.verified_timestamps = validation_times;
if let Some(expected) = &policy.identity {
if !result
.identity
.as_ref()
.is_some_and(|actual| expected.matches(actual))
{
return Err(Error::IdentityMismatch {
expected: expected.clone(),
actual: result.identity.clone(),
});
}
}
if let Some(expected) = &policy.issuer {
if result.issuer.as_ref() != Some(expected) {
return Err(Error::IssuerMismatch {
expected: expected.clone(),
actual: result.issuer.clone(),
});
}
}
result.identity_policy_checked = policy.identity.is_some() || policy.issuer.is_some();
if policy.verify_tlog {
let integrated_time = crate::verify_impl::tlog::verify_tlog_entries(
bundle,
&self.rekor_keys,
cert_info.not_before,
cert_info.not_after,
)?;
result.tlog_verified = true;
if let Some(time) = integrated_time {
result.integrated_time = Some(time);
}
}
if let SignatureContent::DsseEnvelope(envelope) = &bundle.content {
verify_dsse_envelope_signature(
envelope,
&cert_info.public_key,
cert_info.key_algorithm.default_signing_scheme(),
)?;
requirements.verify_binding(&artifact)?;
}
if let SignatureContent::MessageSignature(msg_sig) = &bundle.content {
verify_message_digest_binding(msg_sig, &artifact)?;
verify_message_signature_crypto(cert_info, msg_sig, &artifact)?;
}
crate::verify_impl::verify_tlog_consistency(bundle, &artifact)?;
Ok(result)
}
fn verify_public_key(
&self,
artifact: PreparedArtifact<'_>,
bundle: &Bundle,
policy: &PublicKeyVerificationPolicy,
signing_scheme: SigningScheme,
requirements: &ArtifactRequirements,
) -> Result<VerificationResult> {
let public_key = &policy.public_key;
let mut result = VerificationResult::new();
if policy.verify_tlog {
for entry in &bundle.verification_material.tlog_entries {
crate::verify_impl::tlog::verify_entry_inclusion(entry, &self.rekor_keys)?;
let is_rekor_v1 = matches!(
entry.kind_version,
KindVersion::HashedRekordV001 | KindVersion::DsseV001 | KindVersion::IntotoV002
);
if is_rekor_v1 && entry.inclusion_promise.is_some() {
if let Some(time) = entry.integrated_time {
crate::verify_impl::tlog::validate_integrated_time_not_in_future(
time,
jiff::Timestamp::now(),
)?;
result.integrated_time = Some(crate::verify_impl::tlog::earliest(
result.integrated_time,
time,
));
result.verified_timestamps.push(time);
}
}
}
result.tlog_verified = true;
}
let signature = crate::verify_impl::helpers::extract_signature(&bundle.content)?;
result
.verified_timestamps
.extend(crate::verify_impl::helpers::extract_tsa_timestamps(
bundle,
&signature,
&self.trusted_root,
)?);
match &bundle.content {
SignatureContent::MessageSignature(msg_sig) => {
verify_message_digest_binding(msg_sig, &artifact)?;
verify_signature_over_artifact(
public_key,
signing_scheme,
&msg_sig.signature,
&artifact,
)?;
}
SignatureContent::DsseEnvelope(envelope) => {
verify_dsse_envelope_signature(envelope, public_key, signing_scheme)?;
requirements.verify_binding(&artifact)?;
}
_ => {
return Err(Error::UnsupportedBundle(
"unsupported bundle signature content".to_string(),
))
}
}
crate::verify_impl::rekor::verify_tlog_consistency_with_key(
bundle,
&artifact,
Some(public_key),
)?;
Ok(result)
}
}
fn validate_trust_window(window: Option<sigstore_types::TimeRange>) -> Result<()> {
if window.is_some_and(|range| range.end.is_some_and(|end| end < range.start)) {
return Err(Error::InvalidTrustRoot(
"trusted validity window ends before it starts".into(),
));
}
Ok(())
}
fn verify_message_digest_binding(
msg_sig: &sigstore_types::bundle::MessageSignature,
artifact: &PreparedArtifact<'_>,
) -> Result<()> {
if let Some(digest) = &msg_sig.message_digest {
let artifact_digest = artifact.digest(digest.algorithm)?;
if digest.digest != artifact_digest.as_bytes() {
return Err(Error::ArtifactMismatch(
"message digest in bundle does not match artifact hash".to_string(),
));
}
}
Ok(())
}
fn verify_dsse_envelope_signature(
envelope: &sigstore_types::DsseEnvelope,
public_key: &sigstore_types::DerPublicKey,
scheme: SigningScheme,
) -> Result<()> {
let pae = envelope.pae();
sigstore_crypto::verify_signature(public_key, &pae, &envelope.signature.sig, scheme)
.map_err(|e| Error::SignatureInvalid(format!("DSSE signature verification failed: {}", e)))
}
fn verify_signature_over_artifact(
public_key: &sigstore_types::DerPublicKey,
scheme: SigningScheme,
signature: &sigstore_types::SignatureBytes,
artifact: &PreparedArtifact<'_>,
) -> Result<()> {
let result = if let Some(algorithm) = scheme.hash_algorithm() {
let digest = artifact.digest(algorithm)?;
sigstore_crypto::verify_signature_prehashed(
public_key,
digest.as_bytes(),
signature,
scheme,
)
} else if let Some(blob) = artifact.blob() {
sigstore_crypto::verify_signature(public_key, blob, signature, scheme)
} else {
return Err(Error::UnsupportedArtifact(format!("cannot verify signature from a digest or reader - scheme {} does not support prehashed mode", scheme.name())));
};
result.map_err(|e| Error::SignatureInvalid(format!("signature verification failed: {}", e)))
}
fn signing_scheme_for_message_signature(
key_algorithm: KeyAlgorithm,
msg_sig: &sigstore_types::bundle::MessageSignature,
) -> Result<SigningScheme> {
match &msg_sig.message_digest {
Some(digest) => Ok(key_algorithm.resolve_signing_scheme(digest.algorithm)?),
None => Ok(key_algorithm.default_signing_scheme()),
}
}
fn signing_scheme_for_content(
key_algorithm: KeyAlgorithm,
content: &SignatureContent,
) -> Result<SigningScheme> {
match content {
SignatureContent::MessageSignature(msg_sig) => {
signing_scheme_for_message_signature(key_algorithm, msg_sig)
}
SignatureContent::DsseEnvelope(_) => Ok(key_algorithm.default_signing_scheme()),
_ => Err(Error::UnsupportedBundle(
"unsupported bundle signature content".to_string(),
)),
}
}
fn validate_structure(bundle: &Bundle, verify_tlog: bool) -> Result<()> {
validate_bundle_with_options(
bundle,
&ValidationOptions::new()
.with_require_inclusion_proof(verify_tlog)
.with_require_timestamp(false),
)
.map_err(Error::Bundle)
}
fn prepare_certificate(
bundle: &Bundle,
policy: &VerificationPolicy,
) -> Result<sigstore_crypto::CertificateInfo> {
validate_structure(bundle, policy.verify_tlog)?;
let cert = bundle
.signing_certificate()
.ok_or_else(|| Error::InvalidBundle("bundle has no signing certificate".into()))?;
parse_certificate_info(cert)
.map_err(|e| Error::cert_malformed(format!("failed to parse certificate: {e}")))
}
fn prepare_public_key(
bundle: &Bundle,
policy: &PublicKeyVerificationPolicy,
) -> Result<SigningScheme> {
if !matches!(
bundle.verification_material.content,
VerificationMaterialContent::PublicKey(_)
) {
return Err(Error::UnsupportedBundle(
"bundle contains a certificate but public-key verification was requested".into(),
));
}
validate_structure(bundle, policy.verify_tlog)?;
signing_scheme_for_content(
KeyAlgorithm::from_spki(&policy.public_key)?,
&bundle.content,
)
}
fn prepare<'a>(
bundle: &Bundle,
policy: PolicyKind<'a>,
) -> Result<(Prepared<'a>, ArtifactRequirements)> {
let (prepared, scheme) = match policy {
PolicyKind::Certificate(policy) => {
let cert_info = prepare_certificate(bundle, policy)?;
let scheme = signing_scheme_for_content(cert_info.key_algorithm, &bundle.content)?;
(
Prepared::Certificate {
policy,
cert_info: Box::new(cert_info),
},
scheme,
)
}
PolicyKind::PublicKey(policy) => {
let scheme = prepare_public_key(bundle, policy)?;
(Prepared::PublicKey { policy, scheme }, scheme)
}
};
let requirements = ArtifactRequirements::new(&bundle.content, scheme)?;
Ok((prepared, requirements))
}
fn verify_message_signature_crypto(
cert_info: &sigstore_crypto::CertificateInfo,
msg_sig: &sigstore_types::bundle::MessageSignature,
artifact: &PreparedArtifact<'_>,
) -> Result<()> {
verify_signature_over_artifact(
&cert_info.public_key,
signing_scheme_for_message_signature(cert_info.key_algorithm, msg_sig)?,
&msg_sig.signature,
artifact,
)
}
pub fn verify<'a>(
artifact: impl Into<Artifact<'a>>,
bundle: &Bundle,
policy: &(impl Policy + ?Sized),
trusted_root: &TrustedRoot,
) -> Result<VerificationResult> {
Verifier::new(trusted_root)?.verify(artifact, bundle, policy)
}
#[cfg(test)]
mod tests {
use super::*;
use sigstore_types::HashAlgorithm;
#[test]
fn public_key_policy_defaults_to_tlog_verification() {
let key = sigstore_types::DerPublicKey::new(Vec::new());
let policy = PublicKeyVerificationPolicy::new(key);
assert!(policy.verify_tlog);
assert!(!policy.skip_tlog_unsafe().verify_tlog);
}
#[test]
fn test_verification_policy_default() {
let policy = VerificationPolicy::any_identity();
assert!(policy.verify_tlog);
assert_eq!(
policy.certificate,
CertificatePolicy::Verify { verify_sct: true }
);
}
#[test]
fn test_verification_policy_builder() {
let policy = VerificationPolicy::any_identity()
.require_identity("test@example.com")
.require_issuer("https://accounts.google.com")
.skip_tlog_unsafe();
assert_eq!(
policy.identity(),
Some(&IdentityMatcher::Exact("test@example.com".to_string()))
);
assert_eq!(policy.issuer(), Some("https://accounts.google.com"));
assert!(!policy.verifies_tlog());
}
#[test]
fn test_identity_matcher_kinds() {
let email = SubjectAltName::Email("a@example.com".to_string());
let uri = SubjectAltName::Uri("a@example.com".to_string());
let exact = IdentityMatcher::from("a@example.com");
assert!(exact.matches(&email) && exact.matches(&uri));
let typed = IdentityMatcher::Email("a@example.com".to_string());
assert!(typed.matches(&email));
assert!(!typed.matches(&uri));
assert!(!IdentityMatcher::Uri("b".to_string()).matches(&uri));
}
#[test]
fn test_skip_sct_keeps_certificate_chain_verification() {
let policy = VerificationPolicy::any_identity().skip_sct();
assert_eq!(
policy.certificate,
CertificatePolicy::Verify { verify_sct: false }
);
}
#[test]
fn test_skip_certificate_chain_preserves_legacy_sct_skip() {
let policy = VerificationPolicy::any_identity().skip_certificate_chain();
assert_eq!(policy.certificate, CertificatePolicy::Skip);
}
#[test]
fn test_signing_scheme_follows_message_digest_algorithm() {
let msg_sig = sigstore_types::bundle::MessageSignature::new(
sigstore_types::SignatureBytes::from_bytes(b"sig"),
)
.with_message_digest(sigstore_types::bundle::MessageDigest::new(
HashAlgorithm::Sha2384,
sigstore_types::DigestBytes::new(vec![0; 48]),
));
assert_eq!(
signing_scheme_for_message_signature(KeyAlgorithm::EcdsaP256, &msg_sig).unwrap(),
SigningScheme::EcdsaP256Sha384
);
}
fn unused_signature() -> sigstore_types::DsseSignature {
sigstore_types::DsseSignature::new(
sigstore_types::SignatureBytes::from_bytes(b"unused"),
sigstore_types::KeyId::default(),
)
}
fn in_toto_envelope(payload: &str) -> sigstore_types::DsseEnvelope {
sigstore_types::DsseEnvelope::new(
"application/vnd.in-toto+json",
sigstore_types::PayloadBytes::from_bytes(payload.as_bytes()),
unused_signature(),
)
}
fn statement_with_subject_sha256(hash_hex: &str) -> String {
format!(
r#"{{"_type":"https://in-toto.io/Statement/v1","subject":[{{"name":"artifact","digest":{{"sha256":"{}"}}}}],"predicateType":"https://example.com/predicate/v1","predicate":{{}}}}"#,
hash_hex
)
}
#[test]
fn statement_requirements_fail_closed_and_bind_blob() {
let content = SignatureContent::DsseEnvelope(in_toto_envelope(
&statement_with_subject_sha256(&sigstore_crypto::sha256(b"hello").to_hex()),
));
let requirements =
ArtifactRequirements::new(&content, SigningScheme::EcdsaP256Sha256).unwrap();
for (bytes, matches) in [(b"hello".as_slice(), true), (b"wrong".as_slice(), false)] {
let artifact = PreparedArtifact::from_artifact(bytes.into(), &requirements).unwrap();
assert_eq!(requirements.verify_binding(&artifact).is_ok(), matches);
}
for payload in [
"not json",
r#"{"_type":"https://in-toto.io/Statement/v1","subject":[],"predicateType":"p","predicate":{}}"#,
] {
let content = SignatureContent::DsseEnvelope(in_toto_envelope(payload));
assert!(ArtifactRequirements::new(&content, SigningScheme::EcdsaP256Sha256).is_err());
}
let mut envelope = in_toto_envelope("{}");
envelope.payload_type = "unknown".into();
assert!(ArtifactRequirements::new(
&SignatureContent::DsseEnvelope(envelope),
SigningScheme::EcdsaP256Sha256
)
.is_err());
}
#[test]
fn statement_without_predicate_binds_blob() {
let hash = sigstore_crypto::sha256(b"hello").to_hex();
let statement = format!(
r#"{{"_type":"https://in-toto.io/Statement/v1","subject":[{{"name":"artifact","digest":{{"sha256":"{hash}"}}}}],"predicateType":"https://example.com/predicate/v1"}}"#
);
let content = SignatureContent::DsseEnvelope(in_toto_envelope(&statement));
let requirements =
ArtifactRequirements::new(&content, SigningScheme::EcdsaP256Sha256).unwrap();
let artifact =
PreparedArtifact::from_artifact(b"hello".as_slice().into(), &requirements).unwrap();
assert!(requirements.verify_binding(&artifact).is_ok());
}
const DSSE_TEST_PAYLOAD: &[u8] = br#"{"hello":"world"}"#;
fn dsse_envelope_signed_over(
keypair: &sigstore_crypto::KeyPair,
data: &[u8],
) -> sigstore_types::DsseEnvelope {
sigstore_types::DsseEnvelope::new(
"application/vnd.in-toto+json",
sigstore_types::PayloadBytes::from_bytes(DSSE_TEST_PAYLOAD),
sigstore_types::DsseSignature::new(
keypair.sign(data).unwrap(),
sigstore_types::KeyId::default(),
),
)
}
#[test]
fn test_dsse_envelope_valid_signature_verifies() {
let keypair = sigstore_crypto::KeyPair::generate_ecdsa_p256().unwrap();
let pae = sigstore_types::pae("application/vnd.in-toto+json", DSSE_TEST_PAYLOAD);
let envelope = dsse_envelope_signed_over(&keypair, &pae);
let public_key = keypair.public_key_der().unwrap();
verify_dsse_envelope_signature(&envelope, &public_key, keypair.default_scheme())
.expect("an envelope with a valid signature must verify");
}
#[test]
fn test_dsse_envelope_invalid_signature_is_hard_error() {
let keypair = sigstore_crypto::KeyPair::generate_ecdsa_p256().unwrap();
let envelope = dsse_envelope_signed_over(&keypair, b"not the PAE");
let public_key = keypair.public_key_der().unwrap();
let err = verify_dsse_envelope_signature(&envelope, &public_key, keypair.default_scheme())
.expect_err("an invalid signature must fail verification");
assert!(
err.to_string()
.contains("DSSE signature verification failed"),
"unexpected error: {err}"
);
}
}