Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ use sigstore_trust_root::TrustedRoot;

// Load the trusted root via TUF (recommended - ensures up-to-date trust material)
let root = TrustedRoot::production().await?;
let verifier = Verifier::new(&root);
let verifier = Verifier::new(&root)?;

// Parse the bundle (contains signature, certificate, transparency log entry)
let bundle: sigstore_types::Bundle = serde_json::from_str(&bundle_json)?;
Expand Down
10 changes: 9 additions & 1 deletion crates/sigstore-verify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ This crate provides high-level APIs for verifying Sigstore signatures. It handle
5. Verify timestamps if present
6. Check identity against policy (optional)

## Verifier construction

`Verifier::new(&root)?` prepares Rekor/CT keyrings and Fulcio trust anchors before
reading artifacts. Invalid certificates, keys, duplicate log IDs and reversed
validity windows are errors at construction. Unsupported configured keys must be
removed explicitly rather than silently ignored. Key/authority activation times
are still checked when verifying, so long-lived verifiers do not freeze time.

## Verification results

`VerificationResult` is created only by successful verification and exposes
Expand Down Expand Up @@ -61,7 +69,7 @@ let digest = Sha256Hash::from_hex("b94d27b9...")?;
let result = verify(digest, &bundle, &policy, &root)?;

// Or use a Verifier directly; it also offers the same inputs
let verifier = Verifier::new(&root);
let verifier = Verifier::new(&root)?;
let result = verifier.verify(artifact_bytes.as_slice(), &bundle, &policy)?;

// Stream a large artifact in constant memory
Expand Down
5 changes: 4 additions & 1 deletion crates/sigstore-verify/examples/verify_bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,10 @@ async fn main() {
}

// Verify
let verifier = Verifier::new(&trusted_root);
let verifier = Verifier::new(&trusted_root).unwrap_or_else(|e| {
eprintln!("Error preparing trusted root: {e}");
process::exit(2);
});
let result = if is_digest {
// Parse digest (sha256:hex...)
let hex_digest = artifact_or_digest.strip_prefix("sha256:").unwrap();
Expand Down
8 changes: 8 additions & 0 deletions crates/sigstore-verify/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ pub enum Error {
#[error("Bundle error: {0}")]
Bundle(#[from] sigstore_bundle::Error),

/// Invalid trust-root configuration.
#[error("Trust root error: {0}")]
TrustRoot(#[from] sigstore_trust_root::Error),

/// A configured authority certificate could not be parsed.
#[error("invalid trusted certificate: {0}")]
TrustedCertificate(#[source] webpki::Error),

/// Failed to read artifact input.
#[error("failed to read artifact: {0}")]
ArtifactRead(#[source] std::io::Error),
Expand Down
110 changes: 99 additions & 11 deletions crates/sigstore-verify/src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,17 +267,95 @@ impl VerificationResult {
pub struct Verifier {
/// Trusted root containing verification material
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 {
/// Create a new verifier with a trusted root
///
/// The trusted root is required and contains all cryptographic material
/// needed for verification (Fulcio CA certs, Rekor keys, TSA certs, etc.)
pub fn new(trusted_root: &TrustedRoot) -> Self {
Self {
trusted_root: trusted_root.clone(),
///
/// Prepares configured keys and certificates, rejecting malformed,
/// unsupported, duplicate or invalid-window trust material before artifact
/// I/O. Empty authority lists are allowed for managed-key or relaxed policies.
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_slice(&id.key_id.decode()?)?;
if !ids.insert((is_rekor, id)) {
return Err(Error::Verification(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::Verification(
"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(Error::TrustedCertificate)?;
if is_fulcio {
fulcio_anchors.push((anchor.to_owned(), window));
}
}
}
Ok(Self {
trusted_root: trusted_root.clone(),
rekor_keys,
ct_keys,
fulcio_anchors,
})
}

/// Verify an artifact against a bundle
Expand All @@ -295,7 +373,7 @@ impl Verifier {
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let trusted_root = TrustedRoot::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT)?;
/// let verifier = Verifier::new(&trusted_root);
/// let verifier = Verifier::new(&trusted_root)?;
/// let bundle: Bundle = todo!();
/// let policy = VerificationPolicy::any_identity();
///
Expand Down Expand Up @@ -419,6 +497,7 @@ impl Verifier {
bundle,
&signature,
&self.trusted_root,
&self.rekor_keys,
)?;

// (1): Verify that the signing certificate chains to the root of trust,
Expand All @@ -439,7 +518,7 @@ impl Verifier {
issuer_spki = Some(crate::verify_impl::helpers::verify_certificate_chain(
&bundle.verification_material.content,
validation_time,
&self.trusted_root,
&self.fulcio_anchors,
)?);

// Also verify the certificate is within its validity period
Expand All @@ -458,7 +537,7 @@ impl Verifier {
crate::verify_impl::sct::verify_sct(
cert.as_bytes(),
issuer_spki.as_bytes(),
&self.trusted_root,
&self.ct_keys,
)?;
result.sct_verified = true;
}
Expand Down Expand Up @@ -514,7 +593,7 @@ impl Verifier {
if policy.verify_tlog {
let integrated_time = crate::verify_impl::tlog::verify_tlog_entries(
bundle,
&self.trusted_root,
&self.rekor_keys,
cert_info.not_before,
cert_info.not_after,
)?;
Expand Down Expand Up @@ -584,7 +663,7 @@ impl Verifier {
///
/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let trusted_root = TrustedRoot::from_file("trusted_root.json")?;
/// let verifier = Verifier::new(&trusted_root);
/// let verifier = Verifier::new(&trusted_root)?;
/// let bundle = Bundle::from_json(&std::fs::read_to_string("artifact.sigstore.json")?)?;
/// let public_key = DerPublicKey::from_pem(&std::fs::read_to_string("key.pub")?)?;
/// let artifact = std::fs::read("artifact.txt")?;
Expand Down Expand Up @@ -678,7 +757,7 @@ impl Verifier {
// SETs) without certificate time validation.
if policy.verify_tlog {
for entry in &bundle.verification_material.tlog_entries {
crate::verify_impl::tlog::verify_entry_inclusion(entry, &self.trusted_root)?;
crate::verify_impl::tlog::verify_entry_inclusion(entry, &self.rekor_keys)?;

let is_rekor_v1 = matches!(
entry.kind_version,
Expand Down Expand Up @@ -733,6 +812,15 @@ impl Verifier {
}
}

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::Verification(
"trusted validity window ends before it starts".into(),
));
}
Ok(())
}

/// Check that a `MessageSignature`'s declared `messageDigest`, if any, matches
/// the artifact.
fn verify_message_digest_binding(
Expand Down Expand Up @@ -904,7 +992,7 @@ pub fn verify<'a>(
policy: &VerificationPolicy,
trusted_root: &TrustedRoot,
) -> Result<VerificationResult> {
Verifier::new(trusted_root).verify(artifact, bundle, policy)
Verifier::new(trusted_root)?.verify(artifact, bundle, policy)
}

/// Convenience function to verify a managed-key bundle with a caller-supplied
Expand Down Expand Up @@ -947,7 +1035,7 @@ pub fn verify_with_key<'a>(
policy: &PublicKeyVerificationPolicy,
trusted_root: &TrustedRoot,
) -> Result<VerificationResult> {
Verifier::new(trusted_root).verify_with_key(artifact, bundle, public_key, policy)
Verifier::new(trusted_root)?.verify_with_key(artifact, bundle, public_key, policy)
}

#[cfg(test)]
Expand Down
56 changes: 29 additions & 27 deletions crates/sigstore-verify/src/verify_impl/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ use sigstore_crypto::CertificateInfo;
use sigstore_trust_root::{TrustedRoot, TsaAuthority};
use sigstore_types::bundle::VerificationMaterialContent;
use sigstore_types::{Bundle, DerPublicKey, KindVersion, SignatureBytes, SignatureContent};
use webpki::{anchor_from_trusted_cert, EndEntityCert, KeyUsage, ALL_VERIFICATION_ALGS};
use webpki::{EndEntityCert, KeyUsage, ALL_VERIFICATION_ALGS};

pub(crate) type FulcioAnchor = (
rustls_pki_types::TrustAnchor<'static>,
Option<sigstore_types::TimeRange>,
);

/// Extract signature from bundle content (needed for TSA verification).
///
Expand Down Expand Up @@ -131,7 +136,7 @@ pub fn has_v2_tlog_entries(bundle: &Bundle) -> bool {
/// Returns every authenticated integrated time.
fn extract_v1_integrated_times_with_promise(
bundle: &Bundle,
trusted_root: &TrustedRoot,
rekor_keys: &sigstore_crypto::Keyring,
) -> Result<Vec<jiff::Timestamp>> {
let mut times = Vec::new();

Expand All @@ -147,7 +152,7 @@ fn extract_v1_integrated_times_with_promise(
}

if let Some(time) = entry.integrated_time {
crate::verify_impl::tlog::verify_set(entry, trusted_root)?;
crate::verify_impl::tlog::verify_set(entry, rekor_keys)?;
times.push(time);
}
}
Expand Down Expand Up @@ -186,11 +191,11 @@ pub fn determine_validation_times(
bundle: &Bundle,
signature: &SignatureBytes,
trusted_root: &TrustedRoot,
rekor_keys: &sigstore_crypto::Keyring,
) -> Result<Vec<jiff::Timestamp>> {
let mut times = extract_tsa_timestamps(bundle, signature.as_bytes(), trusted_root)?;
times.extend(extract_v1_integrated_times_with_promise(
bundle,
trusted_root,
bundle, rekor_keys,
)?);

if !times.is_empty() {
Expand Down Expand Up @@ -252,7 +257,7 @@ pub fn validate_certificate_time(
pub fn verify_certificate_chain(
verification_material: &VerificationMaterialContent,
validation_time: jiff::Timestamp,
trusted_root: &TrustedRoot,
fulcio_anchors: &[FulcioAnchor],
) -> Result<DerPublicKey> {
// Extract the end-entity certificate and any intermediates from the bundle
let (ee_cert_der, intermediate_ders) = match verification_material {
Expand All @@ -277,24 +282,13 @@ pub fn verify_certificate_chain(
}
};

// Get Fulcio certificates from trusted root to use as trust anchors
let fulcio_certs = trusted_root.fulcio_certs();

if fulcio_certs.is_empty() {
return Err(Error::Verification(
"no Fulcio certificates in trusted root".to_string(),
));
}

// Build trust anchors from Fulcio root certificates
let trust_anchors: Vec<_> = fulcio_certs
// Keep window selection dynamic: a long-lived verifier may cross the
// activation time of a future authority after construction.
let now = jiff::Timestamp::now();
let trust_anchors: Vec<_> = fulcio_anchors
.iter()
.filter_map(|cert_der| {
let cert = CertificateDer::from(&cert_der[..]);
anchor_from_trusted_cert(&cert)
.map(|anchor| anchor.to_owned())
.ok()
})
.filter(|(_, window)| window.is_none_or(|range| range.has_started_by(now)))
.map(|(anchor, _)| anchor.clone())
.collect();

if trust_anchors.is_empty() {
Expand Down Expand Up @@ -385,7 +379,13 @@ mod tests {
.unwrap();
let signature = extract_signature(&bundle.content);

let times = determine_validation_times(&bundle, &signature, &trusted_root).unwrap();
let times = determine_validation_times(
&bundle,
&signature,
&trusted_root,
&trusted_root.rekor_keys().unwrap(),
)
.unwrap();

assert_eq!(
times.len(),
Expand Down Expand Up @@ -450,16 +450,18 @@ mod tests {
// The canonical flow: the issuer comes from the verified chain, then SCT
// verification uses it. Before the fix, SCT verification returned
// Err("SCT signature verification failed: ... signature invalid").
let issuer_spki = verify_certificate_chain(material, validation_time, &trusted_root)
.expect("certificate chain should verify against the staging root");
let verifier = crate::Verifier::new(&trusted_root).unwrap();
let issuer_spki =
verify_certificate_chain(material, validation_time, &verifier.fulcio_anchors)
.expect("certificate chain should verify against the staging root");
let cert = match material {
VerificationMaterialContent::Certificate(cert) => &cert.raw_bytes,
VerificationMaterialContent::X509CertificateChain { certificates } => {
&certificates[0].raw_bytes
}
_ => panic!("fixture must have a signing certificate"),
};
super::super::sct::verify_sct(cert.as_bytes(), issuer_spki.as_bytes(), &trusted_root)
super::super::sct::verify_sct(cert.as_bytes(), issuer_spki.as_bytes(), &verifier.ct_keys)
.expect("SCT verification should succeed once the correct issuer is selected");
}

Expand Down
Loading
Loading