From 69f64fa61b57231deff2c884bde3803375edaed7 Mon Sep 17 00:00:00 2001 From: Jack Leightcap Date: Wed, 3 Jan 2024 13:12:02 -0500 Subject: [PATCH 01/27] verify: init Signed-off-by: Jack Leightcap --- Cargo.toml | 3 +- src/bundle/mod.rs | 28 +- src/crypto/certificate.rs | 143 ++++++++- src/crypto/mod.rs | 2 + src/crypto/verification.rs | 8 + src/crypto/verification_key.rs | 64 +++- src/errors.rs | 3 + src/lib.rs | 3 + src/tuf/constants.rs | 1 + src/verify/mod.rs | 24 ++ src/verify/models.rs | 222 ++++++++++++++ src/verify/policy.rs | 284 ++++++++++++++++++ src/verify/verifier.rs | 197 ++++++++++++ tests/conformance/Cargo.toml | 2 + tests/conformance/conformance.rs | 67 +++++ .../data/repository/1.registry.npmjs.org.json | 23 ++ tests/data/repository/1.root.json | 119 ++------ tests/data/repository/1.snapshot.json | 32 ++ tests/data/repository/1.targets.json | 148 +++++++++ .../data/repository/2.registry.npmjs.org.json | 23 ++ tests/data/repository/2.root.json | 129 ++------ tests/data/repository/2.snapshot.json | 32 ++ tests/data/repository/2.targets.json | 135 +++++++++ tests/data/repository/registry.npmjs.org.json | 23 ++ tests/data/repository/rekor.json | 23 -- tests/data/repository/root.json | 129 ++------ tests/data/repository/snapshot.json | 52 ++-- tests/data/repository/staging.json | 15 - tests/data/repository/targets.json | 202 +++++++------ ...67072b6f89ddf1032273a78b.trusted_root.json | 86 ++++++ tests/data/repository/targets/artifact.pub | 4 - tests/data/repository/targets/ctfe.pub | 4 - ...40ca812d8ac47a128bf84963.trusted_root.json | 86 ++++++ tests/data/repository/targets/fulcio.crt.pem | 13 - .../data/repository/targets/fulcio_v1.crt.pem | 13 - ...8f8df61bc7274189122c123446248426.keys.json | 26 ++ ...2551fcaa870a30d4601ba1caf6f63699.keys.json | 26 ++ tests/data/repository/targets/rekor.0.pub | 4 - tests/data/repository/targets/rekor.json | 23 -- tests/data/repository/targets/rekor.pub | 4 - tests/data/repository/timestamp.json | 30 +- 41 files changed, 1906 insertions(+), 549 deletions(-) create mode 100644 src/crypto/verification.rs create mode 100644 src/verify/mod.rs create mode 100644 src/verify/models.rs create mode 100644 src/verify/policy.rs create mode 100644 src/verify/verifier.rs create mode 100644 tests/data/repository/1.registry.npmjs.org.json create mode 100644 tests/data/repository/1.snapshot.json create mode 100644 tests/data/repository/1.targets.json create mode 100644 tests/data/repository/2.registry.npmjs.org.json create mode 100644 tests/data/repository/2.snapshot.json create mode 100644 tests/data/repository/2.targets.json create mode 100644 tests/data/repository/registry.npmjs.org.json delete mode 100644 tests/data/repository/rekor.json delete mode 100644 tests/data/repository/staging.json create mode 100644 tests/data/repository/targets/6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b.trusted_root.json delete mode 100644 tests/data/repository/targets/artifact.pub delete mode 100644 tests/data/repository/targets/ctfe.pub create mode 100644 tests/data/repository/targets/fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963.trusted_root.json delete mode 100644 tests/data/repository/targets/fulcio.crt.pem delete mode 100644 tests/data/repository/targets/fulcio_v1.crt.pem create mode 100644 tests/data/repository/targets/registry.npmjs.org/7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426.keys.json create mode 100644 tests/data/repository/targets/registry.npmjs.org/881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699.keys.json delete mode 100644 tests/data/repository/targets/rekor.0.pub delete mode 100644 tests/data/repository/targets/rekor.json delete mode 100644 tests/data/repository/targets/rekor.pub diff --git a/Cargo.toml b/Cargo.toml index 0ce09da945..80b107eef5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" repository = "https://github.com/sigstore/sigstore-rs" [features] -default = ["full-native-tls", "cached-client", "tuf", "sign"] +default = ["full-native-tls", "cached-client", "tuf", "sign", "verify"] wasm = ["getrandom/js"] full-native-tls = [ @@ -43,6 +43,7 @@ rekor = ["reqwest"] tuf = ["tough", "regex"] sign = [] +verify = [] cosign-native-tls = [ "oci-distribution/native-tls", diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs index 2b9b9cb73c..1bf8e8d840 100644 --- a/src/bundle/mod.rs +++ b/src/bundle/mod.rs @@ -15,21 +15,45 @@ //! Useful types for Sigstore bundles. use std::fmt::Display; +use std::str::FromStr; pub use sigstore_protobuf_specs::Bundle; +macro_rules! required { + ($($base:expr )? ; $first_attr:ident $( . $rest_attrs:ident)* $( , $else_err:expr)?) => { + $( $base . )? $first_attr.as_ref() + $( + .and_then(|v| v.$rest_attrs.as_ref()) + )* + $( .ok_or($else_err) )? + } +} +pub(crate) use required; + // Known Sigstore bundle media types. #[derive(Clone, Copy, Debug)] pub enum Version { - _Bundle0_1, + Bundle0_1, Bundle0_2, } impl Display for Version { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match &self { - Version::_Bundle0_1 => "application/vnd.dev.sigstore.bundle+json;version=0.1", + Version::Bundle0_1 => "application/vnd.dev.sigstore.bundle+json;version=0.1", Version::Bundle0_2 => "application/vnd.dev.sigstore.bundle+json;version=0.2", }) } } + +impl FromStr for Version { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "application/vnd.dev.sigstore.bundle+json;version=0.1" => Ok(Version::Bundle0_1), + "application/vnd.dev.sigstore.bundle+json;version=0.2" => Ok(Version::Bundle0_2), + _ => Err(()), + } + } +} diff --git a/src/crypto/certificate.rs b/src/crypto/certificate.rs index 943df12923..34ae09cbc6 100644 --- a/src/crypto/certificate.rs +++ b/src/crypto/certificate.rs @@ -16,7 +16,7 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use const_oid::db::rfc5912::ID_KP_CODE_SIGNING; use x509_cert::{ - ext::pkix::{ExtendedKeyUsage, KeyUsage, KeyUsages, SubjectAltName}, + ext::pkix::{constraints, ExtendedKeyUsage, KeyUsage, KeyUsages, SubjectAltName}, Certificate, }; @@ -120,6 +120,147 @@ fn verify_expiration(certificate: &Certificate, integrated_time: i64) -> Result< Ok(()) } +/// Check if the given certificate is a leaf in the context of the Sigstore profile. +/// +/// * It is not a root or intermediate CA; +/// * It has `keyUsage.digitalSignature` +/// * It has `CODE_SIGNING` as an `ExtendedKeyUsage`. +/// +/// This function does not evaluate the trustworthiness of the certificate. +pub(crate) fn is_leaf(certificate: &Certificate) -> Result { + // NOTE(jl): following structure of sigstore-python over the slightly different handling found + // in `verify_key_usages`. + let tbs = &certificate.tbs_certificate; + + // Only V3 certificates should appear in the context of Sigstore; earlier versions of X.509 lack + // extensions and have ambiguous CA behavior. + if tbs.version != x509_cert::Version::V3 { + return Err(SigstoreError::CertificateUnsupportedVersionError); + } + + if is_ca(certificate)? { + return Ok(false); + }; + + let digital_signature = match tbs.get::()? { + None => { + return Err(SigstoreError::InvalidCertError( + "invalid X.509 certificate: missing KeyUsage".to_string(), + )) + } + Some((_, key_usage)) => key_usage.digital_signature(), + }; + + if !digital_signature { + return Err(SigstoreError::InvalidCertError( + "invalid certificate for Sigstore purposes: missing digital signature usage" + .to_string(), + )); + } + + // Finally, we check to make sure the leaf has an `ExtendedKeyUsages` + // extension that includes a codesigning entitlement. Sigstore should + // never issue a leaf that doesn't have this extended usage. + + let extended_key_usage = match tbs.get::()? { + None => { + return Err(SigstoreError::InvalidCertError( + "invalid X.509 certificate: missing ExtendedKeyUsage".to_string(), + )) + } + Some((_, extended_key_usage)) => extended_key_usage, + }; + + Ok(extended_key_usage.0.contains(&ID_KP_CODE_SIGNING)) +} + +/// Checks if the given `certificate` is a CA certificate. +/// +/// This does **not** indicate trustworthiness of the given `certificate`, only if it has the +/// appropriate interior state. +/// +/// This function is **not** naively invertible: users **must** use the dedicated `is_leaf` +/// utility function to determine whether a particular leaf upholds Sigstore's invariants. +pub(crate) fn is_ca(certificate: &Certificate) -> Result { + let tbs = &certificate.tbs_certificate; + + // Only V3 certificates should appear in the context of Sigstore; earlier versions of X.509 lack + // extensions and have ambiguous CA behavior. + if tbs.version != x509_cert::Version::V3 { + return Err(SigstoreError::CertificateUnsupportedVersionError); + } + + // Valid CA certificates must have the following set: + // + // - `BasicKeyUsage.keyCertSign` + // - `BasicConstraints.ca` + // + // Any other combination of states is inconsistent and invalid, meaning + // that we won't consider the certificate a valid non-CA leaf. + + let ca = match tbs.get::()? { + None => return Ok(false), + Some((false, _)) => { + // BasicConstraints must be marked as critical, per RFC 5280 4.2.1.9. + return Err(SigstoreError::InvalidCertError( + "invalid X.509 certificate: non-critical BasicConstraints in CA".to_string(), + )); + } + Some((true, v)) => v.ca, + }; + + let key_cert_sign = match tbs.get::()? { + None => { + return Err(SigstoreError::InvalidCertError( + "invalid X.509 certificate: missing KeyUsage".to_string(), + )) + } + Some((_, v)) => v.key_cert_sign(), + }; + + // both states set, this is a CA. + if ca && key_cert_sign { + return Ok(true); + } + + if !(ca || key_cert_sign) { + return Ok(false); + } + + // Anything else is an invalid state that should never occur. + Err(SigstoreError::InvalidCertError(format!( + "invalid certificate states: KeyUsage.keyCertSign={}, BasicConstraints.ca={}", + key_cert_sign, ca + ))) +} + +/// Returns `True` if and only if the given `Certificate` indicates +/// that it's a root CA. +/// +/// This is **not** a verification function, and it does not establish +/// the trustworthiness of the given certificate. +pub(crate) fn is_root_ca(certificate: &Certificate) -> Result { + // NOTE(ww): This function is obnoxiously long to make the different + // states explicit. + + let tbs = &certificate.tbs_certificate; + + // Only V3 certificates should appear in the context of Sigstore; earlier versions of X.509 lack + // extensions and have ambiguous CA behavior. + if tbs.version != x509_cert::Version::V3 { + return Err(SigstoreError::CertificateUnsupportedVersionError); + } + + // Non-CAs can't possibly be root CAs. + if !is_ca(certificate)? { + return Ok(false); + } + + // A certificate that is its own issuer and signer is considered a root CA. + // TODO(jl): verify_directly_issued_by + todo!() +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index c8a35e22bf..adfa82ba5e 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -177,6 +177,8 @@ pub enum Signature<'a> { pub(crate) mod certificate; #[cfg(feature = "cert")] pub(crate) mod certificate_pool; +#[cfg(feature = "cert")] +pub(crate) use certificate_pool::CertificatePool; pub mod verification_key; diff --git a/src/crypto/verification.rs b/src/crypto/verification.rs new file mode 100644 index 0000000000..a0a6ddcf2d --- /dev/null +++ b/src/crypto/verification.rs @@ -0,0 +1,8 @@ +use rustls_pki_types::CertificateDer; +use webpki::TrustAnchor; + +/// Machinery for Sigstore end entity certificate verification. +struct CertificateVerificationContext<'a> { + pub trust_anchors: Vec>, + pub intermediate_certs: Vec>, +} diff --git a/src/crypto/verification_key.rs b/src/crypto/verification_key.rs index 1cf40d7eed..f6881f2574 100644 --- a/src/crypto/verification_key.rs +++ b/src/crypto/verification_key.rs @@ -18,7 +18,7 @@ use const_oid::db::rfc5912::{ID_EC_PUBLIC_KEY, RSA_ENCRYPTION}; use ed25519::pkcs8::DecodePublicKey as ED25519DecodePublicKey; use rsa::{pkcs1v15, pss}; use sha2::{Digest, Sha256, Sha384}; -use signature::{DigestVerifier, Verifier}; +use signature::{hazmat::PrehashVerifier, DigestVerifier, Verifier}; use std::convert::TryFrom; use x509_cert::{der::referenced::OwnedToRef, spki::SubjectPublicKeyInfoOwned}; @@ -329,6 +329,68 @@ impl CosignVerificationKey { } } } + + /// Verify the signature provided has been actually generated by the given key + /// when signing the provided prehashed message. + pub fn verify_prehash(&self, signature: Signature, msg: &[u8]) -> Result<()> { + let sig = match signature { + Signature::Raw(data) => data.to_owned(), + Signature::Base64Encoded(data) => BASE64_STD_ENGINE.decode(data)?, + }; + + match self { + CosignVerificationKey::RSA_PSS_SHA256(inner) => { + let sig = pss::Signature::try_from(sig.as_slice())?; + inner + .verify_prehash(msg, &sig) + .map_err(|_| SigstoreError::PublicKeyVerificationError) + } + CosignVerificationKey::RSA_PSS_SHA384(inner) => { + let sig = pss::Signature::try_from(sig.as_slice())?; + inner + .verify_prehash(msg, &sig) + .map_err(|_| SigstoreError::PublicKeyVerificationError) + } + CosignVerificationKey::RSA_PSS_SHA512(inner) => { + let sig = pss::Signature::try_from(sig.as_slice())?; + inner + .verify_prehash(msg, &sig) + .map_err(|_| SigstoreError::PublicKeyVerificationError) + } + CosignVerificationKey::RSA_PKCS1_SHA256(inner) => { + let sig = pkcs1v15::Signature::try_from(sig.as_slice())?; + inner + .verify_prehash(msg, &sig) + .map_err(|_| SigstoreError::PublicKeyVerificationError) + } + CosignVerificationKey::RSA_PKCS1_SHA384(inner) => { + let sig = pkcs1v15::Signature::try_from(sig.as_slice())?; + inner + .verify_prehash(msg, &sig) + .map_err(|_| SigstoreError::PublicKeyVerificationError) + } + CosignVerificationKey::RSA_PKCS1_SHA512(inner) => { + let sig = pkcs1v15::Signature::try_from(sig.as_slice())?; + inner + .verify_prehash(msg, &sig) + .map_err(|_| SigstoreError::PublicKeyVerificationError) + } + // ECDSA signatures are encoded in der. + CosignVerificationKey::ECDSA_P256_SHA256_ASN1(inner) => { + let sig = ecdsa::Signature::from_der(&sig)?; + inner + .verify_prehash(msg, &sig) + .map_err(|_| SigstoreError::PublicKeyVerificationError) + } + CosignVerificationKey::ECDSA_P384_SHA384_ASN1(inner) => { + let sig = ecdsa::Signature::from_der(&sig)?; + inner + .verify_prehash(msg, &sig) + .map_err(|_| SigstoreError::PublicKeyVerificationError) + } + _ => unimplemented!("Ed25519 doesn't implement verify_prehash"), + } + } } #[cfg(test)] diff --git a/src/errors.rs b/src/errors.rs index 22b87e95cb..e6b6b723ff 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -109,6 +109,9 @@ pub enum SigstoreError { #[error("Certificate pool error: {0}")] CertificatePoolError(String), + #[error("Certificate invalid: {0}")] + InvalidCertError(String), + #[error("Signing session expired")] ExpiredSigningSession(), diff --git a/src/lib.rs b/src/lib.rs index 0c648b5f62..3346d8d57e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -288,5 +288,8 @@ pub mod tuf; mod bundle; pub use bundle::Bundle; +#[cfg(feature = "verify")] +pub mod verify; + #[cfg(feature = "sign")] pub mod sign; diff --git a/src/tuf/constants.rs b/src/tuf/constants.rs index 325989706c..0529d1128b 100644 --- a/src/tuf/constants.rs +++ b/src/tuf/constants.rs @@ -23,3 +23,4 @@ macro_rules! tuf_resource { } pub(crate) const SIGSTORE_ROOT: &[u8] = tuf_resource!("prod/root.json"); +pub(crate) const _SIGSTORE_TRUST_BUNDLE: &[u8] = tuf_resource!("prod/trusted_root.json"); diff --git a/src/verify/mod.rs b/src/verify/mod.rs new file mode 100644 index 0000000000..5954d45bd6 --- /dev/null +++ b/src/verify/mod.rs @@ -0,0 +1,24 @@ +// +// Copyright 2023 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Verifier for Sigstore bundles and associated types and policies. +mod models; +pub use models::{VerificationError, VerificationMaterials, VerificationResult}; + +pub mod policy; +pub use policy::VerificationPolicy; + +mod verifier; +pub use verifier::Verifier; diff --git a/src/verify/models.rs b/src/verify/models.rs new file mode 100644 index 0000000000..dcf6e39b68 --- /dev/null +++ b/src/verify/models.rs @@ -0,0 +1,222 @@ +// +// Copyright 2023 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + cell::OnceCell, + io::{self, Read}, + str::FromStr, +}; + +use crate::{ + bundle::required, + bundle::Version as BundleVersion, + crypto::certificate::{is_leaf, is_root_ca}, + errors::SigstoreError, + rekor::models::log_entry, + rekor::models::{ + log_entry::{InclusionProof, Verification}, + LogEntry, + }, +}; + +use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; +use pkcs8::der::Decode; +use sha2::{Digest, Sha256}; +use sigstore_protobuf_specs::Bundle; +use thiserror::Error; +use x509_cert::Certificate; + +#[derive(Error, Debug)] +pub enum VerificationError { + #[error("Certificate expired before time of signing")] + CertificateExpired, + + #[error("Certificate malformed")] + CertificateMalformed, + + #[error("Failed to verify certificate")] + CertificateVerificationFailure, + + #[error("Certificate cannot be used for verification: {0}")] + CertificateTypeError(String), + + #[error("Failed to verify that the signature corresponds to the input")] + SignatureVerificationFailure, + + #[error("{0}")] + PolicyFailure(String), +} +pub type VerificationResult = Result<(), VerificationError>; + +pub struct VerificationMaterials { + pub input_digest: Vec, + pub certificate: Certificate, + pub signature: Vec, + rekor_entry: OnceCell, +} + +impl VerificationMaterials { + pub fn new( + input: &mut R, + certificate: Certificate, + signature: Vec, + offline: bool, + rekor_entry: Option, + ) -> Option { + let mut hasher = Sha256::new(); + io::copy(input, &mut hasher).ok()?; + + if offline && rekor_entry.is_none() { + // offline verification requires a Rekor entry + return None; + } + + let rekor_entry = if let Some(rekor_entry) = rekor_entry { + let cell = OnceCell::new(); + + // TODO(tnytown): Switch to setting if offline when Rekor fetching is implemented. + #[allow(clippy::unwrap_used)] + cell.set(rekor_entry).unwrap(); + + cell + } else { + Default::default() + }; + + Some(Self { + input_digest: hasher.finalize().to_vec(), + rekor_entry, + certificate, + signature, + }) + } + + /// Constructs a VerificationMaterials from the given Bundle. + /// + /// For details on bundle semantics, please refer to [VerificationMaterial]. + /// + /// [VerificationMaterial]: sigstore_protobuf_specs::DevSigstoreBundleV1VerificationMaterial + /// + /// TODO(tnytown): Determine if this type should yield SigstoreResult. + pub fn from_bundle(input: &mut R, bundle: Bundle, offline: bool) -> Option { + fn certificate_from_base64(encoded: &str) -> Option { + Certificate::from_der(&base64.decode(encoded).ok()?).ok() + } + + let certs = required!( + bundle; + verification_material.x_509_certificate_chain.certificates, + SigstoreError::SigstoreBundleMalformedError("Cannot find required field in bundle".to_string()) + ).ok()?; + + // Parse the certificates. The first entry in the chain MUST be a leaf certificate, and the + // rest of the chain MUST NOT include a root CA or any intermediate CAs that appear in an + // independent root of trust. + let certs = certs + .iter() + .map(|cert| certificate_from_base64(cert.raw_bytes.as_ref()?)) + .collect::>>()?; + let [leaf_cert, chain_certs @ ..] = &certs[..] else { + return None; + }; + + if is_leaf(leaf_cert).is_err() { + return None; + } + + for chain_cert in chain_certs { + if is_root_ca(chain_cert).is_ok() { + return None; + } + } + + let signature = base64 + .decode(required!(bundle; message_signature.signature)?) + .ok()?; + let tlog_entries = required!(bundle; verification_material.tlog_entries)?; + if tlog_entries.len() != 1 { + // Expected exactly one tlog entry. + return None; + } + let tlog_entry = &tlog_entries[0]; + + let inclusion_promise = &tlog_entry.inclusion_promise; + let inclusion_proof = tlog_entry.inclusion_proof.as_ref(); + + let has_checkpoint = required!(; inclusion_proof.checkpoint.envelope).is_some(); + match BundleVersion::from_str(&bundle.media_type?).ok()? { + BundleVersion::Bundle0_1 => { + if inclusion_promise.is_none() { + // 0.1 bundle must contain inclusion promise + return None; + } + + if inclusion_proof.is_some() && !has_checkpoint { + // TODO(tnytown): Act here. + // NOTE(jl): in sigstore-python, this is a no-op that prints a warning log. + } + } + BundleVersion::Bundle0_2 => { + inclusion_proof?; + if !has_checkpoint { + // inclusion proofs must contain checkpoints + return None; // FIXME(jl): this raises an error in sigstore-python. + } + } + } + + let parsed_inclusion_proof = if inclusion_proof.is_some() && has_checkpoint { + Some(InclusionProof { + checkpoint: required!(; inclusion_proof.checkpoint.envelope)?.clone(), + hashes: required!(; inclusion_proof.hashes)?.clone(), + log_index: required!(; inclusion_proof.log_index)?.parse().ok()?, + root_hash: required!(; inclusion_proof.log_index)?.clone(), + tree_size: required!(; inclusion_proof.tree_size)?.parse().ok()?, + }) + } else { + None + }; + + let canonicalized_body = { + let decoded = base64 + .decode(tlog_entry.canonicalized_body.as_ref()?) + .ok()?; + serde_json::from_slice(&decoded).ok()? + }; + // required!(tlog_entry; log_id.key_id)?.clone(); + let entry = LogEntry { + uuid: "".to_string(), + body: log_entry::Body::hashedrekord(canonicalized_body), + attestation: None, + integrated_time: required!(tlog_entry; integrated_time)?.parse().ok()?, + log_i_d: "".into(), + log_index: required!(tlog_entry; log_index)?.parse().ok()?, + verification: Verification { + inclusion_proof: parsed_inclusion_proof, + signed_entry_timestamp: required!(; inclusion_promise.signed_entry_timestamp)? + .clone(), + }, + }; + + Self::new(input, leaf_cert.clone(), signature, offline, Some(entry)) + } + + /// Retrieves the [LogEntry] for the materials. + pub fn rekor_entry(&self) -> &LogEntry { + // TODO(tnytown): Fetch online Rekor entry, confirm consistency, and get_or_init here. + #[allow(clippy::unwrap_used)] + self.rekor_entry.get().unwrap() + } +} diff --git a/src/verify/policy.rs b/src/verify/policy.rs new file mode 100644 index 0000000000..8f0791c50f --- /dev/null +++ b/src/verify/policy.rs @@ -0,0 +1,284 @@ +// Copyright 2023 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Verifiers for certificate metadata. +//! +//! + +use const_oid::ObjectIdentifier; +use x509_cert::ext::pkix::{name::GeneralName, SubjectAltName}; + +use crate::verify::VerificationError; + +use super::models::VerificationResult; + +macro_rules! oids { + ($($name:ident = $value:literal),+) => { + $(const $name: ObjectIdentifier = ObjectIdentifier::new_unwrap($value);)+ + }; +} + +macro_rules! impl_policy { + ($policy:ident, $oid:expr, $doc:literal) => { + #[doc = $doc] + pub struct $policy(pub String); + + impl const_oid::AssociatedOid for $policy { + const OID: ObjectIdentifier = $oid; + } + + impl SingleX509ExtPolicy for $policy { + fn new>(val: S) -> Self { + Self(val.as_ref().to_owned()) + } + + fn name() -> &'static str { + stringify!($policy) + } + + fn value(&self) -> &str { + &self.0 + } + } + }; +} + +oids! { + OIDC_ISSUER_OID = "1.3.6.1.4.1.57264.1.1", + OIDC_GITHUB_WORKFLOW_TRIGGER_OID = "1.3.6.1.4.1.57264.1.2", + OIDC_GITHUB_WORKFLOW_SHA_OID = "1.3.6.1.4.1.57264.1.3", + OIDC_GITHUB_WORKFLOW_NAME_OID = "1.3.6.1.4.1.57264.1.4", + OIDC_GITHUB_WORKFLOW_REPOSITORY_OID = "1.3.6.1.4.1.57264.1.5", + OIDC_GITHUB_WORKFLOW_REF_OID = "1.3.6.1.4.1.57264.1.6", + OTHERNAME_OID = "1.3.6.1.4.1.57264.1.7" + +} + +/// A trait for policies that check a single textual value against a X.509 extension. +pub trait SingleX509ExtPolicy { + fn new>(val: S) -> Self; + fn name() -> &'static str; + fn value(&self) -> &str; +} + +impl VerificationPolicy for T { + fn verify(&self, cert: &x509_cert::Certificate) -> VerificationResult { + let extensions = cert.tbs_certificate.extensions.as_deref().unwrap_or(&[]); + let mut extensions = extensions.iter().filter(|ext| ext.extn_id == T::OID); + + // Check for exactly one extension. + let (Some(ext), None) = (extensions.next(), extensions.next()) else { + return Err(VerificationError::PolicyFailure( + "Cannot get policy extensions from certificate".into(), + )); + }; + + // Parse raw string without DER encoding. + let val = std::str::from_utf8(ext.extn_value.as_bytes()) + .expect("failed to parse constructed Extension!"); + + if val != self.value() { + Err(VerificationError::PolicyFailure(format!( + "Certificate's {} does not match (got {}, expected {})", + T::name(), + val, + self.value() + ))) + } else { + Ok(()) + } + } +} + +impl_policy!( + OIDCIssuer, + OIDC_ISSUER_OID, + "Checks the certificate's OIDC issuer." +); + +impl_policy!( + GitHubWorkflowTrigger, + OIDC_GITHUB_WORKFLOW_TRIGGER_OID, + "Checks the certificate's GitHub Actions workflow trigger." +); + +impl_policy!( + GitHubWorkflowSHA, + OIDC_GITHUB_WORKFLOW_SHA_OID, + "Checks the certificate's GitHub Actions workflow commit SHA." +); + +impl_policy!( + GitHubWorkflowName, + OIDC_GITHUB_WORKFLOW_NAME_OID, + "Checks the certificate's GitHub Actions workflow name." +); + +impl_policy!( + GitHubWorkflowRepository, + OIDC_GITHUB_WORKFLOW_REPOSITORY_OID, + "Checks the certificate's GitHub Actions workflow repository." +); + +impl_policy!( + GitHubWorkflowRef, + OIDC_GITHUB_WORKFLOW_REF_OID, + "Checks the certificate's GitHub Actions workflow ref." +); + +/// An interface that all policies must conform to. +pub trait VerificationPolicy { + fn verify(&self, cert: &x509_cert::Certificate) -> VerificationResult; +} + +/// The "any of" policy, corresponding to a logical OR between child policies. +/// +/// An empty list of child policies is considered trivially invalid. +pub struct AnyOf<'a> { + children: Vec<&'a dyn VerificationPolicy>, +} + +impl<'a> AnyOf<'a> { + pub fn new>(policies: I) -> Self { + Self { + children: policies.into_iter().collect(), + } + } +} + +impl VerificationPolicy for AnyOf<'_> { + fn verify(&self, cert: &x509_cert::Certificate) -> VerificationResult { + self.children + .iter() + .find(|policy| policy.verify(cert).is_ok()) + .ok_or(VerificationError::PolicyFailure(format!( + "0 of {} policies succeeded", + self.children.len() + ))) + .map(|_| ()) + } +} + +/// The "all of" policy, corresponding to a logical AND between child policies. +/// +/// An empty list of child policies is considered trivially invalid. +pub struct AllOf<'a> { + children: Vec<&'a dyn VerificationPolicy>, +} + +impl<'a> AllOf<'a> { + pub fn new>(policies: I) -> Self { + Self { + children: policies.into_iter().collect(), + } + } +} + +impl VerificationPolicy for AllOf<'_> { + fn verify(&self, cert: &x509_cert::Certificate) -> VerificationResult { + // Without this, we'd consider empty lists of child policies trivially valid. + // This is almost certainly not what the user wants and is a potential + // source of API misuse, so we explicitly disallow it. + if self.children.is_empty() { + return Err(VerificationError::PolicyFailure( + "no child policies to verify".into(), + )); + } + + let results = self.children.iter().map(|policy| policy.verify(cert)); + let failures: Vec<_> = results + .filter_map(|result| result.err()) + .map(|err| err.to_string()) + .collect(); + + if failures.is_empty() { + Ok(()) + } else { + Err(VerificationError::PolicyFailure(format!( + "{} of {} policies failed:\n- {}", + failures.len(), + self.children.len(), + failures.join("\n- ") + ))) + } + } +} + +pub(crate) struct UnsafeNoOp; + +impl VerificationPolicy for UnsafeNoOp { + fn verify(&self, _cert: &x509_cert::Certificate) -> VerificationResult { + eprintln!("unsafe (no-op) verification policy used! no verification performed!"); + VerificationResult::Ok(()) + } +} + +/// Verifies the certificate's "identity", corresponding to the X.509v3 SAN. +/// Identities are verified modulo an OIDC issuer, so the issuer's URI +/// is also required. +/// +/// Supported SAN types include emails, URIs, and Sigstore-specific "other names". +pub struct Identity { + identity: String, + issuer: OIDCIssuer, +} + +impl Identity { + pub fn new(identity: A, issuer: B) -> Self + where + A: AsRef, + B: AsRef, + { + Self { + identity: identity.as_ref().to_owned(), + issuer: OIDCIssuer::new(issuer), + } + } +} + +impl VerificationPolicy for Identity { + fn verify(&self, cert: &x509_cert::Certificate) -> VerificationResult { + if let err @ Err(_) = self.issuer.verify(cert) { + return err; + } + + let (_, san): (bool, SubjectAltName) = match cert.tbs_certificate.get() { + Ok(Some(result)) => result, + _ => return Err(VerificationError::CertificateMalformed), + }; + + let names: Vec<_> = san + .0 + .iter() + .filter_map(|name| match name { + GeneralName::Rfc822Name(name) => Some(name.as_str()), + GeneralName::UniformResourceIdentifier(name) => Some(name.as_str()), + GeneralName::OtherName(name) if name.type_id == OTHERNAME_OID => { + std::str::from_utf8(name.value.value()).ok() + } + _ => None, + }) + .collect(); + + if names.contains(&self.identity.as_str()) { + Ok(()) + } else { + Err(VerificationError::PolicyFailure(format!( + "Certificate's SANs do not match {}; actual SANs: {}", + self.identity, + names.join(", ") + ))) + } + } +} diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs new file mode 100644 index 0000000000..2645b87919 --- /dev/null +++ b/src/verify/verifier.rs @@ -0,0 +1,197 @@ +// Copyright 2023 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::cell::OnceCell; + +use const_oid::db::rfc5280::ID_KP_CODE_SIGNING; +use pkcs8::der::Encode; +use webpki::types::UnixTime; +use x509_cert::ext::pkix::{ExtendedKeyUsage, KeyUsage}; + +use crate::{ + crypto::{CertificatePool, CosignVerificationKey, Signature}, + errors::Result as SigstoreResult, + rekor::apis::configuration::Configuration as RekorConfiguration, + tuf::{Repository, SigstoreRepository}, + verify::VerificationError, +}; + +use super::{models::VerificationMaterials, policy::VerificationPolicy, VerificationResult}; + +pub struct Verifier<'a, R: Repository> { + #[allow(dead_code)] + rekor_config: RekorConfiguration, + trust_repo: R, + cert_pool: OnceCell>, +} + +impl<'a, R: Repository> Verifier<'a, R> { + pub fn new(rekor_config: RekorConfiguration, trust_repo: R) -> SigstoreResult { + Ok(Self { + rekor_config, + cert_pool: Default::default(), + trust_repo, + }) + } + + /// TODO(tnytown): Evil (?) interior mutability hack to work around lifetime issues. + fn cert_pool(&'a self) -> SigstoreResult<&CertificatePool<'a>> { + let init_cert_pool = || { + let certs = self.trust_repo.fulcio_certs()?; + CertificatePool::from_certificates(certs, []) + }; + + let cert_pool = init_cert_pool()?; + Ok(self.cert_pool.get_or_init(|| cert_pool)) + } + + pub fn verify( + &'a self, + materials: VerificationMaterials, + policy: &impl VerificationPolicy, + ) -> VerificationResult { + let store = self + .cert_pool() + .expect("Failed to construct certificate pool"); + + // In order to verify an artifact, we need to achieve the following: + // + // 1) Verify that the signing certificate is signed by the certificate + // chain and that the signing certificate was valid at the time + // of signing. + // 2) Verify that the signing certificate belongs to the signer. + // 3) Verify that the artifact signature was signed by the public key in the + // signing certificate. + // 4) Verify that the Rekor entry is consistent with the other signing + // materials (preventing CVE-2022-36056) + // 5) Verify the inclusion proof supplied by Rekor for this artifact, + // if we're doing online verification. + // 6) Verify the Signed Entry Timestamp (SET) supplied by Rekor for this + // artifact. + // 7) Verify that the signing certificate was valid at the time of + // signing by comparing the expiry against the integrated timestamp. + + // 1) Verify that the signing certificate is signed by the root certificate and that the + // signing certificate was valid at the time of signing. + + // 1) Verify that the signing certificate is signed by the certificate + // chain and that the signing certificate was valid at the time + // of signing. + let issued_at = materials + .certificate + .tbs_certificate + .validity + .not_before + .to_unix_duration(); + let cert_der = &materials + .certificate + .to_der() + .expect("failed to DER-encode constructed Certificate!"); + store + .verify_cert_with_time(cert_der, UnixTime::since_unix_epoch(issued_at)) + .or(Err(VerificationError::CertificateVerificationFailure))?; + + // 2) Verify that the signing certificate belongs to the signer. + + // TODO(tnytown): How likely is a malformed certificate in this position? Do we want to + // account for it and create an error type as opposed to unwrapping? + let (_, key_usage_ext): (bool, KeyUsage) = materials + .certificate + .tbs_certificate + .get() + .expect("Malformed certificate") + .expect("Malformed certificate"); + + if !key_usage_ext.digital_signature() { + return Err(VerificationError::CertificateTypeError( + "Key usage is not of type `digital signature`".into(), + )); + } + + let (_, extended_key_usage_ext): (bool, ExtendedKeyUsage) = materials + .certificate + .tbs_certificate + .get() + .expect("Malformed certificate") + .expect("Malformed certificate"); + + if !extended_key_usage_ext.0.contains(&ID_KP_CODE_SIGNING) { + return Err(VerificationError::CertificateTypeError( + "Extended key usage does not contain `code signing`".into(), + )); + } + + policy.verify(&materials.certificate)?; + + // 3) Verify that the signature was signed by the public key in the signing certificate + let signing_key: SigstoreResult = (&materials + .certificate + .tbs_certificate + .subject_public_key_info) + .try_into(); + + let signing_key = + signing_key.expect("Malformed certificate (cannot deserialize public key)"); + + signing_key + .verify_prehash( + Signature::Raw(&materials.signature), + &materials.input_digest, + ) + .or(Err(VerificationError::SignatureVerificationFailure))?; + + // 4) Verify that the Rekor entry is consistent with the other signing + // materials + let log_entry = materials.rekor_entry(); + + // 5) Verify the inclusion proof supplied by Rekor for this artifact, + // if we're doing online verification. + // TODO(tnytown): Merkle inclusion + + // 6) Verify the Signed Entry Timestamp (SET) supplied by Rekor for this + // artifact. + // TODO(tnytown) SET verification + + // 7) Verify that the signing certificate was valid at the time of + // signing by comparing the expiry against the integrated timestamp. + let integrated_time = log_entry.integrated_time as u64; + let not_before = materials + .certificate + .tbs_certificate + .validity + .not_before + .to_unix_duration() + .as_secs(); + let not_after = materials + .certificate + .tbs_certificate + .validity + .not_after + .to_unix_duration() + .as_secs(); + if !(not_before <= integrated_time && integrated_time <= not_after) { + return Err(VerificationError::CertificateExpired); + } + + Ok(()) + } +} + +impl<'a> Verifier<'a, SigstoreRepository> { + pub fn production() -> SigstoreResult> { + let updater = SigstoreRepository::new(None)?; + + Verifier::<'a, SigstoreRepository>::new(Default::default(), updater) + } +} diff --git a/tests/conformance/Cargo.toml b/tests/conformance/Cargo.toml index 6c0c5d16a9..b5d896c3e3 100644 --- a/tests/conformance/Cargo.toml +++ b/tests/conformance/Cargo.toml @@ -8,6 +8,8 @@ license = "Apache-2.0" [dependencies] clap = { version = "4.0.8", features = ["derive"] } +anyhow = "1.0.75" +serde_json = "1.0.107" sigstore = { path = "../../" } [[bin]] diff --git a/tests/conformance/conformance.rs b/tests/conformance/conformance.rs index c52c822fe1..801cc6b064 100644 --- a/tests/conformance/conformance.rs +++ b/tests/conformance/conformance.rs @@ -16,7 +16,16 @@ // CLI implemented to specification: // https://github.com/sigstore/sigstore-conformance/blob/main/docs/cli_protocol.md +use std::{fs, process::exit}; + +use anyhow::anyhow; use clap::{Parser, Subcommand}; +use sigstore::{ + oauth::IdentityToken, + sign::SigningContext, + verify::VerificationMaterials, + verify::{policy, Verifier}, +}; #[derive(Parser, Debug)] struct Cli { @@ -106,4 +115,62 @@ struct VerifyBundle { fn main() { let cli = Cli::parse(); + + let result = match cli.command { + Commands::SignBundle(args) => sign_bundle(args), + Commands::VerifyBundle(args) => verify_bundle(args), + _ => unimplemented!("sig/cert commands"), + }; + + if let Err(error) = result { + eprintln!("Operation failed:\n{error:?}"); + exit(-1); + } + + eprintln!("Operation succeeded!"); +} + +fn sign_bundle(args: SignBundle) -> anyhow::Result<()> { + let SignBundle { + identity_token, + bundle, + artifact, + } = args; + let identity_token = IdentityToken::try_from(identity_token.as_str())?; + let bundle = fs::File::create(bundle)?; + let mut artifact = fs::File::open(artifact)?; + + let context = SigningContext::production(); + let signer = context.signer(identity_token); + + let signing_artifact = signer.sign(&mut artifact)?; + let bundle_data = signing_artifact.to_bundle(); + + serde_json::to_writer(bundle, &bundle_data)?; + + Ok(()) +} + +fn verify_bundle(args: VerifyBundle) -> anyhow::Result<()> { + let VerifyBundle { + bundle, + certificate_identity, + certificate_oidc_issuer, + artifact, + } = args; + let bundle = fs::File::open(bundle)?; + let mut artifact = fs::File::open(artifact)?; + + let bundle: sigstore::Bundle = serde_json::from_reader(bundle)?; + let materials = VerificationMaterials::from_bundle(&mut artifact, bundle, true) + .ok_or(anyhow!("Unable to construct VerificationMaterials"))?; + + let verifier = Verifier::production()?; + + verifier.verify( + materials, + &policy::Identity::new(certificate_identity, certificate_oidc_issuer), + )?; + + Ok(()) } diff --git a/tests/data/repository/1.registry.npmjs.org.json b/tests/data/repository/1.registry.npmjs.org.json new file mode 100644 index 0000000000..1c8ec2165b --- /dev/null +++ b/tests/data/repository/1.registry.npmjs.org.json @@ -0,0 +1,23 @@ +{ + "signed": { + "_type": "targets", + "spec_version": "1.0", + "version": 1, + "expires": "2024-09-29T16:47:20Z", + "targets": { + "registry.npmjs.org/keys.json": { + "length": 1017, + "hashes": { + "sha256": "7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426", + "sha512": "881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699" + } + } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "3044022059bf01a64dd2793d5b630e26d7b6e455b0d6d8b47c23049ae856a122e5cec2ab022068b99b8bb39457e53d500f698cb43f9e640958ed26e5d3a47c29619df61889bc" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/1.root.json b/tests/data/repository/1.root.json index dcc71f963a..4ca0da87fe 100644 --- a/tests/data/repository/1.root.json +++ b/tests/data/repository/1.root.json @@ -1,130 +1,65 @@ { - "signatures": [ - { - "keyid": "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "sig": "30450221008a35d51da0f845301a5eac98ad0df00a934f59b709c1eaf81c86be734d9356f80220742942325599749800f52675f6efe124345980a2a636c0dc76f9caf9fc3123b0" - }, - { - "keyid": "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "sig": "3045022100ef9157ece2a09baec1eab80adfc00b04da20b1f9a0d1b47c5dabc4506719ef2c022074f72acd57398e4ddc8c2a5040df902961e9615dca48f3fbe38cbb506e500066" - }, - { - "keyid": "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "sig": "30450220420fdc9a09cd069b8b15fd8db9cedf7d0dee75871bd1cfee77c926d4120a770002210097553b5ad0d6b4a13902ed37509638bb63a9009f78230cd56c802909ffbfead7" - }, - { - "keyid": "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "sig": "304502202aaf32e66f90752f658672b085ecfe45cc1ad31ee6cf5c9ad05f3267685f8d88022100b5df02acdaa371123db9d7a42219553fe079b230b168833e951be7ee56ded347" - }, - { - "keyid": "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209", - "sig": "304402205d420c7d05c58980c1c9f7d221f53b5334aae27a447d2a91c2ceddd685269749022039ec83e51f8e1779d7f0142dfa4a5bbecfe327fc0b91b7416090fea2416fd53a" - } - ], "signed": { "_type": "root", - "consistent_snapshot": false, - "expires": "2021-12-18T13:28:12.99008-06:00", + "spec_version": "1.0", + "version": 1, + "expires": "2024-09-29T16:47:17Z", "keys": { - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04cbc5cab2684160323c25cd06c3307178a6b1d1c9b949328453ae473c5ba7527e35b13f298b41633382241f3fd8526c262d43b45adee5c618fa0642c82b8a9803" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62": { + "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": [ "sha256", "sha512" ], - "keytype": "ecdsa-sha2-nistp256", "keyval": { - "public": "04a71aacd835dc170ba6db3fa33a1a33dee751d4f8b0217b805b9bd3242921ee93672fdcfd840576c5bb0dc0ed815edf394c1ee48c2b5e02485e59bfc512f3adc7" - }, - "scheme": "ecdsa-sha2-nistp256" + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" + } }, - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04117b33dd265715bf23315e368faa499728db8d1f0a377070a1c7b1aba2cc21be6ab1628e42f2cdd7a35479f2dce07b303a8ba646c55569a8d2a504ba7e86e447" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], + "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda": { "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04cc1cd53a61c23e88cc54b488dfae168a257c34fac3e88811c55962b24cffbfecb724447999c54670e365883716302e49da57c79a33cd3e16f81fbc66f0bcdf48" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209": { + "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": [ "sha256", "sha512" ], - "keytype": "ecdsa-sha2-nistp256", "keyval": { - "public": "048a78a44ac01099890d787e5e62afc29c8ccb69a70ec6549a6b04033b0a8acbfb42ab1ab9c713d225cdb52b858886cf46c8e90a7f3b9e6371882f370c259e1c5b" - }, - "scheme": "ecdsa-sha2-nistp256" + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL3vL/VeaH6nBbo4rekyO4cc/QthS\n+nlyJXCXSnyIMAtLmVTa8Pf0qG6YIVaR0TmLkyk9YoSVsZakxuMTuaEwrg==\n-----END PUBLIC KEY-----\n" + } } }, "roles": { "root": { "keyids": [ - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209" + "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" ], - "threshold": 3 + "threshold": 1 }, "snapshot": { "keyids": [ - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209" + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" ], - "threshold": 3 + "threshold": 1 }, "targets": { "keyids": [ - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209" + "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" ], - "threshold": 3 + "threshold": 1 }, "timestamp": { "keyids": [ - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209" + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" ], - "threshold": 3 + "threshold": 1 } }, - "spec_version": "1.0", - "version": 1 - } + "consistent_snapshot": true + }, + "signatures": [ + { + "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", + "sig": "304602210085927cdb96e1d9d0876bfc26b6ceea7421a54f959e30b9af3e12d31f6c750543022100dde611b58a1f2b9fb26c43767138c68f4422cdeb898c8b63f3f0193791030d12" + } + ] } \ No newline at end of file diff --git a/tests/data/repository/1.snapshot.json b/tests/data/repository/1.snapshot.json new file mode 100644 index 0000000000..fcb179878c --- /dev/null +++ b/tests/data/repository/1.snapshot.json @@ -0,0 +1,32 @@ +{ + "signed": { + "_type": "snapshot", + "spec_version": "1.0", + "version": 1, + "expires": "2024-04-19T16:47:48Z", + "meta": { + "registry.npmjs.org.json": { + "length": 713, + "hashes": { + "sha256": "17b361687dbb401c2d51d7ce21688d13547eae7f8e7b2183b7dd6d94fa675705", + "sha512": "3f60a08cdbab650ece48ded43b54943dc816580fdb2f5a2a20c30e878eb2489ab817f0308666cac80da03d75d6f5b71959431b1ba7794335fece8a4ed635eb4d" + }, + "version": 1 + }, + "targets.json": { + "length": 4518, + "hashes": { + "sha256": "cc62e5fb1644717c7429c82b6a1cbd085008f9a2e07aad38573f8fdf9d55386c", + "sha512": "5709bc76bc35da403a9a0a5ec96890db49e797c986eda9e5f7973938dbccad96838c8136617c91f5218cfd919d93745d3942ca6d50a52b5fd0e662e6876b395f" + }, + "version": 1 + } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "304602210082d244d5dab0c20ee07b3229964beffaa8bb0bdf4c5107e2f764619878d124a2022100e7c50116ef636c41348ec49a7502f1c98037238b9c717ee781b62c5154f5a1f0" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/1.targets.json b/tests/data/repository/1.targets.json new file mode 100644 index 0000000000..6844bad771 --- /dev/null +++ b/tests/data/repository/1.targets.json @@ -0,0 +1,148 @@ +{ + "signed": { + "_type": "targets", + "spec_version": "1.0", + "version": 1, + "expires": "2024-09-29T16:47:20Z", + "targets": { + "artifact.pub": { + "length": 177, + "hashes": { + "sha256": "59ebf97a9850aecec4bc39c1f5c1dc46e6490a6b5fd2a6cacdcac0c3a6fc4cbf", + "sha512": "308fd1d1d95d7f80aa33b837795251cc3e886792982275e062409e13e4e236ffc34d676682aa96fdc751414de99c864bf132dde71581fa651c6343905e3bf988" + }, + "custom": { + "sigstore": { + "status": "Active", + "usage": "Unknown" + } + } + }, + "ctfe.pub": { + "length": 177, + "hashes": { + "sha256": "7fcb94a5d0ed541260473b990b99a6c39864c1fb16f3f3e594a5a3cebbfe138a", + "sha512": "4b20747d1afe2544238ad38cc0cc3010921b177d60ac743767e0ef675b915489bd01a36606c0ff83c06448622d7160f0d866c83d20f0c0f44653dcc3f9aa0bd4" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstore.dev/test", + "usage": "CTFE" + } + } + }, + "ctfe_2022.pub": { + "length": 178, + "hashes": { + "sha256": "270488a309d22e804eeb245493e87c667658d749006b9fee9cc614572d4fbbdc", + "sha512": "e83fa4f427b24ee7728637fad1b4aa45ebde2ba02751fa860694b1bb16059a490328f9985e51cc70e4d237545315a1bc866dc4fdeef2f6248d99cc7a6077bf85" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstore.dev/2022", + "usage": "CTFE" + } + } + }, + "fulcio.crt.pem": { + "length": 744, + "hashes": { + "sha256": "f360c53b2e13495a628b9b8096455badcb6d375b185c4816d95a5d746ff29908", + "sha512": "0713252a7fd17f7f3ab12f88a64accf2eb14b8ad40ca711d7fe8b4ecba3b24db9e9dffadb997b196d3867b8f9ff217faf930d80e4dab4e235c7fc3f07be69224" + }, + "custom": { + "sigstore": { + "status": "Expired", + "uri": "https://fulcio.sigstore.dev", + "usage": "Fulcio" + } + } + }, + "fulcio_intermediate_v1.crt.pem": { + "length": 789, + "hashes": { + "sha256": "f8cbecf186db7714624a5f4e99da31a917cbef70a94dd6921f5c3ca969dfe30a", + "sha512": "0f99f47dbc26c5f1e3cba0bfd9af4245a26e5cb735d6ef005792ec7e603f66fdb897de985973a6e50940ca7eff5e1849719e967b5ad2dac74a29115a41cf6f21" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://fulcio.sigstore.dev", + "usage": "Fulcio" + } + } + }, + "fulcio_v1.crt.pem": { + "length": 740, + "hashes": { + "sha256": "f989aa23def87c549404eadba767768d2a3c8d6d30a8b793f9f518a8eafd2cf5", + "sha512": "f2e33a6dc208cee1f51d33bbea675ab0f0ced269617497985f9a0680689ee7073e4b6f8fef64c91bda590d30c129b3070dddce824c05bc165ac9802f0705cab6" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://fulcio.sigstore.dev", + "usage": "Fulcio" + } + } + }, + "rekor.pub": { + "length": 178, + "hashes": { + "sha256": "dce5ef715502ec9f3cdfd11f8cc384b31a6141023d3e7595e9908a81cb6241bd", + "sha512": "0ae7705e02db33e814329746a4a0e5603c5bdcd91c96d072158d71011a2695788866565a2fec0fe363eb72cbcaeda39e54c5fe8d416daf9f3101fdba4217ef35" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://rekor.sigstore.dev", + "usage": "Rekor" + } + } + }, + "trusted_root.json": { + "length": 4567, + "hashes": { + "sha256": "cec894ad77f79b1cb324150f6363012bcef7492954f3ab9134f932e6aa2e2e20", + "sha512": "08be2fd75c19e654caad30852847c566f97e6245f2bbcc54d347d6bdec7e879135e3395b5633b9e3b85d739fdb9b4eb8c09ddc70495792bc2ea65c8caf770d27" + } + } + }, + "delegations": { + "keys": { + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" + } + } + }, + "roles": [ + { + "name": "registry.npmjs.org", + "keyids": [ + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" + ], + "threshold": 1, + "terminating": true, + "paths": [ + "registry.npmjs.org/*" + ] + } + ] + } + }, + "signatures": [ + { + "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", + "sig": "304402201662b260e99e59f7271bd9e3fb01aa47a399bef8c5ec808bea6d40ae2d93625d022042fd2a275d84196dc50e17ca9c9408a34349372410febc7217415b11eb978bbb" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/2.registry.npmjs.org.json b/tests/data/repository/2.registry.npmjs.org.json new file mode 100644 index 0000000000..d53f15267b --- /dev/null +++ b/tests/data/repository/2.registry.npmjs.org.json @@ -0,0 +1,23 @@ +{ + "signed": { + "_type": "targets", + "spec_version": "1.0", + "version": 2, + "expires": "2028-09-29T21:10:55Z", + "targets": { + "registry.npmjs.org/keys.json": { + "length": 1017, + "hashes": { + "sha256": "7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426", + "sha512": "881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699" + } + } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "3045022057b9fc8afd9feaf45cf3173d3420fdcd6b68c22e4ef7b47e80a6887e1f20246c0221009f39c42fac630ab354c5197288c9a82ab6d46a59b423f81fff719da57cff16ab" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/2.root.json b/tests/data/repository/2.root.json index 386ebe62c1..f848d7d846 100644 --- a/tests/data/repository/2.root.json +++ b/tests/data/repository/2.root.json @@ -1,144 +1,65 @@ { - "signatures": [ - { - "keyid": "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "sig": "3046022100d3ea59490b253beae0926c6fa63f54336dea1ed700555be9f27ff55cd347639c0221009157d1ba012cead81948a4ab777d355451d57f5c4a2d333fc68d2e3f358093c2" - }, - { - "keyid": "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "sig": "304502206eaef40564403ce572c6d062e0c9b0aab5e0223576133e081e1b495e8deb9efd02210080fd6f3464d759601b4afec596bbd5952f3a224cd06ed1cdfc3c399118752ba2" - }, - { - "keyid": "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "sig": "304502207baace02f56d8e6069f10b6ff098a26e7f53a7f9324ad62cffa0557bdeb9036c022100fb3032baaa090d0040c3f2fd872571c84479309b773208601d65948df87a9720" - }, - { - "keyid": "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "sig": "304402205180c01905505dd88acd7a2dad979dd75c979b3722513a7bdedac88c6ae8dbeb022056d1ddf7a192f0b1c2c90ff487de2fb3ec9f0c03f66ea937c78d3b6a493504ca" - }, - { - "keyid": "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209", - "sig": "3046022100c8806d4647c514d80fd8f707d3369444c4fd1d0812a2d25f828e564c99790e3f022100bb51f12e862ef17a7d3da2ac103bebc5c7e792237006c4cafacd76267b249c2f" - } - ], "signed": { "_type": "root", - "consistent_snapshot": false, - "expires": "2022-05-11T19:09:02.663975009Z", + "spec_version": "1.0", + "version": 2, + "expires": "2028-09-29T21:10:11Z", "keys": { - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04cbc5cab2684160323c25cd06c3307178a6b1d1c9b949328453ae473c5ba7527e35b13f298b41633382241f3fd8526c262d43b45adee5c618fa0642c82b8a9803" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d": { + "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": [ "sha256", "sha512" ], - "keytype": "ecdsa-sha2-nistp256", "keyval": { - "public": "04fa1a3e42f2300cd3c5487a61509348feb1e936920fef2f83b7cd5dbe7ba045f538725ab8f18a666e6233edb7e0db8766c8dc336633449c5e1bbe0c182b02df0b" - }, - "scheme": "ecdsa-sha2-nistp256" + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" + } }, - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], + "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda": { "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04a71aacd835dc170ba6db3fa33a1a33dee751d4f8b0217b805b9bd3242921ee93672fdcfd840576c5bb0dc0ed815edf394c1ee48c2b5e02485e59bfc512f3adc7" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b": { + "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": [ "sha256", "sha512" ], - "keytype": "ecdsa-sha2-nistp256", "keyval": { - "public": "04117b33dd265715bf23315e368faa499728db8d1f0a377070a1c7b1aba2cc21be6ab1628e42f2cdd7a35479f2dce07b303a8ba646c55569a8d2a504ba7e86e447" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04cc1cd53a61c23e88cc54b488dfae168a257c34fac3e88811c55962b24cffbfecb724447999c54670e365883716302e49da57c79a33cd3e16f81fbc66f0bcdf48" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "048a78a44ac01099890d787e5e62afc29c8ccb69a70ec6549a6b04033b0a8acbfb42ab1ab9c713d225cdb52b858886cf46c8e90a7f3b9e6371882f370c259e1c5b" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "fc61191ba8a516fe386c7d6c97d918e1d241e1589729add09b122725b8c32451": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "044c7793ab74b9ddd713054e587b8d9c75c5f6025633d0fef7ca855ed5b8d5a474b23598fe33eb4a63630d526f74d4bdaec8adcb51993ed65652d651d7c49203eb" - }, - "scheme": "ecdsa-sha2-nistp256" + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL3vL/VeaH6nBbo4rekyO4cc/QthS\n+nlyJXCXSnyIMAtLmVTa8Pf0qG6YIVaR0TmLkyk9YoSVsZakxuMTuaEwrg==\n-----END PUBLIC KEY-----\n" + } } }, "roles": { "root": { "keyids": [ - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209" + "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" ], - "threshold": 3 + "threshold": 1 }, "snapshot": { "keyids": [ - "fc61191ba8a516fe386c7d6c97d918e1d241e1589729add09b122725b8c32451" + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" ], "threshold": 1 }, "targets": { "keyids": [ - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209" + "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" ], - "threshold": 3 + "threshold": 1 }, "timestamp": { "keyids": [ - "b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d" + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" ], "threshold": 1 } }, - "spec_version": "1.0", - "version": 2 - } + "consistent_snapshot": true + }, + "signatures": [ + { + "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", + "sig": "3045022057bbd23dd9f69f8280c5e5d2b0a0b1ace98d6d8efa0f59ef0a3190188f6e2c89022100b39e6c24091c4271d2b8b4cfa75e6120638b276fbffddda8da5bca1778c8f08c" + } + ] } \ No newline at end of file diff --git a/tests/data/repository/2.snapshot.json b/tests/data/repository/2.snapshot.json new file mode 100644 index 0000000000..6c1e4dd147 --- /dev/null +++ b/tests/data/repository/2.snapshot.json @@ -0,0 +1,32 @@ +{ + "signed": { + "_type": "snapshot", + "spec_version": "1.0", + "version": 2, + "expires": "2028-04-19T21:11:16Z", + "meta": { + "registry.npmjs.org.json": { + "length": 715, + "hashes": { + "sha256": "4dc55b2b468b0d1c9629c457c5cfce2cc1c330c59c5a7cf71cb7549f1ef76f1d", + "sha512": "278f4b6112db9d4bd9366e1717cf710ad7eacf44605fd4f894c3374fc5dff850a1a03c24c4a885d050a4ac1a86fa6929537fae12d8c2864c8e0c239b382d5556" + }, + "version": 2 + }, + "targets.json": { + "length": 4120, + "hashes": { + "sha256": "095d093de09350cec021828f49361688b5dd692486ad7bfb03d4150b3269ef8a", + "sha512": "97b9c75f49fb41eaf2f33c5f58b125febc3bbecd4c97f6edd0901423a231e4d0c5760d4780bc180a364d7198b5e0710f07ee0abf84dcd163fe3348d6bce26fab" + }, + "version": 2 + } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "3044022013bf1032cf0a37d9f88ab9d33d0abb7a932efd95fadbc354fc21a807c7be29ef0220677e651c3e67e0728591faa20c5a09b8a16953038c3ceeffb7d9cfec766b3245" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/2.targets.json b/tests/data/repository/2.targets.json new file mode 100644 index 0000000000..dea42487f0 --- /dev/null +++ b/tests/data/repository/2.targets.json @@ -0,0 +1,135 @@ +{ + "signed": { + "_type": "targets", + "spec_version": "1.0", + "version": 2, + "expires": "2028-09-29T21:10:55Z", + "targets": { + "ctfe.pub": { + "length": 775, + "hashes": { + "sha256": "bd7a6812a1f239dfddbbb19d36c7423d21510da56d466ba5018401959cd66037", + "sha512": "b861189e48df51186a39612230fba6b02af951f7b35ad9375e8ca182d0e085d470e26d69f7cd4d7450a0f223991e8e5a4ddf8f1968caa15255de8e37035af43a" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstage.dev/test", + "usage": "CTFE" + } + } + }, + "ctfe_2022.pub": { + "length": 178, + "hashes": { + "sha256": "910d899c7763563095a0fe684c8477573fedc19a78586de6ecfbfd8f289f5423", + "sha512": "ab975a75600fc366a837536d0dcba841b755552d21bb114498ff8ac9d2403f76643f5b91269bce5d124a365514719a3edee9dcc2b046cb173f51af659911fcd3" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstage.dev/2022", + "usage": "CTFE" + } + } + }, + "ctfe_2022_2.pub": { + "length": 178, + "hashes": { + "sha256": "7054b4f15f969daca1c242bb9e77527abaf0b9acf9818a2a35144e4b32b20dc6", + "sha512": "3d035f94e1b14ac84627a28afdbed9a34861fb84239f76d73aa1a99f52262bfd95c4fa0ee71f1fd7e3bfb998d89cd5e0f0eafcff9fa7fa87c6e23484fc1e0cec" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstage.dev/2022-2", + "usage": "CTFE" + } + } + }, + "fulcio.crt.pem": { + "length": 741, + "hashes": { + "sha256": "0e6b0442485ad552bea5f62f11c29e2acfda35307d7538430b4cc1dbef49bff1", + "sha512": "c69ae618883a0c89c282c0943a1ad0c16b0a7788f74e47a1adefc631dac48a0c4449d8c3de7455ae7d772e43c4a87e341f180b0614a46a86006969f8a7b84532" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://fulcio.sigstage.dev", + "usage": "Fulcio" + } + } + }, + "fulcio_intermediate.crt.pem": { + "length": 790, + "hashes": { + "sha256": "782868913fe13c385105ddf33e827191386f58da40a931f2075a7e27b1b6ac7b", + "sha512": "90659875a02f73d1026055427c6d857c556e410e23748ff88aeb493227610fd2f5fbdd95ef2a21565f91438dfb3e073f50c4c9dd06f9a601b5d9b064d5cb60b4" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://fulcio.sigstage.dev", + "usage": "Fulcio" + } + } + }, + "rekor.pub": { + "length": 178, + "hashes": { + "sha256": "1d80b8f72505a43e65e6e125247cd508f61b459dc457c1d1bcb78d96e1760959", + "sha512": "09ab08698a67354a95d3b8897d9ce7eaef05f06f5ed5f0202d79c228579858ecc5816b7e1b7cc6786abe7d6aaa758e1fcb05900cb749235186c3bf9522d6d7ce" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://rekor.sigstage.dev", + "usage": "Rekor" + } + } + }, + "trusted_root.json": { + "length": 4521, + "hashes": { + "sha256": "6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b", + "sha512": "fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963" + } + } + }, + "delegations": { + "keys": { + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" + } + } + }, + "roles": [ + { + "name": "registry.npmjs.org", + "keyids": [ + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" + ], + "threshold": 1, + "terminating": true, + "paths": [ + "registry.npmjs.org/*" + ] + } + ] + } + }, + "signatures": [ + { + "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", + "sig": "304502210090b089087d1b17b2517c464b7774d76d3ea558ffca874eed63ccbee8f6bc3b76022022b56f551bcd0ac8a9c35cd0724ac5b00b4984544cbf812f47f276a9b48db8db" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/registry.npmjs.org.json b/tests/data/repository/registry.npmjs.org.json new file mode 100644 index 0000000000..d53f15267b --- /dev/null +++ b/tests/data/repository/registry.npmjs.org.json @@ -0,0 +1,23 @@ +{ + "signed": { + "_type": "targets", + "spec_version": "1.0", + "version": 2, + "expires": "2028-09-29T21:10:55Z", + "targets": { + "registry.npmjs.org/keys.json": { + "length": 1017, + "hashes": { + "sha256": "7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426", + "sha512": "881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699" + } + } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "3045022057b9fc8afd9feaf45cf3173d3420fdcd6b68c22e4ef7b47e80a6887e1f20246c0221009f39c42fac630ab354c5197288c9a82ab6d46a59b423f81fff719da57cff16ab" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/rekor.json b/tests/data/repository/rekor.json deleted file mode 100644 index f86930d537..0000000000 --- a/tests/data/repository/rekor.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "signatures": [ - { - "keyid": "ae0c689c6347ada7359df48934991f4e013193d6ddf3482a5ffb293f74f3b217", - "sig": "3045022076eadd73f6664bac5cc91f12d3a7ddcdd53f9bde661f147651196ff66e7235d1022100f7b3143792405f9e8a75331a05d4128bdf083de302801e99c3d027919a4b03da" - } - ], - "signed": { - "_type": "targets", - "expires": "2022-05-11T19:10:11Z", - "spec_version": "1.0", - "targets": { - "rekor.0.pub": { - "hashes": { - "sha256": "dce5ef715502ec9f3cdfd11f8cc384b31a6141023d3e7595e9908a81cb6241bd", - "sha512": "0ae7705e02db33e814329746a4a0e5603c5bdcd91c96d072158d71011a2695788866565a2fec0fe363eb72cbcaeda39e54c5fe8d416daf9f3101fdba4217ef35" - }, - "length": 178 - } - }, - "version": 1 - } -} \ No newline at end of file diff --git a/tests/data/repository/root.json b/tests/data/repository/root.json index 386ebe62c1..f848d7d846 100644 --- a/tests/data/repository/root.json +++ b/tests/data/repository/root.json @@ -1,144 +1,65 @@ { - "signatures": [ - { - "keyid": "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "sig": "3046022100d3ea59490b253beae0926c6fa63f54336dea1ed700555be9f27ff55cd347639c0221009157d1ba012cead81948a4ab777d355451d57f5c4a2d333fc68d2e3f358093c2" - }, - { - "keyid": "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "sig": "304502206eaef40564403ce572c6d062e0c9b0aab5e0223576133e081e1b495e8deb9efd02210080fd6f3464d759601b4afec596bbd5952f3a224cd06ed1cdfc3c399118752ba2" - }, - { - "keyid": "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "sig": "304502207baace02f56d8e6069f10b6ff098a26e7f53a7f9324ad62cffa0557bdeb9036c022100fb3032baaa090d0040c3f2fd872571c84479309b773208601d65948df87a9720" - }, - { - "keyid": "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "sig": "304402205180c01905505dd88acd7a2dad979dd75c979b3722513a7bdedac88c6ae8dbeb022056d1ddf7a192f0b1c2c90ff487de2fb3ec9f0c03f66ea937c78d3b6a493504ca" - }, - { - "keyid": "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209", - "sig": "3046022100c8806d4647c514d80fd8f707d3369444c4fd1d0812a2d25f828e564c99790e3f022100bb51f12e862ef17a7d3da2ac103bebc5c7e792237006c4cafacd76267b249c2f" - } - ], "signed": { "_type": "root", - "consistent_snapshot": false, - "expires": "2022-05-11T19:09:02.663975009Z", + "spec_version": "1.0", + "version": 2, + "expires": "2028-09-29T21:10:11Z", "keys": { - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04cbc5cab2684160323c25cd06c3307178a6b1d1c9b949328453ae473c5ba7527e35b13f298b41633382241f3fd8526c262d43b45adee5c618fa0642c82b8a9803" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d": { + "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": [ "sha256", "sha512" ], - "keytype": "ecdsa-sha2-nistp256", "keyval": { - "public": "04fa1a3e42f2300cd3c5487a61509348feb1e936920fef2f83b7cd5dbe7ba045f538725ab8f18a666e6233edb7e0db8766c8dc336633449c5e1bbe0c182b02df0b" - }, - "scheme": "ecdsa-sha2-nistp256" + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" + } }, - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], + "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda": { "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04a71aacd835dc170ba6db3fa33a1a33dee751d4f8b0217b805b9bd3242921ee93672fdcfd840576c5bb0dc0ed815edf394c1ee48c2b5e02485e59bfc512f3adc7" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b": { + "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": [ "sha256", "sha512" ], - "keytype": "ecdsa-sha2-nistp256", "keyval": { - "public": "04117b33dd265715bf23315e368faa499728db8d1f0a377070a1c7b1aba2cc21be6ab1628e42f2cdd7a35479f2dce07b303a8ba646c55569a8d2a504ba7e86e447" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04cc1cd53a61c23e88cc54b488dfae168a257c34fac3e88811c55962b24cffbfecb724447999c54670e365883716302e49da57c79a33cd3e16f81fbc66f0bcdf48" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "048a78a44ac01099890d787e5e62afc29c8ccb69a70ec6549a6b04033b0a8acbfb42ab1ab9c713d225cdb52b858886cf46c8e90a7f3b9e6371882f370c259e1c5b" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "fc61191ba8a516fe386c7d6c97d918e1d241e1589729add09b122725b8c32451": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "044c7793ab74b9ddd713054e587b8d9c75c5f6025633d0fef7ca855ed5b8d5a474b23598fe33eb4a63630d526f74d4bdaec8adcb51993ed65652d651d7c49203eb" - }, - "scheme": "ecdsa-sha2-nistp256" + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL3vL/VeaH6nBbo4rekyO4cc/QthS\n+nlyJXCXSnyIMAtLmVTa8Pf0qG6YIVaR0TmLkyk9YoSVsZakxuMTuaEwrg==\n-----END PUBLIC KEY-----\n" + } } }, "roles": { "root": { "keyids": [ - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209" + "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" ], - "threshold": 3 + "threshold": 1 }, "snapshot": { "keyids": [ - "fc61191ba8a516fe386c7d6c97d918e1d241e1589729add09b122725b8c32451" + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" ], "threshold": 1 }, "targets": { "keyids": [ - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209" + "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" ], - "threshold": 3 + "threshold": 1 }, "timestamp": { "keyids": [ - "b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d" + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" ], "threshold": 1 } }, - "spec_version": "1.0", - "version": 2 - } + "consistent_snapshot": true + }, + "signatures": [ + { + "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", + "sig": "3045022057bbd23dd9f69f8280c5e5d2b0a0b1ace98d6d8efa0f59ef0a3190188f6e2c89022100b39e6c24091c4271d2b8b4cfa75e6120638b276fbffddda8da5bca1778c8f08c" + } + ] } \ No newline at end of file diff --git a/tests/data/repository/snapshot.json b/tests/data/repository/snapshot.json index 61636531c6..6c1e4dd147 100644 --- a/tests/data/repository/snapshot.json +++ b/tests/data/repository/snapshot.json @@ -1,48 +1,32 @@ { - "signatures": [ - { - "keyid": "fc61191ba8a516fe386c7d6c97d918e1d241e1589729add09b122725b8c32451", - "sig": "3046022100f59f6f92d8c61519afd0de0642ff45419ac960954cf412549874c247c6ae509902210085da85c9df818c3072c0b7744b75e92d2ee521402d4bac77c985b8fc6d138e41" - } - ], "signed": { "_type": "snapshot", - "expires": "2022-01-05T00:40:06Z", + "spec_version": "1.0", + "version": 2, + "expires": "2028-04-19T21:11:16Z", "meta": { - "rekor.json": { + "registry.npmjs.org.json": { + "length": 715, "hashes": { - "sha256": "a7412a87f8d7b330e0380b19a4a76c00357c39a1aa7f56fd87445d4e12faafe4", - "sha512": "720cb3c42bac50c5bc3cb7076e730301ef29f1893ea52e25f9393fc05851c7a531638c42d9fc992969805982a2bf51d676e33d28a7382ea589b5a9f87474c63f" + "sha256": "4dc55b2b468b0d1c9629c457c5cfce2cc1c330c59c5a7cf71cb7549f1ef76f1d", + "sha512": "278f4b6112db9d4bd9366e1717cf710ad7eacf44605fd4f894c3374fc5dff850a1a03c24c4a885d050a4ac1a86fa6929537fae12d8c2864c8e0c239b382d5556" }, - "length": 697, - "version": 1 - }, - "root.json": { - "hashes": { - "sha256": "f5ad897c9414cca99629f400ac3585e41bd8ebb44c5af07fb08dd636a9eced9c", - "sha512": "7445ddfdd338ef786c324fc3d68f75be28cb95b7fb581d2a383e3e5dde18aa17029a5636ec0a22e9631931bbcb34057788311718ea41e21e7cdd3c0de13ede42" - }, - "length": 5297, "version": 2 }, - "staging.json": { - "hashes": { - "sha256": "c7f32379c2a76f0ec0af84e86794a8f4fe285e44fb62f336d598810dccdc7343", - "sha512": "5462cb15fe5248a12cc12387a732ad43caf42391361f36113ea3d4b7e5e193cdf39fbe91c309c0691134377cb83afeba50cf6d711537d8280ce16ce9cd8752ba" - }, - "length": 399, - "version": 1 - }, "targets.json": { + "length": 4120, "hashes": { - "sha256": "18d10c07c8d6bd7484772b02dcc988d0abf8a0fa379d5893a502410590c17fe6", - "sha512": "c2ba2a84820288997c8fae264776df7b262dde97c4f9e0320ad354879ce5afabd1d43494734fecffd23253442a14cfe217787de8b65cf7fd1f03130b72a0767c" + "sha256": "095d093de09350cec021828f49361688b5dd692486ad7bfb03d4150b3269ef8a", + "sha512": "97b9c75f49fb41eaf2f33c5f58b125febc3bbecd4c97f6edd0901423a231e4d0c5760d4780bc180a364d7198b5e0710f07ee0abf84dcd163fe3348d6bce26fab" }, - "length": 4167, "version": 2 } - }, - "spec_version": "1.0", - "version": 6 - } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "3044022013bf1032cf0a37d9f88ab9d33d0abb7a932efd95fadbc354fc21a807c7be29ef0220677e651c3e67e0728591faa20c5a09b8a16953038c3ceeffb7d9cfec766b3245" + } + ] } \ No newline at end of file diff --git a/tests/data/repository/staging.json b/tests/data/repository/staging.json deleted file mode 100644 index 084010de75..0000000000 --- a/tests/data/repository/staging.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "signatures": [ - { - "keyid": "b811bd53f2d7adcf5d93e6bb4a8ed2e0ca0f83d454a3e51f105c8e8376bc80d4", - "sig": "304502204486f7b23eadb69df87776ac7a4938ac75a8a2b2e93c84c05d962373837ea91c022100aaeb0fa587430f49618711bb4bd0c1092637c22c223d03c0f1b5a09baea0ed9f" - } - ], - "signed": { - "_type": "targets", - "expires": "2022-02-11T20:10:16Z", - "spec_version": "1.0", - "targets": {}, - "version": 1 - } -} \ No newline at end of file diff --git a/tests/data/repository/targets.json b/tests/data/repository/targets.json index b26926a438..dea42487f0 100644 --- a/tests/data/repository/targets.json +++ b/tests/data/repository/targets.json @@ -1,117 +1,135 @@ { - "signatures": [ - { - "keyid": "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "sig": "3046022100cc1b2ed390e75a112c0fdd6bcbd8bb775300a410f5737ae39996b1858753c8e4022100b591f73370e9378914fb2fab837f700661abd1a74c680f139f6164ec12cb538f" - }, - { - "keyid": "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "sig": "3045022100bc6c45a125e45507339af96aa63983e847565c769f20d7d71bcd2deb7bd36ea902202bf6bd3b76d434c318287899e53f64b4dc178eb0ba403080f1c4fba88a2177ca" - }, - { - "keyid": "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "sig": "304502210085d5bc8a158d31536b4e76cddceef25185c7abbe9091b84f5f2b0d615d9b4ee90220136a36fed2d5986c2519b7d165556f20dfe41fddececda48dffa8dec5258cb95" - }, - { - "keyid": "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "sig": "304402202fe73a61dfe05b4202bc50f66e52bba3d3475134434dab9576735caed659b03c0220449755a87f4dab9961566f10477204637b2415f87e162b58a23b13327dec53e3" - }, - { - "keyid": "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209", - "sig": "304602210091f453ef75c5178299175734355a65a2fc2d0ee137410f46ba8439d99037fc08022100fc800d15f0b751fa225a77542928f4264835c013054a5c409c674e2ea5a70384" - } - ], "signed": { "_type": "targets", - "delegations": { - "keys": { - "ae0c689c6347ada7359df48934991f4e013193d6ddf3482a5ffb293f74f3b217": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "043463588ae9df33a419d1099761245af52aaf7e638b2047bc0f739a62de9808c50a21ea8a1a273799f857f31a1bcb66e6661dd9d5ac7ac3ca260b0b8130c3fed8" - }, - "scheme": "ecdsa-sha2-nistp256" + "spec_version": "1.0", + "version": 2, + "expires": "2028-09-29T21:10:55Z", + "targets": { + "ctfe.pub": { + "length": 775, + "hashes": { + "sha256": "bd7a6812a1f239dfddbbb19d36c7423d21510da56d466ba5018401959cd66037", + "sha512": "b861189e48df51186a39612230fba6b02af951f7b35ad9375e8ca182d0e085d470e26d69f7cd4d7450a0f223991e8e5a4ddf8f1968caa15255de8e37035af43a" }, - "b811bd53f2d7adcf5d93e6bb4a8ed2e0ca0f83d454a3e51f105c8e8376bc80d4": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "041b4b13a6e7110292d284c0dbfc3962a12d2a779a800c99aff59c6afe779296943c75d84aa5bad0be28e4061cf93e0cd3d372d9b2f75ea9f29b907cbccd82006f" - }, - "scheme": "ecdsa-sha2-nistp256" + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstage.dev/test", + "usage": "CTFE" + } } }, - "roles": [ - { - "keyids": [ - "ae0c689c6347ada7359df48934991f4e013193d6ddf3482a5ffb293f74f3b217" - ], - "name": "rekor", - "paths": [ - "rekor.*.pub" - ], - "terminating": true, - "threshold": 1 - }, - { - "keyids": [ - "b811bd53f2d7adcf5d93e6bb4a8ed2e0ca0f83d454a3e51f105c8e8376bc80d4" - ], - "name": "staging", - "paths": [ - "*" - ], - "terminating": false, - "threshold": 1 - } - ] - }, - "expires": "2022-05-11T19:10:16Z", - "spec_version": "1.0", - "targets": { - "artifact.pub": { + "ctfe_2022.pub": { + "length": 178, "hashes": { - "sha256": "59ebf97a9850aecec4bc39c1f5c1dc46e6490a6b5fd2a6cacdcac0c3a6fc4cbf", - "sha512": "308fd1d1d95d7f80aa33b837795251cc3e886792982275e062409e13e4e236ffc34d676682aa96fdc751414de99c864bf132dde71581fa651c6343905e3bf988" + "sha256": "910d899c7763563095a0fe684c8477573fedc19a78586de6ecfbfd8f289f5423", + "sha512": "ab975a75600fc366a837536d0dcba841b755552d21bb114498ff8ac9d2403f76643f5b91269bce5d124a365514719a3edee9dcc2b046cb173f51af659911fcd3" }, - "length": 177 + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstage.dev/2022", + "usage": "CTFE" + } + } }, - "ctfe.pub": { + "ctfe_2022_2.pub": { + "length": 178, "hashes": { - "sha256": "7fcb94a5d0ed541260473b990b99a6c39864c1fb16f3f3e594a5a3cebbfe138a", - "sha512": "4b20747d1afe2544238ad38cc0cc3010921b177d60ac743767e0ef675b915489bd01a36606c0ff83c06448622d7160f0d866c83d20f0c0f44653dcc3f9aa0bd4" + "sha256": "7054b4f15f969daca1c242bb9e77527abaf0b9acf9818a2a35144e4b32b20dc6", + "sha512": "3d035f94e1b14ac84627a28afdbed9a34861fb84239f76d73aa1a99f52262bfd95c4fa0ee71f1fd7e3bfb998d89cd5e0f0eafcff9fa7fa87c6e23484fc1e0cec" }, - "length": 177 + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstage.dev/2022-2", + "usage": "CTFE" + } + } }, "fulcio.crt.pem": { + "length": 741, "hashes": { - "sha256": "f360c53b2e13495a628b9b8096455badcb6d375b185c4816d95a5d746ff29908", - "sha512": "0713252a7fd17f7f3ab12f88a64accf2eb14b8ad40ca711d7fe8b4ecba3b24db9e9dffadb997b196d3867b8f9ff217faf930d80e4dab4e235c7fc3f07be69224" + "sha256": "0e6b0442485ad552bea5f62f11c29e2acfda35307d7538430b4cc1dbef49bff1", + "sha512": "c69ae618883a0c89c282c0943a1ad0c16b0a7788f74e47a1adefc631dac48a0c4449d8c3de7455ae7d772e43c4a87e341f180b0614a46a86006969f8a7b84532" }, - "length": 744 + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://fulcio.sigstage.dev", + "usage": "Fulcio" + } + } }, - "fulcio_v1.crt.pem": { + "fulcio_intermediate.crt.pem": { + "length": 790, "hashes": { - "sha256": "f989aa23def87c549404eadba767768d2a3c8d6d30a8b793f9f518a8eafd2cf5", - "sha512": "f2e33a6dc208cee1f51d33bbea675ab0f0ced269617497985f9a0680689ee7073e4b6f8fef64c91bda590d30c129b3070dddce824c05bc165ac9802f0705cab6" + "sha256": "782868913fe13c385105ddf33e827191386f58da40a931f2075a7e27b1b6ac7b", + "sha512": "90659875a02f73d1026055427c6d857c556e410e23748ff88aeb493227610fd2f5fbdd95ef2a21565f91438dfb3e073f50c4c9dd06f9a601b5d9b064d5cb60b4" }, - "length": 740 + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://fulcio.sigstage.dev", + "usage": "Fulcio" + } + } }, "rekor.pub": { + "length": 178, "hashes": { - "sha256": "dce5ef715502ec9f3cdfd11f8cc384b31a6141023d3e7595e9908a81cb6241bd", - "sha512": "0ae7705e02db33e814329746a4a0e5603c5bdcd91c96d072158d71011a2695788866565a2fec0fe363eb72cbcaeda39e54c5fe8d416daf9f3101fdba4217ef35" + "sha256": "1d80b8f72505a43e65e6e125247cd508f61b459dc457c1d1bcb78d96e1760959", + "sha512": "09ab08698a67354a95d3b8897d9ce7eaef05f06f5ed5f0202d79c228579858ecc5816b7e1b7cc6786abe7d6aaa758e1fcb05900cb749235186c3bf9522d6d7ce" }, - "length": 178 + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://rekor.sigstage.dev", + "usage": "Rekor" + } + } + }, + "trusted_root.json": { + "length": 4521, + "hashes": { + "sha256": "6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b", + "sha512": "fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963" + } } }, - "version": 2 - } + "delegations": { + "keys": { + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" + } + } + }, + "roles": [ + { + "name": "registry.npmjs.org", + "keyids": [ + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" + ], + "threshold": 1, + "terminating": true, + "paths": [ + "registry.npmjs.org/*" + ] + } + ] + } + }, + "signatures": [ + { + "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", + "sig": "304502210090b089087d1b17b2517c464b7774d76d3ea558ffca874eed63ccbee8f6bc3b76022022b56f551bcd0ac8a9c35cd0724ac5b00b4984544cbf812f47f276a9b48db8db" + } + ] } \ No newline at end of file diff --git a/tests/data/repository/targets/6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b.trusted_root.json b/tests/data/repository/targets/6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b.trusted_root.json new file mode 100644 index 0000000000..6a1c1f5a40 --- /dev/null +++ b/tests/data/repository/targets/6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b.trusted_root.json @@ -0,0 +1,86 @@ +{ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "https://rekor.sigstage.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27.000Z" + } + }, + "logId": { + "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstage.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=" + }, + { + "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==" + } + ] + }, + "validFor": { + "start": "2022-03-25T16:50:46.000Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.sigstage.dev/test", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==", + "keyDetails": "PKCS1_RSA_PKCS1V5", + "validFor": { + "start": "2021-03-14T00:00:00.000Z" + } + }, + "logId": { + "keyId": "s9AOb93xWxr+a4ztxJnxxJCX7VZ0V3IF4jTu/OoL84A=" + } + }, + { + "baseUrl": "https://ctfe.sigstage.dev/2022", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-07-01T00:00:00.000Z" + } + }, + "logId": { + "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=" + } + }, + { + "baseUrl": "https://ctfe.sigstage.dev/2022-2", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-07-01T00:00:00.000Z" + } + }, + "logId": { + "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=" + } + } + ], + "timestampAuthorities": [] +} diff --git a/tests/data/repository/targets/artifact.pub b/tests/data/repository/targets/artifact.pub deleted file mode 100644 index d6e745bdd0..0000000000 --- a/tests/data/repository/targets/artifact.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhyQCx0E9wQWSFI9ULGwy3BuRklnt -IqozONbbdbqz11hlRJy9c7SG+hdcFl9jE9uE/dwtuwU2MqU9T/cN0YkWww== ------END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/data/repository/targets/ctfe.pub b/tests/data/repository/targets/ctfe.pub deleted file mode 100644 index 1bb1488c99..0000000000 --- a/tests/data/repository/targets/ctfe.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3Pyu -dDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w== ------END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/data/repository/targets/fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963.trusted_root.json b/tests/data/repository/targets/fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963.trusted_root.json new file mode 100644 index 0000000000..6a1c1f5a40 --- /dev/null +++ b/tests/data/repository/targets/fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963.trusted_root.json @@ -0,0 +1,86 @@ +{ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "https://rekor.sigstage.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27.000Z" + } + }, + "logId": { + "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstage.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=" + }, + { + "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==" + } + ] + }, + "validFor": { + "start": "2022-03-25T16:50:46.000Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.sigstage.dev/test", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==", + "keyDetails": "PKCS1_RSA_PKCS1V5", + "validFor": { + "start": "2021-03-14T00:00:00.000Z" + } + }, + "logId": { + "keyId": "s9AOb93xWxr+a4ztxJnxxJCX7VZ0V3IF4jTu/OoL84A=" + } + }, + { + "baseUrl": "https://ctfe.sigstage.dev/2022", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-07-01T00:00:00.000Z" + } + }, + "logId": { + "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=" + } + }, + { + "baseUrl": "https://ctfe.sigstage.dev/2022-2", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-07-01T00:00:00.000Z" + } + }, + "logId": { + "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=" + } + } + ], + "timestampAuthorities": [] +} diff --git a/tests/data/repository/targets/fulcio.crt.pem b/tests/data/repository/targets/fulcio.crt.pem deleted file mode 100644 index 6a06ff300b..0000000000 --- a/tests/data/repository/targets/fulcio.crt.pem +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAq -MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx -MDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUu -ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSy -A7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0Jcas -taRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6Nm -MGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE -FMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2u -Su1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJx -Ve/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uup -Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ== ------END CERTIFICATE----- \ No newline at end of file diff --git a/tests/data/repository/targets/fulcio_v1.crt.pem b/tests/data/repository/targets/fulcio_v1.crt.pem deleted file mode 100644 index 3afc46bb6e..0000000000 --- a/tests/data/repository/targets/fulcio_v1.crt.pem +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw -KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y -MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl -LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7 -XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex -X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j -YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY -wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ -KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM -WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 -TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ ------END CERTIFICATE----- \ No newline at end of file diff --git a/tests/data/repository/targets/registry.npmjs.org/7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426.keys.json b/tests/data/repository/targets/registry.npmjs.org/7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426.keys.json new file mode 100644 index 0000000000..f5667a5f0e --- /dev/null +++ b/tests/data/repository/targets/registry.npmjs.org/7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426.keys.json @@ -0,0 +1,26 @@ +{ + "keys": [ + { + "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", + "keyUsage": "npm:signatures", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "1999-01-01T00:00:00.000Z" + } + } + }, + { + "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", + "keyUsage": "npm:attestations", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-12-01T00:00:00.000Z" + } + } + } + ] +} diff --git a/tests/data/repository/targets/registry.npmjs.org/881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699.keys.json b/tests/data/repository/targets/registry.npmjs.org/881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699.keys.json new file mode 100644 index 0000000000..f5667a5f0e --- /dev/null +++ b/tests/data/repository/targets/registry.npmjs.org/881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699.keys.json @@ -0,0 +1,26 @@ +{ + "keys": [ + { + "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", + "keyUsage": "npm:signatures", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "1999-01-01T00:00:00.000Z" + } + } + }, + { + "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", + "keyUsage": "npm:attestations", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-12-01T00:00:00.000Z" + } + } + } + ] +} diff --git a/tests/data/repository/targets/rekor.0.pub b/tests/data/repository/targets/rekor.0.pub deleted file mode 100644 index 050ef60149..0000000000 --- a/tests/data/repository/targets/rekor.0.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr -kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw== ------END PUBLIC KEY----- diff --git a/tests/data/repository/targets/rekor.json b/tests/data/repository/targets/rekor.json deleted file mode 100644 index f86930d537..0000000000 --- a/tests/data/repository/targets/rekor.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "signatures": [ - { - "keyid": "ae0c689c6347ada7359df48934991f4e013193d6ddf3482a5ffb293f74f3b217", - "sig": "3045022076eadd73f6664bac5cc91f12d3a7ddcdd53f9bde661f147651196ff66e7235d1022100f7b3143792405f9e8a75331a05d4128bdf083de302801e99c3d027919a4b03da" - } - ], - "signed": { - "_type": "targets", - "expires": "2022-05-11T19:10:11Z", - "spec_version": "1.0", - "targets": { - "rekor.0.pub": { - "hashes": { - "sha256": "dce5ef715502ec9f3cdfd11f8cc384b31a6141023d3e7595e9908a81cb6241bd", - "sha512": "0ae7705e02db33e814329746a4a0e5603c5bdcd91c96d072158d71011a2695788866565a2fec0fe363eb72cbcaeda39e54c5fe8d416daf9f3101fdba4217ef35" - }, - "length": 178 - } - }, - "version": 1 - } -} \ No newline at end of file diff --git a/tests/data/repository/targets/rekor.pub b/tests/data/repository/targets/rekor.pub deleted file mode 100644 index 050ef60149..0000000000 --- a/tests/data/repository/targets/rekor.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr -kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw== ------END PUBLIC KEY----- diff --git a/tests/data/repository/timestamp.json b/tests/data/repository/timestamp.json index 8cb4f094b7..4b4a4dec2c 100644 --- a/tests/data/repository/timestamp.json +++ b/tests/data/repository/timestamp.json @@ -1,24 +1,24 @@ { - "signatures": [ - { - "keyid": "b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d", - "sig": "30440220590dc4d9eb4e3b2745315348c1ea5481f29f981dfd6c2d72bde13256a25e0caf02205704352c828451bf1e41bba154db9ecb4e901b4bc47d721a91fabfb84a48c61f" - } - ], "signed": { "_type": "timestamp", - "expires": "2022-01-05T00:40:07Z", + "spec_version": "1.0", + "version": 2, + "expires": "2028-04-12T21:11:28Z", "meta": { "snapshot.json": { + "length": 1039, "hashes": { - "sha256": "e202c20580ac4edc7a52ad2bcbe97c5af557c04463f10f2d9a28e2624e0c8edf", - "sha512": "f0b9f17797fe6d89a745f8fc9a39a073823bc04400307711eebe3b00dfe418e4d1d4419697eee29445c9cd5e03c3e24532d4fb03824d7555ecc0de54bd73ffd1" + "sha256": "b480856ab72c80fe10902ffac69ec10340e827e02b2bd114d6f141de910a96c5", + "sha512": "da06f65c1ee242d63820ba646fb1b4037fe355460309d89f98a923d1d009e7d46f11d4272a0d8e07829734baea655f7692d8c23383d6044b4f72263a4dbf3057" }, - "length": 1658, - "version": 6 + "version": 2 } - }, - "spec_version": "1.0", - "version": 6 - } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "3044022040e243b1bc8edb798df66803c2460471a4129704421d59f55c825dc549493f840220267e4684875d4803ae0948140af32fc9f560453efb84d9728ee66619e8767d8c" + } + ] } \ No newline at end of file From 6af5c49edcfa40e2dec22a12dbc6582c29655702 Mon Sep 17 00:00:00 2001 From: Jack Leightcap Date: Wed, 31 Jan 2024 10:53:49 -0500 Subject: [PATCH 02/27] pr feedback 2024-01-31 Signed-off-by: Jack Leightcap --- src/crypto/verification.rs | 8 ----- src/tuf/constants.rs | 9 ++++-- src/tuf/mod.rs | 51 ++++++++++++++++++-------------- src/verify/models.rs | 6 ++-- tests/conformance/conformance.rs | 2 +- 5 files changed, 38 insertions(+), 38 deletions(-) delete mode 100644 src/crypto/verification.rs diff --git a/src/crypto/verification.rs b/src/crypto/verification.rs deleted file mode 100644 index a0a6ddcf2d..0000000000 --- a/src/crypto/verification.rs +++ /dev/null @@ -1,8 +0,0 @@ -use rustls_pki_types::CertificateDer; -use webpki::TrustAnchor; - -/// Machinery for Sigstore end entity certificate verification. -struct CertificateVerificationContext<'a> { - pub trust_anchors: Vec>, - pub intermediate_certs: Vec>, -} diff --git a/src/tuf/constants.rs b/src/tuf/constants.rs index 0529d1128b..e84909785a 100644 --- a/src/tuf/constants.rs +++ b/src/tuf/constants.rs @@ -13,14 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::path::Path; +use tough::TargetName; + pub(crate) const SIGSTORE_METADATA_BASE: &str = "https://tuf-repo-cdn.sigstore.dev"; pub(crate) const SIGSTORE_TARGET_BASE: &str = "https://tuf-repo-cdn.sigstore.dev/targets"; macro_rules! tuf_resource { ($path:literal) => { - include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/trust_root/", $path)) + Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/trust_root/", $path)) }; } -pub(crate) const SIGSTORE_ROOT: &[u8] = tuf_resource!("prod/root.json"); -pub(crate) const _SIGSTORE_TRUST_BUNDLE: &[u8] = tuf_resource!("prod/trusted_root.json"); +pub(crate) const SIGSTORE_ROOT: &Path = tuf_resource!("prod/root.json"); +pub(crate) const SIGSTORE_TRUST_BUNDLE: &Path = tuf_resource!("prod/trusted_root.json"); diff --git a/src/tuf/mod.rs b/src/tuf/mod.rs index e695cc5044..cd7e594581 100644 --- a/src/tuf/mod.rs +++ b/src/tuf/mod.rs @@ -45,6 +45,8 @@ use tough::TargetName; use tracing::debug; use webpki::types::CertificateDer; +use crate::tuf::constants::SIGSTORE_TRUST_BUNDLE; + use self::trustroot::{CertificateAuthority, TimeRange, TransparencyLogInstance, TrustedRoot}; use super::errors::{Result, SigstoreError}; @@ -94,11 +96,14 @@ impl SigstoreRepository { let metadata_base = url::Url::parse(constants::SIGSTORE_METADATA_BASE)?; let target_base = url::Url::parse(constants::SIGSTORE_TARGET_BASE)?; - let repository = - tough::RepositoryLoader::new(constants::SIGSTORE_ROOT, metadata_base, target_base) - .expiration_enforcement(tough::ExpirationEnforcement::Safe) - .load() - .map_err(Box::new)?; + let repository = tough::RepositoryLoader::new( + include_bytes!(constants::SIGSTORE_ROOT), + metadata_base, + target_base, + ) + .expiration_enforcement(tough::ExpirationEnforcement::Safe) + .load() + .map_err(Box::new)?; Ok(Self { repository, @@ -108,30 +113,30 @@ impl SigstoreRepository { } fn trusted_root(&self) -> Result<&TrustedRoot> { - fn init_trusted_root( - repository: &tough::Repository, - checkout_dir: Option<&PathBuf>, - ) -> Result { - let trusted_root_target = TargetName::new("trusted_root.json").map_err(Box::new)?; - let local_path = checkout_dir.map(|d| d.join(trusted_root_target.raw())); + return if let Some(root) = self.trusted_root.get() { + Ok(root) + } else { + let trusted_root_target = SIGSTORE_TRUST_BUNDLE + .to_str() + .and_then(TargetName::new) + .map_err(Box::new)?; + let local_path = self + .checkout_dir + .as_ref() + .map(|d| d.join(trusted_root_target.raw())); let data = fetch_target_or_reuse_local_cache( - repository, + &self.repository, &trusted_root_target, local_path.as_ref(), )?; debug!("data:\n{}", String::from_utf8_lossy(&data)); - Ok(serde_json::from_slice(&data[..])?) - } + let root = serde_json::from_slice(&data[..])?; - if let Some(root) = self.trusted_root.get() { - return Ok(root); - } - - let root = init_trusted_root(&self.repository, self.checkout_dir.as_ref())?; - Ok(self.trusted_root.get_or_init(|| root)) + Ok(self.trusted_root.get_or_init(|| root)) + }; } /// Prefetches trust materials. @@ -264,10 +269,10 @@ fn fetch_target_or_reuse_local_cache( local_file: Option<&PathBuf>, ) -> Result> { let (local_file_outdated, local_file_contents) = if let Some(path) = local_file { - is_local_file_outdated(repository, target_name, path) + is_local_file_outdated(repository, target_name, path)? } else { - Ok((true, None)) - }?; + (true, None) + }; let data = if local_file_outdated { let data = fetch_target(repository, target_name)?; diff --git a/src/verify/models.rs b/src/verify/models.rs index dcf6e39b68..19c021f75f 100644 --- a/src/verify/models.rs +++ b/src/verify/models.rs @@ -36,6 +36,7 @@ use pkcs8::der::Decode; use sha2::{Digest, Sha256}; use sigstore_protobuf_specs::Bundle; use thiserror::Error; +use tracing::warn; use x509_cert::Certificate; #[derive(Error, Debug)] @@ -164,15 +165,14 @@ impl VerificationMaterials { } if inclusion_proof.is_some() && !has_checkpoint { - // TODO(tnytown): Act here. - // NOTE(jl): in sigstore-python, this is a no-op that prints a warning log. + warn!("0.1 bundle contains inclusion proof without checkpoint; ignoring"); } } BundleVersion::Bundle0_2 => { inclusion_proof?; if !has_checkpoint { // inclusion proofs must contain checkpoints - return None; // FIXME(jl): this raises an error in sigstore-python. + return None; } } } diff --git a/tests/conformance/conformance.rs b/tests/conformance/conformance.rs index 801cc6b064..cfb11b7f72 100644 --- a/tests/conformance/conformance.rs +++ b/tests/conformance/conformance.rs @@ -143,7 +143,7 @@ fn sign_bundle(args: SignBundle) -> anyhow::Result<()> { let context = SigningContext::production(); let signer = context.signer(identity_token); - let signing_artifact = signer.sign(&mut artifact)?; + let signing_artifact = signer?.sign(&mut artifact)?; let bundle_data = signing_artifact.to_bundle(); serde_json::to_writer(bundle, &bundle_data)?; From afdf3b0d83c82d3f0639cc7ab4ca7ae6566cbef6 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Thu, 15 Feb 2024 13:50:51 -0600 Subject: [PATCH 03/27] tuf: fixup target fetching Signed-off-by: Andrew Pan --- src/tuf/constants.rs | 23 ++++-- src/tuf/mod.rs | 169 +++++++++++++++---------------------------- 2 files changed, 73 insertions(+), 119 deletions(-) diff --git a/src/tuf/constants.rs b/src/tuf/constants.rs index e84909785a..bb08b5b4e9 100644 --- a/src/tuf/constants.rs +++ b/src/tuf/constants.rs @@ -13,17 +13,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::path::Path; -use tough::TargetName; - pub(crate) const SIGSTORE_METADATA_BASE: &str = "https://tuf-repo-cdn.sigstore.dev"; pub(crate) const SIGSTORE_TARGET_BASE: &str = "https://tuf-repo-cdn.sigstore.dev/targets"; -macro_rules! tuf_resource { - ($path:literal) => { - Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/trust_root/", $path)) +macro_rules! impl_static_resource { + {$($name:literal,)+} => { + #[inline] + pub(crate) fn static_resource(name: impl AsRef) -> Option<&'static [u8]> { + match name.as_ref() { + $( + $name => Some(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/trust_root/prod/", $name))) + ),+, + _ => None, + } + } }; } -pub(crate) const SIGSTORE_ROOT: &Path = tuf_resource!("prod/root.json"); -pub(crate) const SIGSTORE_TRUST_BUNDLE: &Path = tuf_resource!("prod/trusted_root.json"); +impl_static_resource! { + "root.json", + "trusted_root.json", +} diff --git a/src/tuf/mod.rs b/src/tuf/mod.rs index cd7e594581..07808ee55e 100644 --- a/src/tuf/mod.rs +++ b/src/tuf/mod.rs @@ -32,7 +32,6 @@ //! ``` use std::{ cell::OnceCell, - fs, io::Read, path::{Path, PathBuf}, }; @@ -45,8 +44,6 @@ use tough::TargetName; use tracing::debug; use webpki::types::CertificateDer; -use crate::tuf::constants::SIGSTORE_TRUST_BUNDLE; - use self::trustroot::{CertificateAuthority, TimeRange, TransparencyLogInstance, TrustedRoot}; use super::errors::{Result, SigstoreError}; @@ -97,7 +94,7 @@ impl SigstoreRepository { let target_base = url::Url::parse(constants::SIGSTORE_TARGET_BASE)?; let repository = tough::RepositoryLoader::new( - include_bytes!(constants::SIGSTORE_ROOT), + constants::static_resource("root.json").expect("Failed to fetch required resource!"), metadata_base, target_base, ) @@ -116,20 +113,7 @@ impl SigstoreRepository { return if let Some(root) = self.trusted_root.get() { Ok(root) } else { - let trusted_root_target = SIGSTORE_TRUST_BUNDLE - .to_str() - .and_then(TargetName::new) - .map_err(Box::new)?; - let local_path = self - .checkout_dir - .as_ref() - .map(|d| d.join(trusted_root_target.raw())); - - let data = fetch_target_or_reuse_local_cache( - &self.repository, - &trusted_root_target, - local_path.as_ref(), - )?; + let data = self.fetch_target("trusted_root.json")?; debug!("data:\n{}", String::from_utf8_lossy(&data)); @@ -139,6 +123,62 @@ impl SigstoreRepository { }; } + fn fetch_target(&self, name: N) -> Result> + where + N: TryInto, + { + let read_remote_target = |name: &TargetName| -> Result> { + let Some(mut reader) = self.repository.read_target(name).map_err(Box::new)? else { + return Err(SigstoreError::TufTargetNotFoundError(name.raw().to_owned())); + }; + + debug!("fetching target {} from remote", name.raw()); + + let mut repo_data = Vec::new(); + reader.read_to_end(&mut repo_data)?; + Ok(repo_data) + }; + + let name: TargetName = name.try_into().map_err(Box::new)?; + let local_path = self.checkout_dir.as_ref().map(|d| d.join(name.raw())); + + // Try reading the target from disk cache. + let data = if let Some(Ok(local_data)) = local_path.as_ref().map(std::fs::read) { + local_data.to_vec() + // Try reading the target embedded into the binary. + } else if let Some(embedded_data) = constants::static_resource(name.raw()) { + debug!("read embedded target {}", name.raw()); + embedded_data.to_vec() + // If all else fails, read the data from the TUF repo. + } else if let Ok(remote_data) = read_remote_target(&name) { + remote_data + } else { + return Err(SigstoreError::TufTargetNotFoundError(name.raw().to_owned())); + }; + + // Get metadata (hash) of the target and update the disk copy if it doesn't match. + let Some(target) = self.repository.targets().signed.targets.get(&name) else { + return Err(SigstoreError::TufMetadataError(format!( + "couldn't get metadata for {}", + name.raw() + ))); + }; + + let data = if Sha256::digest(&data)[..] != target.hashes.sha256[..] { + read_remote_target(&name)? + } else { + data + }; + + // Write the up-to-date data back to the disk. This doesn't need to succeed, as we can + // always fetch the target again later. + if let Some(local_path) = local_path { + let _ = std::fs::write(local_path, &data); + } + + Ok(data) + } + /// Prefetches trust materials. /// /// [Repository::fulcio_certs()] and [Repository::rekor_keys()] on [SigstoreRepository] lazily @@ -248,96 +288,3 @@ fn is_timerange_valid(range: Option<&TimeRange>, allow_expired: bool) -> bool { }, } } - -/// Download a file stored inside of a TUF repository, try to reuse a local -/// cache when possible. -/// -/// * `repository`: TUF repository holding the file -/// * `target_name`: TUF representation of the file to be downloaded -/// * `local_file`: location where the file should be downloaded -/// -/// This function will reuse the local copy of the file if contents -/// didn't change. -/// This check is done by comparing the digest of the local file, if found, -/// with the digest reported inside of the TUF repository metadata. -/// -/// **Note well:** the `local_file` is updated whenever its contents are -/// outdated. -fn fetch_target_or_reuse_local_cache( - repository: &tough::Repository, - target_name: &TargetName, - local_file: Option<&PathBuf>, -) -> Result> { - let (local_file_outdated, local_file_contents) = if let Some(path) = local_file { - is_local_file_outdated(repository, target_name, path)? - } else { - (true, None) - }; - - let data = if local_file_outdated { - let data = fetch_target(repository, target_name)?; - if let Some(path) = local_file { - // update the local file to have latest data from the TUF repo - fs::write(path, data.clone())?; - } - data - } else { - local_file_contents - .expect("local file contents to not be 'None'") - .as_bytes() - .to_owned() - }; - - Ok(data) -} - -/// Download a file from a TUF repository -fn fetch_target(repository: &tough::Repository, target_name: &TargetName) -> Result> { - let data: Vec; - match repository.read_target(target_name).map_err(Box::new)? { - None => Err(SigstoreError::TufTargetNotFoundError( - target_name.raw().to_string(), - )), - Some(reader) => { - data = read_to_end(reader)?; - Ok(data) - } - } -} - -/// Compares the checksum of a local file, with the digest reported inside of -/// TUF repository metadata -fn is_local_file_outdated( - repository: &tough::Repository, - target_name: &TargetName, - local_file: &Path, -) -> Result<(bool, Option)> { - let target = repository - .targets() - .signed - .targets - .get(target_name) - .ok_or_else(|| SigstoreError::TufTargetNotFoundError(target_name.raw().to_string()))?; - - if local_file.exists() { - let data = fs::read_to_string(local_file)?; - let local_checksum = Sha256::digest(data.clone()); - let expected_digest: Vec = target.hashes.sha256.to_vec(); - - if local_checksum.as_slice() == expected_digest.as_slice() { - // local data is not outdated - Ok((false, Some(data))) - } else { - Ok((true, None)) - } - } else { - Ok((true, None)) - } -} - -/// Gets the goods from a read and makes a Vec -fn read_to_end(mut reader: R) -> Result> { - let mut v = Vec::new(); - reader.read_to_end(&mut v)?; - Ok(v) -} From 4dc13fea0bd4ff99f2de68ff1152faf308fd1406 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Thu, 15 Feb 2024 16:17:52 -0600 Subject: [PATCH 04/27] sign, verify: use `sigstore-protobuf-specs` 0.3.2 https://github.com/trail-of-forks/sigstore-rs/tree/ap/with-prost-protobuf-specs Co-authored-by: Alex Cameron Signed-off-by: Andrew Pan --- Cargo.toml | 16 ++- src/bundle/mod.rs | 89 +++++++++++-- src/crypto/certificate_pool.rs | 32 ++--- src/fulcio/mod.rs | 36 ++--- src/fulcio/models.rs | 60 +++++---- src/sign.rs | 135 +++++++------------ src/tuf/mod.rs | 68 +++++++--- src/tuf/trustroot.rs | 194 --------------------------- src/verify/mod.rs | 2 +- src/verify/models.rs | 233 +++++++++++++++++++-------------- src/verify/policy.rs | 132 ++++++++++--------- src/verify/verifier.rs | 106 +++++++-------- 12 files changed, 504 insertions(+), 599 deletions(-) delete mode 100644 src/tuf/trustroot.rs diff --git a/Cargo.toml b/Cargo.toml index 80b107eef5..d19a7cb041 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/sigstore/sigstore-rs" [features] default = ["full-native-tls", "cached-client", "tuf", "sign", "verify"] -wasm = ["getrandom/js"] +wasm = ["getrandom/js", "ring/wasm32_unknown_unknown_js"] full-native-tls = [ "fulcio-native-tls", @@ -76,7 +76,7 @@ base64 = "0.21.0" cached = { version = "0.48.0", optional = true, features = ["async"] } cfg-if = "1.0.0" chrono = { version = "0.4.27", default-features = false, features = ["serde"] } -const-oid = "0.9.1" +const-oid = { version = "0.9.6", features = ["db"] } digest = { version = "0.10.3", default-features = false } ecdsa = { version = "0.16.7", features = ["pkcs8", "digest", "der", "signing"] } ed25519 = { version = "2.2.1", features = ["alloc"] } @@ -88,9 +88,9 @@ olpc-cjson = "0.1" openidconnect = { version = "3.0", default-features = false, features = [ "reqwest", ], optional = true } -p256 = "0.13.2" +p256 = "0.13" p384 = "0.13" -webbrowser = "0.8.4" +webbrowser = "0.8.12" pem = { version = "3.0", features = ["serde"] } pkcs1 = { version = "0.7.5", features = ["std"] } pkcs8 = { version = "0.10.2", features = [ @@ -110,23 +110,25 @@ rsa = "0.9.2" scrypt = "0.11.0" serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" -serde_with = { version = "3.4.0", features = ["base64"] } +serde_with = { version = "3.4.0", features = ["base64", "json"] } sha2 = { version = "0.10.6", features = ["oid"] } signature = { version = "2.0" } -sigstore_protobuf_specs = "0.1.0-rc.2" +sigstore_protobuf_specs = "0.3.2" thiserror = "1.0.30" tokio = { version = "1.17.0", features = ["rt"] } tokio-util = { version = "0.7.10", features = ["io-util"] } tough = { version = "0.14", features = ["http"], optional = true } tracing = "0.1.31" url = "2.2.2" -x509-cert = { version = "0.2.2", features = ["builder", "pem", "std"] } +x509-cert = { version = "0.2.5", features = ["builder", "pem", "std", "sct"] } crypto_secretbox = "0.1.1" zeroize = "1.5.7" rustls-webpki = { version = "0.102.1", features = ["alloc"] } serde_repr = "0.1.16" hex = "0.4.3" json-syntax = { version = "0.11.1", features = ["canonicalize", "serde"] } +tls_codec = { version = "0.4.1", features = ["derive"] } +ring = "0.17.6" [dev-dependencies] anyhow = { version = "1.0", features = ["backtrace"] } diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs index 1bf8e8d840..b3b040c15f 100644 --- a/src/bundle/mod.rs +++ b/src/bundle/mod.rs @@ -17,18 +17,17 @@ use std::fmt::Display; use std::str::FromStr; -pub use sigstore_protobuf_specs::Bundle; - -macro_rules! required { - ($($base:expr )? ; $first_attr:ident $( . $rest_attrs:ident)* $( , $else_err:expr)?) => { - $( $base . )? $first_attr.as_ref() - $( - .and_then(|v| v.$rest_attrs.as_ref()) - )* - $( .ok_or($else_err) )? - } -} -pub(crate) use required; +use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; +use json_syntax::Print; +pub use sigstore_protobuf_specs::dev::sigstore::bundle::v1::Bundle; +use sigstore_protobuf_specs::dev::sigstore::{ + common::v1::LogId, + rekor::v1::{Checkpoint, InclusionPromise, InclusionProof, KindVersion, TransparencyLogEntry}, +}; + +use crate::rekor::models::{ + log_entry::InclusionProof as RekorInclusionProof, LogEntry as RekorLogEntry, +}; // Known Sigstore bundle media types. #[derive(Clone, Copy, Debug)] @@ -57,3 +56,69 @@ impl FromStr for Version { } } } + +#[inline] +fn decode_hex>(hex: S) -> Result, ()> { + hex::decode(hex.as_ref()).or(Err(())) +} + +impl TryFrom for InclusionProof { + type Error = (); + + fn try_from(value: RekorInclusionProof) -> Result { + let hashes = value + .hashes + .iter() + .map(decode_hex) + .collect::, _>>()?; + + Ok(InclusionProof { + checkpoint: Some(Checkpoint { + envelope: value.checkpoint, + }), + hashes, + log_index: value.log_index, + root_hash: decode_hex(value.root_hash)?, + tree_size: value.tree_size, + }) + } +} + +/// Convert log entries returned from Rekor into Sigstore Bundle format entries. +impl TryFrom for TransparencyLogEntry { + type Error = (); + + fn try_from(value: RekorLogEntry) -> Result { + let canonicalized_body = { + let mut body = + json_syntax::to_value(value.body).expect("failed to parse constructed Body!"); + body.canonicalize(); + body.compact_print().to_string().into_bytes() + }; + let inclusion_promise = Some(InclusionPromise { + signed_entry_timestamp: base64 + .decode(value.verification.signed_entry_timestamp) + .or(Err(()))?, + }); + let inclusion_proof = value + .verification + .inclusion_proof + .map(|p| p.try_into()) + .transpose()?; + + Ok(TransparencyLogEntry { + canonicalized_body, + inclusion_promise, + inclusion_proof, + integrated_time: value.integrated_time, + kind_version: Some(KindVersion { + kind: "hashedrekord".to_owned(), + version: "0.0.1".to_owned(), + }), + log_id: Some(LogId { + key_id: decode_hex(value.log_i_d)?, + }), + log_index: value.log_index, + }) + } +} diff --git a/src/crypto/certificate_pool.rs b/src/crypto/certificate_pool.rs index 731a68c6c3..cd067aff48 100644 --- a/src/crypto/certificate_pool.rs +++ b/src/crypto/certificate_pool.rs @@ -16,7 +16,7 @@ use const_oid::db::rfc5280::ID_KP_CODE_SIGNING; use webpki::{ types::{CertificateDer, TrustAnchor, UnixTime}, - EndEntityCert, KeyUsage, + EndEntityCert, KeyUsage, VerifiedPath, }; use crate::errors::{Result, SigstoreError}; @@ -83,24 +83,26 @@ impl<'a> CertificatePool<'a> { der: &[u8], verification_time: Option, ) -> Result<()> { - self.verify_cert_with_time(der, verification_time.unwrap_or(UnixTime::now())) + let der = CertificateDer::from(der); + let cert = EndEntityCert::try_from(&der)?; + + self.verify_cert_with_time(&cert, verification_time.unwrap_or(UnixTime::now()))?; + + Ok(()) } - /// TODO(tnytown): nudge webpki into behaving as the cosign code expects - pub(crate) fn verify_cert_with_time( - &self, - cert: &[u8], + pub(crate) fn verify_cert_with_time<'cert>( + &'a self, + cert: &'cert EndEntityCert<'cert>, verification_time: UnixTime, - ) -> Result<()> { - let der = CertificateDer::from(cert); - let cert = EndEntityCert::try_from(&der)?; - - // TODO(tnytown): Determine which of these algs are used in the Sigstore ecosystem. + ) -> Result> + where + 'a: 'cert, + { let signing_algs = webpki::ALL_VERIFICATION_ALGS; - let eku_code_signing = ID_KP_CODE_SIGNING.as_bytes(); - cert.verify_for_usage( + Ok(cert.verify_for_usage( signing_algs, &self.trusted_roots, self.intermediates.as_slice(), @@ -108,8 +110,6 @@ impl<'a> CertificatePool<'a> { KeyUsage::required(eku_code_signing), None, None, - )?; - - Ok(()) + )?) } } diff --git a/src/fulcio/mod.rs b/src/fulcio/mod.rs index 81477cab6e..1330354022 100644 --- a/src/fulcio/mod.rs +++ b/src/fulcio/mod.rs @@ -10,17 +10,15 @@ use crate::fulcio::oauth::OauthTokenProvider; use crate::oauth::IdentityToken; use base64::{engine::general_purpose::STANDARD as BASE64_STD_ENGINE, Engine as _}; use openidconnect::core::CoreIdToken; -use pkcs8::der::Decode; use reqwest::{header, Body}; use serde::ser::SerializeStruct; use serde::{Serialize, Serializer}; use std::convert::{TryFrom, TryInto}; use std::fmt::{Debug, Display, Formatter}; -use tracing::debug; use url::Url; -use x509_cert::Certificate; +use x509_cert::{Certificate, der::Decode}; -pub use models::CertificateResponse; +pub use models::{CertificateResponse, SigningCertificateDetachedSCT}; /// Default public Fulcio server root. pub const FULCIO_ROOT: &str = "https://fulcio.sigstore.dev/"; @@ -232,24 +230,23 @@ impl FulcioClient { header::ACCEPT => "application/pem-certificate-chain" ); - let response: SigningCertificate = client + let response = client .post(self.root_url.join(SIGNING_CERT_V2_PATH)?) .headers(headers) .json(&CreateSigningCertificateRequest { certificate_signing_request: request, }) .send() - .await? - .json() .await?; + let response = response.json().await?; - let sct_embedded = matches!( - response, - SigningCertificate::SignedCertificateEmbeddedSct(_) - ); - let certs = match response { - SigningCertificate::SignedCertificateDetachedSct(ref sc) => &sc.chain.certificates, - SigningCertificate::SignedCertificateEmbeddedSct(ref sc) => &sc.chain.certificates, + let (certs, detached_sct) = match response { + SigningCertificate::SignedCertificateDetachedSct(ref sc) => { + (&sc.chain.certificates, Some(sc.clone())) + } + SigningCertificate::SignedCertificateEmbeddedSct(ref sc) => { + (&sc.chain.certificates, None) + } }; if certs.len() < 2 { @@ -264,19 +261,10 @@ impl FulcioClient { .map(|pem| Certificate::from_der(pem.contents())) .collect::, _>>()?; - // TODO(tnytown): Implement SCT extraction. - // see: https://github.com/RustCrypto/formats/pull/1134 - if sct_embedded { - debug!("PrecertificateSignedCertificateTimestamps isn't implemented yet in x509_cert."); - } else { - // No embedded SCT, Fulcio instance that provides detached SCT: - if let SigningCertificate::SignedCertificateDetachedSct(_sct) = response {} - }; - Ok(CertificateResponse { cert, chain, - // sct, + detached_sct, }) } } diff --git a/src/fulcio/models.rs b/src/fulcio/models.rs index b4e7dc367d..0a784a76d0 100644 --- a/src/fulcio/models.rs +++ b/src/fulcio/models.rs @@ -14,13 +14,17 @@ //! Models for interfacing with Fulcio. //! -//! https://github.com/sigstore/fulcio/blob/9da27be4fb64b85c907ab9ddd8a5d3cbd38041d4/fulcio.proto +//! -use base64::{engine::general_purpose::STANDARD as BASE64_STD_ENGINE, Engine as _}; use pem::Pem; use pkcs8::der::EncodePem; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_repr::Deserialize_repr; +use serde_with::{ + base64::{Base64, Standard}, + formats::Padded, + serde_as, DeserializeAs, SerializeAs, +}; use x509_cert::Certificate; fn serialize_x509_csr( @@ -31,31 +35,36 @@ where S: Serializer, { let encoded = input - .to_pem(pkcs8::LineEnding::CRLF) + .to_pem(pkcs8::LineEnding::LF) .map_err(serde::ser::Error::custom)?; - let encoded = BASE64_STD_ENGINE.encode(encoded); - ser.serialize_str(&encoded) + Base64::::serialize_as(&encoded, ser) } -fn deserialize_base64<'de, D>(de: D) -> std::result::Result, D::Error> +fn deserialize_inner_detached_sct<'de, D>(de: D) -> std::result::Result where D: Deserializer<'de>, { - let buf: &str = Deserialize::deserialize(de)?; - - BASE64_STD_ENGINE - .decode(buf) - .map_err(serde::de::Error::custom) + let buf: Vec = Base64::::deserialize_as(de)?; + serde_json::from_slice(&buf).map_err(serde::de::Error::custom) } -fn deserialize_inner_detached_sct<'de, D>(de: D) -> std::result::Result +fn deserialize_inner_detached_sct_signature<'de, D>(de: D) -> Result, D::Error> where D: Deserializer<'de>, { - let buf = deserialize_base64(de)?; - - serde_json::from_slice(&buf).map_err(serde::de::Error::custom) + let buf: Vec = Base64::::deserialize_as(de)?; + + // The first two bytes indicate the signature and hash algorithms so let's skip those. + // The next two bytes indicate the size of the signature. + let signature_size = u16::from_be_bytes(buf[2..4].try_into().expect("unexpected length")); + + // This should be equal to the length of the remainder of the signature buffer. + let signature = buf[4..].to_vec(); + if signature_size as usize != signature.len() { + return Err(serde::de::Error::custom("signature size mismatch")); + } + Ok(signature) } #[derive(Serialize)] @@ -72,7 +81,7 @@ pub enum SigningCertificate { SignedCertificateEmbeddedSct(SigningCertificateEmbeddedSCT), } -#[derive(Deserialize)] +#[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct SigningCertificateDetachedSCT { pub chain: CertificateChain, @@ -86,31 +95,34 @@ pub struct SigningCertificateEmbeddedSCT { pub chain: CertificateChain, } -#[derive(Deserialize)] +#[derive(Deserialize, Debug, Clone)] pub struct CertificateChain { pub certificates: Vec, } -#[derive(Deserialize)] +#[serde_as] +#[derive(Deserialize, Debug, Clone)] pub struct InnerDetachedSCT { pub sct_version: SCTVersion, - #[serde(deserialize_with = "deserialize_base64")] - pub id: Vec, + #[serde_as(as = "Base64")] + pub id: [u8; 32], pub timestamp: u64, - #[serde(deserialize_with = "deserialize_base64")] + #[serde(deserialize_with = "deserialize_inner_detached_sct_signature")] pub signature: Vec, - #[serde(deserialize_with = "deserialize_base64")] + #[serde_as(as = "Base64")] pub extensions: Vec, } -#[derive(Deserialize_repr, PartialEq, Debug)] +#[derive(Deserialize_repr, PartialEq, Debug, Clone)] #[repr(u8)] pub enum SCTVersion { V1 = 0, } +// TODO(tnytown): Make this type prettier. SigningCertificateDetachedSCT duplicates most of the data +// in cert and chain. pub struct CertificateResponse { pub cert: Certificate, pub chain: Vec, - // pub sct: InnerDetachedSCT, + pub detached_sct: Option, } diff --git a/src/sign.rs b/src/sign.rs index 1d4b36838e..95b3158b48 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -19,19 +19,18 @@ use std::time::SystemTime; use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; use hex; -use json_syntax::Print; use p256::NistP256; use pkcs8::der::{Encode, EncodePem}; use sha2::{Digest, Sha256}; use signature::DigestSigner; -use sigstore_protobuf_specs::{ - Bundle, DevSigstoreBundleV1VerificationMaterial, DevSigstoreCommonV1HashOutput, - DevSigstoreCommonV1LogId, DevSigstoreCommonV1MessageSignature, - DevSigstoreCommonV1X509Certificate, DevSigstoreCommonV1X509CertificateChain, - DevSigstoreRekorV1Checkpoint, DevSigstoreRekorV1InclusionPromise, - DevSigstoreRekorV1InclusionProof, DevSigstoreRekorV1KindVersion, - DevSigstoreRekorV1TransparencyLogEntry, +use sigstore_protobuf_specs::dev::sigstore::bundle::v1::bundle; +use sigstore_protobuf_specs::dev::sigstore::bundle::v1::{ + verification_material, Bundle, VerificationMaterial, }; +use sigstore_protobuf_specs::dev::sigstore::common::v1::{ + HashAlgorithm, HashOutput, MessageSignature, X509Certificate, X509CertificateChain, +}; +use sigstore_protobuf_specs::dev::sigstore::rekor::v1::TransparencyLogEntry; use tokio::io::AsyncRead; use tokio_util::io::SyncIoBridge; use url::Url; @@ -46,7 +45,6 @@ use crate::fulcio::{self, FulcioClient, FULCIO_ROOT}; use crate::oauth::IdentityToken; use crate::rekor::apis::configuration::Configuration as RekorConfiguration; use crate::rekor::apis::entries_api::create_log_entry; -use crate::rekor::models::LogEntry; use crate::rekor::models::{hashedrekord, proposed_entry::ProposedEntry as ProposedLogEntry}; /// An asynchronous Sigstore signing session. @@ -130,14 +128,15 @@ impl<'ctx> AsyncSigningSession<'ctx> { return Err(SigstoreError::ExpiredSigningSession()); } - // TODO(tnytown): Verify SCT here. - // Sign artifact. let input_hash: &[u8] = &hasher.clone().finalize(); + let mut signature_bytes = Vec::new(); let artifact_signature: p256::ecdsa::Signature = self.private_key.sign_digest(hasher); + artifact_signature + .to_der() + .encode_to_vec(&mut signature_bytes) + .expect("failed to encode Signature!"); - // Prepare inputs. - let b64_artifact_signature = base64.encode(artifact_signature.to_der()); let cert = &self.certs.cert; // Create the transparency log entry. @@ -145,9 +144,9 @@ impl<'ctx> AsyncSigningSession<'ctx> { api_version: "0.0.1".to_owned(), spec: hashedrekord::Spec { signature: hashedrekord::Signature { - content: b64_artifact_signature.clone(), + content: base64.encode(&signature_bytes), public_key: hashedrekord::PublicKey::new( - base64.encode(cert.to_pem(pkcs8::LineEnding::CRLF)?), + base64.encode(cert.to_pem(pkcs8::LineEnding::LF)?), ), }, data: hashedrekord::Data { @@ -166,10 +165,10 @@ impl<'ctx> AsyncSigningSession<'ctx> { // TODO(tnytown): Maybe run through the verification flow here? See sigstore-rs#296. Ok(SigningArtifact { - input_digest: base64.encode(input_hash), + input_digest: input_hash.to_owned(), cert: cert.to_der()?, - b64_signature: b64_artifact_signature, - log_entry: entry, + signature: signature_bytes, + log_entry: entry.try_into().expect("TODO"), }) } @@ -245,7 +244,10 @@ pub struct SigningContext { impl SigningContext { /// Manually constructs a [SigningContext] from its constituent data. - pub fn new(fulcio: FulcioClient, rekor_config: RekorConfiguration) -> Self { + pub fn new( + fulcio: FulcioClient, + rekor_config: RekorConfiguration, + ) -> Self { Self { fulcio, rekor_config, @@ -254,14 +256,14 @@ impl SigningContext { /// Returns a [SigningContext] configured against the public-good production Sigstore /// infrastructure. - pub fn production() -> Self { - Self::new( + pub fn production() -> SigstoreResult { + Ok(Self::new( FulcioClient::new( Url::parse(FULCIO_ROOT).expect("constant FULCIO root fails to parse!"), crate::fulcio::TokenProvider::Oauth(OauthTokenProvider::default()), ), Default::default(), - ) + )) } /// Configures and returns an [AsyncSigningSession] with the held context. @@ -282,10 +284,10 @@ impl SigningContext { /// A signature and its associated metadata. pub struct SigningArtifact { - input_digest: String, + input_digest: Vec, cert: Vec, - b64_signature: String, - log_entry: LogEntry, + signature: Vec, + log_entry: TransparencyLogEntry, } impl SigningArtifact { @@ -293,84 +295,35 @@ impl SigningArtifact { /// /// The resulting bundle can be serialized with [serde_json]. pub fn to_bundle(self) -> Bundle { - #[inline] - fn hex_to_base64>(hex: S) -> String { - let decoded = hex::decode(hex.as_ref()).expect("Malformed data in Rekor response"); - base64.encode(decoded) - } - // NOTE: We explicitly only include the leaf certificate in the bundle's "chain" // here: the specs explicitly forbid the inclusion of the root certificate, // and discourage inclusion of any intermediates (since they're in the root of // trust already). - let x_509_certificate_chain = Some(DevSigstoreCommonV1X509CertificateChain { - certificates: Some(vec![DevSigstoreCommonV1X509Certificate { - raw_bytes: Some(base64.encode(&self.cert)), - }]), - }); - - let inclusion_proof = if let Some(proof) = self.log_entry.verification.inclusion_proof { - let hashes = proof.hashes.iter().map(hex_to_base64).collect(); - Some(DevSigstoreRekorV1InclusionProof { - checkpoint: Some(DevSigstoreRekorV1Checkpoint { - envelope: Some(proof.checkpoint), - }), - hashes: Some(hashes), - log_index: Some(proof.log_index.to_string()), - root_hash: Some(hex_to_base64(proof.root_hash)), - tree_size: Some(proof.tree_size.to_string()), - }) - } else { - None - }; - - let canonicalized_body = { - let mut body = json_syntax::to_value(self.log_entry.body) - .expect("failed to parse constructed Body!"); - body.canonicalize(); - Some(base64.encode(body.compact_print().to_string())) + let x509_certificate_chain = X509CertificateChain { + certificates: vec![X509Certificate { + raw_bytes: self.cert, + }], }; - // TODO(tnytown): When we fix `sigstore_protobuf_specs`, have the Rekor client APIs convert - // responses into types from the specs as opposed to returning the raw `LogEntry` model type. - let tlog_entry = DevSigstoreRekorV1TransparencyLogEntry { - canonicalized_body, - inclusion_promise: Some(DevSigstoreRekorV1InclusionPromise { - // XX: sigstore-python deserializes the SET from base64 here because their protobuf - // library transparently serializes `bytes` fields as base64. - signed_entry_timestamp: Some(self.log_entry.verification.signed_entry_timestamp), - }), - inclusion_proof, - integrated_time: Some(self.log_entry.integrated_time.to_string()), - kind_version: Some(DevSigstoreRekorV1KindVersion { - kind: Some("hashedrekord".to_owned()), - version: Some("0.0.1".to_owned()), - }), - log_id: Some(DevSigstoreCommonV1LogId { - key_id: Some(hex_to_base64(self.log_entry.log_i_d)), - }), - log_index: Some(self.log_entry.log_index.to_string()), - }; - - let verification_material = Some(DevSigstoreBundleV1VerificationMaterial { - public_key: None, + let verification_material = Some(VerificationMaterial { timestamp_verification_data: None, - tlog_entries: Some(vec![tlog_entry]), - x_509_certificate_chain, + tlog_entries: vec![self.log_entry], + content: Some(verification_material::Content::X509CertificateChain( + x509_certificate_chain, + )), }); - let message_signature = Some(DevSigstoreCommonV1MessageSignature { - message_digest: Some(DevSigstoreCommonV1HashOutput { - algorithm: Some("SHA2_256".to_owned()), - digest: Some(self.input_digest), + let message_signature = MessageSignature { + message_digest: Some(HashOutput { + algorithm: HashAlgorithm::Sha2256.into(), + digest: self.input_digest, }), - signature: Some(self.b64_signature), - }); + signature: self.signature, + }; Bundle { - dsse_envelope: None, - media_type: Some(Version::Bundle0_2.to_string()), - message_signature, + media_type: Version::Bundle0_2.to_string(), verification_material, + content: Some(bundle::Content::MessageSignature(message_signature)), } } } diff --git a/src/tuf/mod.rs b/src/tuf/mod.rs index 07808ee55e..6fd4e2cd9f 100644 --- a/src/tuf/mod.rs +++ b/src/tuf/mod.rs @@ -37,21 +37,23 @@ use std::{ }; mod constants; -mod trustroot; use sha2::{Digest, Sha256}; +use sigstore_protobuf_specs::dev::sigstore::{ + common::v1::TimeRange, + trustroot::v1::{CertificateAuthority, TransparencyLogInstance, TrustedRoot}, +}; use tough::TargetName; use tracing::debug; use webpki::types::CertificateDer; -use self::trustroot::{CertificateAuthority, TimeRange, TransparencyLogInstance, TrustedRoot}; - use super::errors::{Result, SigstoreError}; /// A `Repository` owns all key material necessary for establishing a root of trust. pub trait Repository { fn fulcio_certs(&self) -> Result>; fn rekor_keys(&self) -> Result>; + fn ctfe_keys(&self) -> Result>; } /// A `ManualRepository` is a [Repository] with out-of-band trust materials. @@ -60,6 +62,7 @@ pub trait Repository { pub struct ManualRepository<'a> { pub fulcio_certs: Option>>, pub rekor_key: Option>, + pub ctfe_keys: Option>>, } impl Repository for ManualRepository<'_> { @@ -76,6 +79,13 @@ impl Repository for ManualRepository<'_> { None => Vec::new(), }) } + + fn ctfe_keys(&self) -> Result> { + Ok(match &self.ctfe_keys { + Some(keys) => keys.iter().map(|v| &v[..]).collect(), + None => Vec::new(), + }) + } } /// Securely fetches Rekor public key and Fulcio certificates from Sigstore's TUF repository. @@ -205,8 +215,9 @@ impl SigstoreRepository { fn tlog_keys(tlogs: &[TransparencyLogInstance]) -> impl Iterator { tlogs .iter() - .filter(|key| is_timerange_valid(key.public_key.valid_for.as_ref(), false)) - .filter_map(|key| key.public_key.raw_bytes.as_ref()) + .filter_map(|tlog| tlog.public_key.as_ref()) + .filter(|key| is_timerange_valid(key.valid_for.as_ref(), false)) + .filter_map(|key| key.raw_bytes.as_ref()) .map(|key_bytes| key_bytes.as_slice()) } @@ -216,8 +227,9 @@ impl SigstoreRepository { allow_expired: bool, ) -> impl Iterator { cas.iter() - .filter(move |ca| is_timerange_valid(Some(&ca.valid_for), allow_expired)) - .flat_map(|ca| ca.cert_chain.certificates.iter()) + .filter(move |ca| is_timerange_valid(ca.valid_for.as_ref(), allow_expired)) + .flat_map(|ca| ca.cert_chain.as_ref()) + .flat_map(|chain| chain.certificates.iter()) .map(|cert| cert.raw_bytes.as_slice()) } } @@ -266,25 +278,47 @@ impl Repository for SigstoreRepository { Ok(keys) } } + + /// Fetch CTFE public keys from the given TUF repository or reuse + /// the local cache if it's not outdated. + /// + /// The contents of the local cache are updated when they are outdated. + /// + /// **Warning:** this method needs special handling when invoked from + /// an async function because it performs blocking operations. + fn ctfe_keys(&self) -> Result> { + let root = self.trusted_root()?; + let keys: Vec<_> = Self::tlog_keys(&root.ctlogs).collect(); + + if keys.is_empty() { + Err(SigstoreError::TufMetadataError( + "CTFE keys not found".into(), + )) + } else { + Ok(keys) + } + } } /// Given a `range`, checks that the the current time is not before `start`. If /// `allow_expired` is `false`, also checks that the current time is not after /// `end`. fn is_timerange_valid(range: Option<&TimeRange>, allow_expired: bool) -> bool { - let time = chrono::Utc::now(); + let now = chrono::Utc::now().timestamp(); + + let start = range.and_then(|r| r.start.as_ref()).map(|t| t.seconds); + let end = range.and_then(|r| r.end.as_ref()).map(|t| t.seconds); - match range { + match (start, end) { // If there was no validity period specified, the key is always valid. - None => true, + (None, _) => true, // Active: if the current time is before the starting period, we are not yet valid. - Some(range) if time < range.start => false, - // If we want Expired keys, then the key is valid at this point. + (Some(start), _) if now < start => false, + // If we want Expired keys, then we don't need to check the end. _ if allow_expired => true, - // Otherwise, check that we are in range if the range has an end. - Some(range) => match range.end { - None => true, - Some(end) => time <= end, - }, + // If there is no expiry date, the key is valid. + (_, None) => true, + // If we have an expiry date, check it. + (_, Some(end)) => now <= end, } } diff --git a/src/tuf/trustroot.rs b/src/tuf/trustroot.rs deleted file mode 100644 index aeb321fd92..0000000000 --- a/src/tuf/trustroot.rs +++ /dev/null @@ -1,194 +0,0 @@ -#![allow(dead_code)] - -// HACK(jl): protobuf-specs schemas are currently compiled for direct dependencies of the Bundle schema. -// See note https://github.com/sigstore/protobuf-specs/blob/main/gen/pb-rust/src/lib.rs#L1-L23 -// HACK(ap): We should probably use definitions from sigstore-protobuf-specs, but -// the autogenerated definitions are unergonomic. Declare it locally here. - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use serde_with::base64::Base64; - -use serde_with::serde_as; - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[allow(non_camel_case_types)] -/// Only a subset of the secure hash standard algorithms are supported. -/// See for more -/// details. -/// UNSPECIFIED SHOULD not be used, primary reason for inclusion is to force -/// any proto JSON serialization to emit the used hash algorithm, as default -/// option is to *omit* the default value of an enum (which is the first -/// value, represented by '0'. -pub(crate) enum HashAlgorithm { - HASH_ALGORITHM_UNSPECIFIED = 0, - SHA2_256 = 1, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[allow(non_camel_case_types)] -/// Details of a specific public key, capturing the the key encoding method, -/// and signature algorithm. -/// To avoid the possibility of contradicting formats such as PKCS1 with -/// ED25519 the valid permutations are listed as a linear set instead of a -/// cartesian set (i.e one combined variable instead of two, one for encoding -/// and one for the signature algorithm). -pub(crate) enum PublicKeyDetails { - PUBLIC_KEY_DETAILS_UNSPECIFIED = 0, - // RSA - PKCS1_RSA_PKCS1V5 = 1, // See RFC8017 - PKCS1_RSA_PSS = 2, // See RFC8017 - PKIX_RSA_PKCS1V5 = 3, - PKIX_RSA_PSS = 4, - // ECDSA - PKIX_ECDSA_P256_SHA_256 = 5, // See NIST FIPS 186-4 - PKIX_ECDSA_P256_HMAC_SHA_256 = 6, // See RFC6979 - // Ed 25519 - PKIX_ED25519 = 7, // See RFC8032 -} - -#[serde_as] -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -/// LogId captures the identity of a transparency log. -pub(crate) struct LogId { - #[serde_as(as = "Base64")] - pub key_id: Vec, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -/// The time range is closed and includes both the start and end times, -/// (i.e., [start, end]). -/// End is optional to be able to capture a period that has started but -/// has no known end. -pub(crate) struct TimeRange { - pub start: DateTime, - pub end: Option>, -} - -#[serde_as] -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -pub(crate) struct PublicKey { - #[serde_as(as = "Option")] - pub raw_bytes: Option>, - pub key_details: PublicKeyDetails, - pub valid_for: Option, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -pub(crate) struct DistinguishedName { - pub organization: String, - pub common_name: String, -} - -#[serde_as] -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -pub(crate) struct X509Certificate { - #[serde_as(as = "Base64")] - pub raw_bytes: Vec, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -/// A chain of X.509 certificates. -pub(crate) struct X509CertificateChain { - pub certificates: Vec, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -/// TransparencyLogInstance describes the immutable parameters from a -/// transparency log. -/// See https://www.rfc-editor.org/rfc/rfc9162.html#name-log-parameters -/// for more details. -/// The included parameters are the minimal set required to identify a log, -/// and verify an inclusion proof/promise. -pub(crate) struct TransparencyLogInstance { - pub base_url: String, - pub hash_algorithm: HashAlgorithm, - pub public_key: PublicKey, - pub log_id: LogId, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -/// CertificateAuthority enlists the information required to identify which -/// CA to use and perform signature verification. -pub(crate) struct CertificateAuthority { - pub subject: DistinguishedName, - pub uri: Option, - pub cert_chain: X509CertificateChain, - pub valid_for: TimeRange, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -/// TrustedRoot describes the client's complete set of trusted entities. -/// How the TrustedRoot is populated is not specified, but can be a -/// combination of many sources such as TUF repositories, files on disk etc. -/// -/// The TrustedRoot is not meant to be used for any artifact verification, only -/// to capture the complete/global set of trusted verification materials. -/// When verifying an artifact, based on the artifact and policies, a selection -/// of keys/authorities are expected to be extracted and provided to the -/// verification function. This way the set of keys/authorities can be kept to -/// a minimal set by the policy to gain better control over what signatures -/// that are allowed. -/// -/// The embedded transparency logs, CT logs, CAs and TSAs MUST include any -/// previously used instance -- otherwise signatures made in the past cannot -/// be verified. -/// The currently used instances MUST NOT have their 'end' timestamp set in -/// their 'valid_for' attribute for easy identification. -/// All the listed instances SHOULD be sorted by the 'valid_for' in ascending -/// order, that is, the oldest instance first and the current instance last. -pub(crate) struct TrustedRoot { - pub media_type: String, - pub tlogs: Vec, - pub certificate_authorities: Vec, - pub ctlogs: Vec, - pub timestamp_authorities: Vec, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn tuf_serde_as_base64() { - let data = X509Certificate { - raw_bytes: b"Hello World".to_vec(), // NOTE(jl): value not representative - }; - let json = serde_json::json!({"rawBytes": "SGVsbG8gV29ybGQ=",}); - - assert_eq!(json, serde_json::to_value(&data).unwrap()); - assert_eq!(data, serde_json::from_value(json).unwrap()); - } - - #[test] - fn tuf_serde_as_nested_structure_base64() { - let data = PublicKey { - raw_bytes: Some(b"Hello World".to_vec()), - key_details: PublicKeyDetails::PKIX_ED25519, - valid_for: Some(TimeRange { - start: DateTime::from_timestamp(1_500_000_000, 0).unwrap(), - end: None, - }), - }; - let json = serde_json::json!({ - "rawBytes": "SGVsbG8gV29ybGQ=", - "keyDetails": "PKIX_ED25519", - "validFor": { - "start": "2017-07-14T02:40:00Z", - "end": None::> - } - }); - - assert_eq!(json, serde_json::to_value(&data).unwrap()); - assert_eq!(data, serde_json::from_value(json).unwrap()); - } -} diff --git a/src/verify/mod.rs b/src/verify/mod.rs index 5954d45bd6..95e3375ed3 100644 --- a/src/verify/mod.rs +++ b/src/verify/mod.rs @@ -18,7 +18,7 @@ mod models; pub use models::{VerificationError, VerificationMaterials, VerificationResult}; pub mod policy; -pub use policy::VerificationPolicy; +pub use policy::{PolicyError, VerificationPolicy}; mod verifier; pub use verifier::Verifier; diff --git a/src/verify/models.rs b/src/verify/models.rs index 19c021f75f..004a664423 100644 --- a/src/verify/models.rs +++ b/src/verify/models.rs @@ -14,31 +14,30 @@ // limitations under the License. use std::{ - cell::OnceCell, io::{self, Read}, str::FromStr, }; use crate::{ - bundle::required, bundle::Version as BundleVersion, crypto::certificate::{is_leaf, is_root_ca}, - errors::SigstoreError, - rekor::models::log_entry, - rekor::models::{ - log_entry::{InclusionProof, Verification}, - LogEntry, - }, + rekor::models as rekor, }; +use crate::Bundle; use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; -use pkcs8::der::Decode; +use pkcs8::der::{Decode, EncodePem}; use sha2::{Digest, Sha256}; -use sigstore_protobuf_specs::Bundle; +use sigstore_protobuf_specs::dev::sigstore::{ + bundle::v1::{bundle, verification_material}, + rekor::v1::{InclusionProof, TransparencyLogEntry}, +}; use thiserror::Error; -use tracing::warn; +use tracing::{debug, error, warn}; use x509_cert::Certificate; +use super::policy::PolicyError; + #[derive(Error, Debug)] pub enum VerificationError { #[error("Certificate expired before time of signing")] @@ -56,16 +55,18 @@ pub enum VerificationError { #[error("Failed to verify that the signature corresponds to the input")] SignatureVerificationFailure, - #[error("{0}")] - PolicyFailure(String), + #[error(transparent)] + PolicyFailure(#[from] PolicyError), } pub type VerificationResult = Result<(), VerificationError>; pub struct VerificationMaterials { - pub input_digest: Vec, - pub certificate: Certificate, - pub signature: Vec, - rekor_entry: OnceCell, + pub(crate) input_digest: Vec, + pub(crate) certificate: Certificate, + pub(crate) signature: Vec, + rekor_entry: TransparencyLogEntry, + + offline: bool, } impl VerificationMaterials { @@ -74,33 +75,29 @@ impl VerificationMaterials { certificate: Certificate, signature: Vec, offline: bool, - rekor_entry: Option, + rekor_entry: TransparencyLogEntry, ) -> Option { let mut hasher = Sha256::new(); io::copy(input, &mut hasher).ok()?; - if offline && rekor_entry.is_none() { - // offline verification requires a Rekor entry + if matches!( + rekor_entry, + TransparencyLogEntry { + inclusion_promise: None, + inclusion_proof: None, + .. + } + ) { + error!("encountered TransparencyLogEntry without any inclusion materials"); return None; } - let rekor_entry = if let Some(rekor_entry) = rekor_entry { - let cell = OnceCell::new(); - - // TODO(tnytown): Switch to setting if offline when Rekor fetching is implemented. - #[allow(clippy::unwrap_used)] - cell.set(rekor_entry).unwrap(); - - cell - } else { - Default::default() - }; - Some(Self { input_digest: hasher.finalize().to_vec(), rekor_entry, certificate, signature, + offline, }) } @@ -108,27 +105,36 @@ impl VerificationMaterials { /// /// For details on bundle semantics, please refer to [VerificationMaterial]. /// - /// [VerificationMaterial]: sigstore_protobuf_specs::DevSigstoreBundleV1VerificationMaterial - /// - /// TODO(tnytown): Determine if this type should yield SigstoreResult. + /// [VerificationMaterial]: sigstore_protobuf_specs::dev::sigstore::bundle::v1::VerificationMaterial pub fn from_bundle(input: &mut R, bundle: Bundle, offline: bool) -> Option { - fn certificate_from_base64(encoded: &str) -> Option { - Certificate::from_der(&base64.decode(encoded).ok()?).ok() - } - - let certs = required!( - bundle; - verification_material.x_509_certificate_chain.certificates, - SigstoreError::SigstoreBundleMalformedError("Cannot find required field in bundle".to_string()) - ).ok()?; + let (content, mut tlog_entries) = match bundle.verification_material { + Some(m) => (m.content, m.tlog_entries), + _ => { + error!("bundle missing VerificationMaterial"); + return None; + } + }; // Parse the certificates. The first entry in the chain MUST be a leaf certificate, and the // rest of the chain MUST NOT include a root CA or any intermediate CAs that appear in an // independent root of trust. + let certs = match content { + Some(verification_material::Content::X509CertificateChain(ch)) => ch.certificates, + Some(verification_material::Content::Certificate(cert)) => { + vec![cert] + } + _ => { + error!("bundle includes unsupported VerificationMaterial Content"); + return None; + } + }; let certs = certs .iter() - .map(|cert| certificate_from_base64(cert.raw_bytes.as_ref()?)) - .collect::>>()?; + .map(|c| c.raw_bytes.as_slice()) + .map(Certificate::from_der) + .collect::, _>>() + .ok()?; + let [leaf_cert, chain_certs @ ..] = &certs[..] else { return None; }; @@ -143,80 +149,107 @@ impl VerificationMaterials { } } - let signature = base64 - .decode(required!(bundle; message_signature.signature)?) - .ok()?; - let tlog_entries = required!(bundle; verification_material.tlog_entries)?; + let signature = match bundle.content? { + bundle::Content::MessageSignature(s) => s.signature, + _ => { + error!("bundle includes unsupported DSSE signature"); + return None; + } + }; + if tlog_entries.len() != 1 { - // Expected exactly one tlog entry. + error!("bundle expected 1 tlog entry; got {}", tlog_entries.len()); return None; } - let tlog_entry = &tlog_entries[0]; + let tlog_entry = tlog_entries.remove(0); - let inclusion_promise = &tlog_entry.inclusion_promise; - let inclusion_proof = tlog_entry.inclusion_proof.as_ref(); + let (inclusion_promise, inclusion_proof) = + (&tlog_entry.inclusion_promise, &tlog_entry.inclusion_proof); - let has_checkpoint = required!(; inclusion_proof.checkpoint.envelope).is_some(); - match BundleVersion::from_str(&bundle.media_type?).ok()? { - BundleVersion::Bundle0_1 => { + // `inclusion_proof` is now a required field in the protobuf spec, + // but older versions of Rekor didn't provide inclusion proofs. + // + // https://github.com/sigstore/sigstore-python/pull/634#discussion_r1182769140 + match BundleVersion::from_str(&bundle.media_type) { + Ok(BundleVersion::Bundle0_1) => { if inclusion_promise.is_none() { - // 0.1 bundle must contain inclusion promise + error!("bundle must contain inclusion promise"); return None; } - if inclusion_proof.is_some() && !has_checkpoint { - warn!("0.1 bundle contains inclusion proof without checkpoint; ignoring"); + if matches!( + inclusion_proof, + Some(InclusionProof { + checkpoint: None, + .. + }) + ) { + debug!("0.1 bundle contains inclusion proof without checkpoint"); } } - BundleVersion::Bundle0_2 => { - inclusion_proof?; - if !has_checkpoint { - // inclusion proofs must contain checkpoints + Ok(BundleVersion::Bundle0_2) => { + if inclusion_proof.is_none() { + error!("bundle must contain inclusion proof"); + return None; + } + + if matches!( + inclusion_proof, + Some(InclusionProof { + checkpoint: None, + .. + }) + ) { + error!("bundle must contain checkpoint"); return None; } } + Err(_) => { + error!("unknown bundle version"); + return None; + } } - let parsed_inclusion_proof = if inclusion_proof.is_some() && has_checkpoint { - Some(InclusionProof { - checkpoint: required!(; inclusion_proof.checkpoint.envelope)?.clone(), - hashes: required!(; inclusion_proof.hashes)?.clone(), - log_index: required!(; inclusion_proof.log_index)?.parse().ok()?, - root_hash: required!(; inclusion_proof.log_index)?.clone(), - tree_size: required!(; inclusion_proof.tree_size)?.parse().ok()?, - }) - } else { - None - }; + Self::new(input, leaf_cert.clone(), signature, offline, tlog_entry) + } - let canonicalized_body = { - let decoded = base64 - .decode(tlog_entry.canonicalized_body.as_ref()?) - .ok()?; - serde_json::from_slice(&decoded).ok()? - }; - // required!(tlog_entry; log_id.key_id)?.clone(); - let entry = LogEntry { - uuid: "".to_string(), - body: log_entry::Body::hashedrekord(canonicalized_body), - attestation: None, - integrated_time: required!(tlog_entry; integrated_time)?.parse().ok()?, - log_i_d: "".into(), - log_index: required!(tlog_entry; log_index)?.parse().ok()?, - verification: Verification { - inclusion_proof: parsed_inclusion_proof, - signed_entry_timestamp: required!(; inclusion_promise.signed_entry_timestamp)? - .clone(), + /// Retrieves the [LogEntry] for the materials. + pub fn rekor_entry(&self) -> Option<&TransparencyLogEntry> { + let base64_pem_certificate = + base64.encode(self.certificate.to_pem(pkcs8::LineEnding::LF).ok()?); + + let expected_entry = rekor::Hashedrekord { + kind: "hashedrekord".to_owned(), + api_version: "0.0.1".to_owned(), + spec: rekor::hashedrekord::Spec { + signature: rekor::hashedrekord::Signature { + content: base64.encode(&self.signature), + public_key: rekor::hashedrekord::PublicKey::new(base64_pem_certificate), + }, + data: rekor::hashedrekord::Data { + hash: rekor::hashedrekord::Hash { + algorithm: rekor::hashedrekord::AlgorithmKind::sha256, + value: hex::encode(&self.input_digest), + }, + }, }, }; - Self::new(input, leaf_cert.clone(), signature, offline, Some(entry)) - } + let entry = if !self.offline && self.rekor_entry.inclusion_proof.is_none() { + warn!("online rekor fetching is not implemented yet, but is necessary for this bundle"); + return None; + } else { + &self.rekor_entry + }; - /// Retrieves the [LogEntry] for the materials. - pub fn rekor_entry(&self) -> &LogEntry { - // TODO(tnytown): Fetch online Rekor entry, confirm consistency, and get_or_init here. - #[allow(clippy::unwrap_used)] - self.rekor_entry.get().unwrap() + let actual: serde_json::Value = + serde_json::from_slice(&self.rekor_entry.canonicalized_body).ok()?; + let expected: serde_json::Value = serde_json::to_value(expected_entry).ok()?; + + if actual != expected { + return None; + } + + Some(entry) } } diff --git a/src/verify/policy.rs b/src/verify/policy.rs index 8f0791c50f..ec6f0409f3 100644 --- a/src/verify/policy.rs +++ b/src/verify/policy.rs @@ -17,12 +17,10 @@ //! use const_oid::ObjectIdentifier; +use thiserror::Error; +use tracing::warn; use x509_cert::ext::pkix::{name::GeneralName, SubjectAltName}; -use crate::verify::VerificationError; - -use super::models::VerificationResult; - macro_rules! oids { ($($name:ident = $value:literal),+) => { $(const $name: ObjectIdentifier = ObjectIdentifier::new_unwrap($value);)+ @@ -65,7 +63,32 @@ oids! { } -/// A trait for policies that check a single textual value against a X.509 extension. +#[derive(Error, Debug)] +pub enum PolicyError { + #[error("did not find exactly 1 of the required extension in the certificate")] + ExtensionNotFound, + + #[error("certificate's {extension} does not match (got {actual}, expected {expected})")] + ExtensionCheckFailed { + extension: String, + expected: String, + actual: String, + }, + + #[error("{0} of {total} policies failed: {1}\n- ", + errors.len(), + errors.iter().map(|e| e.to_string()).collect::>().join("\n- ") + )] + AllOf { + total: usize, + errors: Vec, + }, + + #[error("0 of {total} policies succeeded")] + AnyOf { total: usize }, +} + +/// A policy that checks a single textual value against a X.509 extension. pub trait SingleX509ExtPolicy { fn new>(val: S) -> Self; fn name() -> &'static str; @@ -73,15 +96,13 @@ pub trait SingleX509ExtPolicy { } impl VerificationPolicy for T { - fn verify(&self, cert: &x509_cert::Certificate) -> VerificationResult { + fn verify(&self, cert: &x509_cert::Certificate) -> Option { let extensions = cert.tbs_certificate.extensions.as_deref().unwrap_or(&[]); let mut extensions = extensions.iter().filter(|ext| ext.extn_id == T::OID); // Check for exactly one extension. let (Some(ext), None) = (extensions.next(), extensions.next()) else { - return Err(VerificationError::PolicyFailure( - "Cannot get policy extensions from certificate".into(), - )); + return Some(PolicyError::ExtensionNotFound); }; // Parse raw string without DER encoding. @@ -89,14 +110,13 @@ impl VerificationPolicy for T .expect("failed to parse constructed Extension!"); if val != self.value() { - Err(VerificationError::PolicyFailure(format!( - "Certificate's {} does not match (got {}, expected {})", - T::name(), - val, - self.value() - ))) + Some(PolicyError::ExtensionCheckFailed { + extension: T::name().to_owned(), + expected: self.value().to_owned(), + actual: val.to_owned(), + }) } else { - Ok(()) + None } } } @@ -139,7 +159,7 @@ impl_policy!( /// An interface that all policies must conform to. pub trait VerificationPolicy { - fn verify(&self, cert: &x509_cert::Certificate) -> VerificationResult; + fn verify(&self, cert: &x509_cert::Certificate) -> Option; } /// The "any of" policy, corresponding to a logical OR between child policies. @@ -158,15 +178,13 @@ impl<'a> AnyOf<'a> { } impl VerificationPolicy for AnyOf<'_> { - fn verify(&self, cert: &x509_cert::Certificate) -> VerificationResult { + fn verify(&self, cert: &x509_cert::Certificate) -> Option { self.children .iter() - .find(|policy| policy.verify(cert).is_ok()) - .ok_or(VerificationError::PolicyFailure(format!( - "0 of {} policies succeeded", - self.children.len() - ))) - .map(|_| ()) + .find(|policy| policy.verify(cert).is_some()) + .map(|_| PolicyError::AnyOf { + total: self.children.len(), + }) } } @@ -178,39 +196,33 @@ pub struct AllOf<'a> { } impl<'a> AllOf<'a> { - pub fn new>(policies: I) -> Self { - Self { - children: policies.into_iter().collect(), + pub fn new>(policies: I) -> Option { + let children: Vec<_> = policies.into_iter().collect(); + + // Without this, we'd be able to construct an `AllOf` containing an empty list of child + // policies. This is almost certainly not what the user wants and is a potential source + // of API misuse, so we explicitly disallow it. + if children.is_empty() { + warn!("attempted to construct an AllOf with an empty list of child policies"); + return None; } + + Some(Self { children }) } } impl VerificationPolicy for AllOf<'_> { - fn verify(&self, cert: &x509_cert::Certificate) -> VerificationResult { - // Without this, we'd consider empty lists of child policies trivially valid. - // This is almost certainly not what the user wants and is a potential - // source of API misuse, so we explicitly disallow it. - if self.children.is_empty() { - return Err(VerificationError::PolicyFailure( - "no child policies to verify".into(), - )); - } - + fn verify(&self, cert: &x509_cert::Certificate) -> Option { let results = self.children.iter().map(|policy| policy.verify(cert)); - let failures: Vec<_> = results - .filter_map(|result| result.err()) - .map(|err| err.to_string()) - .collect(); + let failures: Vec<_> = results.flatten().collect(); if failures.is_empty() { - Ok(()) + None } else { - Err(VerificationError::PolicyFailure(format!( - "{} of {} policies failed:\n- {}", - failures.len(), - self.children.len(), - failures.join("\n- ") - ))) + Some(PolicyError::AllOf { + total: self.children.len(), + errors: failures, + }) } } } @@ -218,9 +230,9 @@ impl VerificationPolicy for AllOf<'_> { pub(crate) struct UnsafeNoOp; impl VerificationPolicy for UnsafeNoOp { - fn verify(&self, _cert: &x509_cert::Certificate) -> VerificationResult { - eprintln!("unsafe (no-op) verification policy used! no verification performed!"); - VerificationResult::Ok(()) + fn verify(&self, _cert: &x509_cert::Certificate) -> Option { + warn!("unsafe (no-op) verification policy used! no verification performed!"); + None } } @@ -248,14 +260,14 @@ impl Identity { } impl VerificationPolicy for Identity { - fn verify(&self, cert: &x509_cert::Certificate) -> VerificationResult { - if let err @ Err(_) = self.issuer.verify(cert) { + fn verify(&self, cert: &x509_cert::Certificate) -> Option { + if let err @ Some(_) = self.issuer.verify(cert) { return err; } let (_, san): (bool, SubjectAltName) = match cert.tbs_certificate.get() { Ok(Some(result)) => result, - _ => return Err(VerificationError::CertificateMalformed), + _ => return Some(PolicyError::ExtensionNotFound), }; let names: Vec<_> = san @@ -272,13 +284,13 @@ impl VerificationPolicy for Identity { .collect(); if names.contains(&self.identity.as_str()) { - Ok(()) + None } else { - Err(VerificationError::PolicyFailure(format!( - "Certificate's SANs do not match {}; actual SANs: {}", - self.identity, - names.join(", ") - ))) + Some(PolicyError::ExtensionCheckFailed { + extension: "SubjectAltName".to_owned(), + expected: self.identity.clone(), + actual: names.join(", "), + }) } } } diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index 2645b87919..3c695995cf 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -15,8 +15,13 @@ use std::cell::OnceCell; use const_oid::db::rfc5280::ID_KP_CODE_SIGNING; -use pkcs8::der::Encode; -use webpki::types::UnixTime; +use tracing::debug; +use webpki::{ + types::{CertificateDer, UnixTime}, + EndEntityCert, +}; + +use x509_cert::der::Encode; use x509_cert::ext::pkix::{ExtendedKeyUsage, KeyUsage}; use crate::{ @@ -45,7 +50,6 @@ impl<'a, R: Repository> Verifier<'a, R> { }) } - /// TODO(tnytown): Evil (?) interior mutability hack to work around lifetime issues. fn cert_pool(&'a self) -> SigstoreResult<&CertificatePool<'a>> { let init_cert_pool = || { let certs = self.trust_repo.fulcio_certs()?; @@ -82,36 +86,31 @@ impl<'a, R: Repository> Verifier<'a, R> { // 7) Verify that the signing certificate was valid at the time of // signing by comparing the expiry against the integrated timestamp. - // 1) Verify that the signing certificate is signed by the root certificate and that the - // signing certificate was valid at the time of signing. - // 1) Verify that the signing certificate is signed by the certificate // chain and that the signing certificate was valid at the time // of signing. - let issued_at = materials - .certificate - .tbs_certificate - .validity - .not_before - .to_unix_duration(); - let cert_der = &materials + let tbs_certificate = &materials.certificate.tbs_certificate; + let issued_at = tbs_certificate.validity.not_before.to_unix_duration(); + let cert_der: CertificateDer = materials .certificate .to_der() - .expect("failed to DER-encode constructed Certificate!"); - store - .verify_cert_with_time(cert_der, UnixTime::since_unix_epoch(issued_at)) - .or(Err(VerificationError::CertificateVerificationFailure))?; + .expect("failed to DER-encode constructed Certificate!") + .into(); + let ee_cert: EndEntityCert = (&cert_der).try_into().expect("TODO"); + + let Ok(_trusted_chain) = + store.verify_cert_with_time(&ee_cert, UnixTime::since_unix_epoch(issued_at)) + else { + return Err(VerificationError::CertificateVerificationFailure); + }; + + debug!("signing certificate chains back to trusted root"); // 2) Verify that the signing certificate belongs to the signer. - // TODO(tnytown): How likely is a malformed certificate in this position? Do we want to - // account for it and create an error type as opposed to unwrapping? - let (_, key_usage_ext): (bool, KeyUsage) = materials - .certificate - .tbs_certificate - .get() - .expect("Malformed certificate") - .expect("Malformed certificate"); + let Ok(Some((_, key_usage_ext))) = tbs_certificate.get::() else { + return Err(VerificationError::CertificateMalformed); + }; if !key_usage_ext.digital_signature() { return Err(VerificationError::CertificateTypeError( @@ -119,12 +118,10 @@ impl<'a, R: Repository> Verifier<'a, R> { )); } - let (_, extended_key_usage_ext): (bool, ExtendedKeyUsage) = materials - .certificate - .tbs_certificate - .get() - .expect("Malformed certificate") - .expect("Malformed certificate"); + let Ok(Some((_, extended_key_usage_ext))) = tbs_certificate.get::() + else { + return Err(VerificationError::CertificateMalformed); + }; if !extended_key_usage_ext.0.contains(&ID_KP_CODE_SIGNING) { return Err(VerificationError::CertificateTypeError( @@ -132,28 +129,33 @@ impl<'a, R: Repository> Verifier<'a, R> { )); } - policy.verify(&materials.certificate)?; + if let Some(err) = policy.verify(&materials.certificate) { + return Err(err)?; + } + debug!("signing certificate conforms to policy"); // 3) Verify that the signature was signed by the public key in the signing certificate - let signing_key: SigstoreResult = (&materials - .certificate - .tbs_certificate - .subject_public_key_info) - .try_into(); - - let signing_key = - signing_key.expect("Malformed certificate (cannot deserialize public key)"); + let Ok(signing_key): SigstoreResult = + (&tbs_certificate.subject_public_key_info).try_into() + else { + return Err(VerificationError::CertificateMalformed); + }; - signing_key - .verify_prehash( - Signature::Raw(&materials.signature), - &materials.input_digest, - ) - .or(Err(VerificationError::SignatureVerificationFailure))?; + let verify_sig = signing_key.verify_prehash( + Signature::Raw(&materials.signature), + &materials.input_digest, + ); + if verify_sig.is_err() { + return Err(VerificationError::SignatureVerificationFailure); + } + debug!("signature corresponds to public key"); // 4) Verify that the Rekor entry is consistent with the other signing // materials - let log_entry = materials.rekor_entry(); + let Some(log_entry) = materials.rekor_entry() else { + return Err(VerificationError::CertificateMalformed); + }; + debug!("log entry is consistent with other materials"); // 5) Verify the inclusion proof supplied by Rekor for this artifact, // if we're doing online verification. @@ -166,16 +168,12 @@ impl<'a, R: Repository> Verifier<'a, R> { // 7) Verify that the signing certificate was valid at the time of // signing by comparing the expiry against the integrated timestamp. let integrated_time = log_entry.integrated_time as u64; - let not_before = materials - .certificate - .tbs_certificate + let not_before = tbs_certificate .validity .not_before .to_unix_duration() .as_secs(); - let not_after = materials - .certificate - .tbs_certificate + let not_after = tbs_certificate .validity .not_after .to_unix_duration() @@ -183,7 +181,9 @@ impl<'a, R: Repository> Verifier<'a, R> { if !(not_before <= integrated_time && integrated_time <= not_after) { return Err(VerificationError::CertificateExpired); } + debug!("data signed during validity period"); + debug!("successfully verified!"); Ok(()) } } From 428aa41caa0fa5eb5faf388ba395722773c7efdb Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Thu, 15 Feb 2024 16:25:34 -0600 Subject: [PATCH 05/27] Add `bundle` feature Fixes compile on `--no-default-features`. Signed-off-by: Andrew Pan --- Cargo.toml | 5 +++-- src/fulcio/mod.rs | 2 +- src/lib.rs | 2 ++ src/sign.rs | 5 +---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d19a7cb041..46ad8fc908 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,8 +42,9 @@ rekor = ["reqwest"] tuf = ["tough", "regex"] -sign = [] -verify = [] +bundle = [] +sign = ["bundle"] +verify = ["bundle"] cosign-native-tls = [ "oci-distribution/native-tls", diff --git a/src/fulcio/mod.rs b/src/fulcio/mod.rs index 1330354022..a96f0a18ce 100644 --- a/src/fulcio/mod.rs +++ b/src/fulcio/mod.rs @@ -16,7 +16,7 @@ use serde::{Serialize, Serializer}; use std::convert::{TryFrom, TryInto}; use std::fmt::{Debug, Display, Formatter}; use url::Url; -use x509_cert::{Certificate, der::Decode}; +use x509_cert::{der::Decode, Certificate}; pub use models::{CertificateResponse, SigningCertificateDetachedSCT}; diff --git a/src/lib.rs b/src/lib.rs index 3346d8d57e..39ee2ddeb0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -285,7 +285,9 @@ pub mod rekor; pub mod tuf; // Don't export yet -- these types should only be useful internally. +#[cfg(feature = "bundle")] mod bundle; +#[cfg(feature = "bundle")] pub use bundle::Bundle; #[cfg(feature = "verify")] diff --git a/src/sign.rs b/src/sign.rs index 95b3158b48..d5419851f0 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -244,10 +244,7 @@ pub struct SigningContext { impl SigningContext { /// Manually constructs a [SigningContext] from its constituent data. - pub fn new( - fulcio: FulcioClient, - rekor_config: RekorConfiguration, - ) -> Self { + pub fn new(fulcio: FulcioClient, rekor_config: RekorConfiguration) -> Self { Self { fulcio, rekor_config, From 7d0db8b1004004b012f9f84c51f70b7733d47c59 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Thu, 15 Feb 2024 17:13:37 -0600 Subject: [PATCH 06/27] verify: rework certificate validation Signed-off-by: Andrew Pan --- src/crypto/certificate.rs | 29 +++++++++++++++++++---------- src/fulcio/models.rs | 2 -- src/sign.rs | 9 +++++++-- src/verify/verifier.rs | 32 +++++--------------------------- 4 files changed, 31 insertions(+), 41 deletions(-) diff --git a/src/crypto/certificate.rs b/src/crypto/certificate.rs index 34ae09cbc6..d82b4d07b2 100644 --- a/src/crypto/certificate.rs +++ b/src/crypto/certificate.rs @@ -20,7 +20,10 @@ use x509_cert::{ Certificate, }; -use crate::errors::{Result, SigstoreError}; +use crate::{ + crypto::certificate, + errors::{Result, SigstoreError}, +}; /// Ensure the given certificate can be trusted for verifying cosign /// signatures. @@ -127,7 +130,7 @@ fn verify_expiration(certificate: &Certificate, integrated_time: i64) -> Result< /// * It has `CODE_SIGNING` as an `ExtendedKeyUsage`. /// /// This function does not evaluate the trustworthiness of the certificate. -pub(crate) fn is_leaf(certificate: &Certificate) -> Result { +pub(crate) fn is_leaf(certificate: &Certificate) -> Result<()> { // NOTE(jl): following structure of sigstore-python over the slightly different handling found // in `verify_key_usages`. let tbs = &certificate.tbs_certificate; @@ -139,13 +142,15 @@ pub(crate) fn is_leaf(certificate: &Certificate) -> Result { } if is_ca(certificate)? { - return Ok(false); + return Err(SigstoreError::InvalidCertError( + "invalid signing cert: missing KeyUsage".to_string(), + )); }; let digital_signature = match tbs.get::()? { None => { return Err(SigstoreError::InvalidCertError( - "invalid X.509 certificate: missing KeyUsage".to_string(), + "invalid signing cert: missing KeyUsage".to_string(), )) } Some((_, key_usage)) => key_usage.digital_signature(), @@ -153,8 +158,7 @@ pub(crate) fn is_leaf(certificate: &Certificate) -> Result { if !digital_signature { return Err(SigstoreError::InvalidCertError( - "invalid certificate for Sigstore purposes: missing digital signature usage" - .to_string(), + "invalid signing cert: missing digital signature usage".to_string(), )); } @@ -165,13 +169,19 @@ pub(crate) fn is_leaf(certificate: &Certificate) -> Result { let extended_key_usage = match tbs.get::()? { None => { return Err(SigstoreError::InvalidCertError( - "invalid X.509 certificate: missing ExtendedKeyUsage".to_string(), + "invalid signing cert: missing ExtendedKeyUsage".to_string(), )) } Some((_, extended_key_usage)) => extended_key_usage, }; - Ok(extended_key_usage.0.contains(&ID_KP_CODE_SIGNING)) + if !extended_key_usage.0.contains(&ID_KP_CODE_SIGNING) { + return Err(SigstoreError::InvalidCertError( + "invalid signing cert: missing CODE_SIGNING ExtendedKeyUsage".into(), + )); + } + + Ok(()) } /// Checks if the given `certificate` is a CA certificate. @@ -257,8 +267,7 @@ pub(crate) fn is_root_ca(certificate: &Certificate) -> Result { } // A certificate that is its own issuer and signer is considered a root CA. - // TODO(jl): verify_directly_issued_by - todo!() + Ok(tbs.issuer == tbs.subject) } #[cfg(test)] diff --git a/src/fulcio/models.rs b/src/fulcio/models.rs index 0a784a76d0..3441f4ca44 100644 --- a/src/fulcio/models.rs +++ b/src/fulcio/models.rs @@ -119,8 +119,6 @@ pub enum SCTVersion { V1 = 0, } -// TODO(tnytown): Make this type prettier. SigningCertificateDetachedSCT duplicates most of the data -// in cert and chain. pub struct CertificateResponse { pub cert: Certificate, pub chain: Vec, diff --git a/src/sign.rs b/src/sign.rs index d5419851f0..eac694f465 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -158,9 +158,14 @@ impl<'ctx> AsyncSigningSession<'ctx> { }, }; - let entry = create_log_entry(&self.context.rekor_config, proposed_entry) + let log_entry = create_log_entry(&self.context.rekor_config, proposed_entry) .await .map_err(|err| SigstoreError::RekorClientError(err.to_string()))?; + let log_entry = log_entry + .try_into() + .or(Err(SigstoreError::RekorClientError( + "Rekor returned malformed LogEntry".into(), + )))?; // TODO(tnytown): Maybe run through the verification flow here? See sigstore-rs#296. @@ -168,7 +173,7 @@ impl<'ctx> AsyncSigningSession<'ctx> { input_digest: input_hash.to_owned(), cert: cert.to_der()?, signature: signature_bytes, - log_entry: entry.try_into().expect("TODO"), + log_entry, }) } diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index 3c695995cf..b3927ac5e6 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -14,7 +14,6 @@ use std::cell::OnceCell; -use const_oid::db::rfc5280::ID_KP_CODE_SIGNING; use tracing::debug; use webpki::{ types::{CertificateDer, UnixTime}, @@ -22,7 +21,6 @@ use webpki::{ }; use x509_cert::der::Encode; -use x509_cert::ext::pkix::{ExtendedKeyUsage, KeyUsage}; use crate::{ crypto::{CertificatePool, CosignVerificationKey, Signature}, @@ -96,7 +94,9 @@ impl<'a, R: Repository> Verifier<'a, R> { .to_der() .expect("failed to DER-encode constructed Certificate!") .into(); - let ee_cert: EndEntityCert = (&cert_der).try_into().expect("TODO"); + let Ok(ee_cert) = (&cert_der).try_into() else { + return Err(VerificationError::CertificateVerificationFailure); + }; let Ok(_trusted_chain) = store.verify_cert_with_time(&ee_cert, UnixTime::since_unix_epoch(issued_at)) @@ -107,28 +107,6 @@ impl<'a, R: Repository> Verifier<'a, R> { debug!("signing certificate chains back to trusted root"); // 2) Verify that the signing certificate belongs to the signer. - - let Ok(Some((_, key_usage_ext))) = tbs_certificate.get::() else { - return Err(VerificationError::CertificateMalformed); - }; - - if !key_usage_ext.digital_signature() { - return Err(VerificationError::CertificateTypeError( - "Key usage is not of type `digital signature`".into(), - )); - } - - let Ok(Some((_, extended_key_usage_ext))) = tbs_certificate.get::() - else { - return Err(VerificationError::CertificateMalformed); - }; - - if !extended_key_usage_ext.0.contains(&ID_KP_CODE_SIGNING) { - return Err(VerificationError::CertificateTypeError( - "Extended key usage does not contain `code signing`".into(), - )); - } - if let Some(err) = policy.verify(&materials.certificate) { return Err(err)?; } @@ -159,11 +137,11 @@ impl<'a, R: Repository> Verifier<'a, R> { // 5) Verify the inclusion proof supplied by Rekor for this artifact, // if we're doing online verification. - // TODO(tnytown): Merkle inclusion + // TODO(tnytown): Merkle inclusion; sigstore-rs#285 // 6) Verify the Signed Entry Timestamp (SET) supplied by Rekor for this // artifact. - // TODO(tnytown) SET verification + // TODO(tnytown) SET verification; sigstore-rs#285 // 7) Verify that the signing certificate was valid at the time of // signing by comparing the expiry against the integrated timestamp. From ffc22325a241d2d11e013188a70dc4d0311284ff Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Thu, 15 Feb 2024 18:02:20 -0600 Subject: [PATCH 07/27] crypto: fixup is_root_ca & usages Signed-off-by: Andrew Pan --- src/crypto/certificate.rs | 2 +- src/verify/models.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/certificate.rs b/src/crypto/certificate.rs index d82b4d07b2..6c14050fa7 100644 --- a/src/crypto/certificate.rs +++ b/src/crypto/certificate.rs @@ -262,7 +262,7 @@ pub(crate) fn is_root_ca(certificate: &Certificate) -> Result { } // Non-CAs can't possibly be root CAs. - if !is_ca(certificate)? { + if !matches!(is_ca(certificate), Ok(true)) { return Ok(false); } diff --git a/src/verify/models.rs b/src/verify/models.rs index 004a664423..8d22497995 100644 --- a/src/verify/models.rs +++ b/src/verify/models.rs @@ -144,7 +144,7 @@ impl VerificationMaterials { } for chain_cert in chain_certs { - if is_root_ca(chain_cert).is_ok() { + if matches!(is_root_ca(chain_cert), Ok(true)) { return None; } } From 0e792048eaaa096077ee5508f15d356999494c39 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Thu, 15 Feb 2024 18:07:38 -0600 Subject: [PATCH 08/27] conformance: fixup Signed-off-by: Andrew Pan --- src/crypto/certificate.rs | 1 - src/verify/verifier.rs | 1 - tests/conformance/conformance.rs | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/crypto/certificate.rs b/src/crypto/certificate.rs index 6c14050fa7..5a3334d0f6 100644 --- a/src/crypto/certificate.rs +++ b/src/crypto/certificate.rs @@ -21,7 +21,6 @@ use x509_cert::{ }; use crate::{ - crypto::certificate, errors::{Result, SigstoreError}, }; diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index b3927ac5e6..9b6e4c3da4 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -17,7 +17,6 @@ use std::cell::OnceCell; use tracing::debug; use webpki::{ types::{CertificateDer, UnixTime}, - EndEntityCert, }; use x509_cert::der::Encode; diff --git a/tests/conformance/conformance.rs b/tests/conformance/conformance.rs index cfb11b7f72..771c0091bb 100644 --- a/tests/conformance/conformance.rs +++ b/tests/conformance/conformance.rs @@ -140,7 +140,7 @@ fn sign_bundle(args: SignBundle) -> anyhow::Result<()> { let bundle = fs::File::create(bundle)?; let mut artifact = fs::File::open(artifact)?; - let context = SigningContext::production(); + let context = SigningContext::production()?; let signer = context.signer(identity_token); let signing_artifact = signer?.sign(&mut artifact)?; From 68fea4a2d9a4f1f13b6418d14920ce54b42a90b2 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Thu, 15 Feb 2024 18:16:17 -0600 Subject: [PATCH 09/27] `cargo fmt` Signed-off-by: Andrew Pan --- src/crypto/certificate.rs | 4 +--- src/verify/verifier.rs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/crypto/certificate.rs b/src/crypto/certificate.rs index 5a3334d0f6..0847c3dcc5 100644 --- a/src/crypto/certificate.rs +++ b/src/crypto/certificate.rs @@ -20,9 +20,7 @@ use x509_cert::{ Certificate, }; -use crate::{ - errors::{Result, SigstoreError}, -}; +use crate::errors::{Result, SigstoreError}; /// Ensure the given certificate can be trusted for verifying cosign /// signatures. diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index 9b6e4c3da4..62d07f0345 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -15,9 +15,7 @@ use std::cell::OnceCell; use tracing::debug; -use webpki::{ - types::{CertificateDer, UnixTime}, -}; +use webpki::types::{CertificateDer, UnixTime}; use x509_cert::der::Encode; From 752f87fc2688920ed2b81ddfcb95f9c8eda42914 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Fri, 16 Feb 2024 13:05:33 -0600 Subject: [PATCH 10/27] sign, verify: add TODOs for SCT verification Signed-off-by: Andrew Pan --- src/sign.rs | 2 ++ src/verify/verifier.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/sign.rs b/src/sign.rs index eac694f465..124d521591 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -128,6 +128,8 @@ impl<'ctx> AsyncSigningSession<'ctx> { return Err(SigstoreError::ExpiredSigningSession()); } + // TODO(tnytown): verify SCT here, sigstore-rs#326 + // Sign artifact. let input_hash: &[u8] = &hasher.clone().finalize(); let mut signature_bytes = Vec::new(); diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index 62d07f0345..b41173448c 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -103,6 +103,8 @@ impl<'a, R: Repository> Verifier<'a, R> { debug!("signing certificate chains back to trusted root"); + // TODO(tnytown): verify SCT here, sigstore-rs#326 + // 2) Verify that the signing certificate belongs to the signer. if let Some(err) = policy.verify(&materials.certificate) { return Err(err)?; From 5c6854a4560f32ffda60c96b5dffb70a9f538e72 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Fri, 23 Feb 2024 15:47:43 -0600 Subject: [PATCH 11/27] verify, crypto: asyncify, revamp error handling - remove lifetimes from CertificatePool & co - rework OnceCell usage - enumerate errors for `verify` and its dependencies Signed-off-by: Andrew Pan --- examples/cosign/verify/main.rs | 2 +- src/cosign/client.rs | 10 +- src/cosign/client_builder.rs | 2 +- src/cosign/mod.rs | 4 +- src/crypto/certificate.rs | 160 +++++++++++------ src/crypto/certificate_pool.rs | 35 ++-- src/tuf/mod.rs | 109 +++++------- src/verify/mod.rs | 4 +- src/verify/models.rs | 293 +++++++++++++++++-------------- src/verify/policy.rs | 46 ++--- src/verify/verifier.rs | 169 ++++++++++++------ tests/conformance/conformance.rs | 8 +- 12 files changed, 491 insertions(+), 351 deletions(-) diff --git a/examples/cosign/verify/main.rs b/examples/cosign/verify/main.rs index fc687ba627..ca3281bdd7 100644 --- a/examples/cosign/verify/main.rs +++ b/examples/cosign/verify/main.rs @@ -232,7 +232,7 @@ async fn fulcio_and_rekor_data(cli: &Cli) -> anyhow::Result = spawn_blocking(|| { info!("Downloading data from Sigstore TUF repository"); - SigstoreRepository::new(None)?.prefetch() + SigstoreRepository::new(None) }) .await .map_err(|e| anyhow!("Error spawning blocking task inside of tokio: {}", e))?; diff --git a/src/cosign/client.rs b/src/cosign/client.rs index af70bf457b..1c97e113af 100644 --- a/src/cosign/client.rs +++ b/src/cosign/client.rs @@ -37,15 +37,15 @@ pub const CONFIG_DATA: &str = "{}"; /// Cosign Client /// /// Instances of `Client` can be built via [`sigstore::cosign::ClientBuilder`](crate::cosign::ClientBuilder). -pub struct Client<'a> { +pub struct Client { pub(crate) registry_client: Box, pub(crate) rekor_pub_key: Option, - pub(crate) fulcio_cert_pool: Option>, + pub(crate) fulcio_cert_pool: Option, } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl CosignCapabilities for Client<'_> { +impl CosignCapabilities for Client { async fn triangulate( &mut self, image: &OciReference, @@ -140,7 +140,7 @@ impl CosignCapabilities for Client<'_> { } } -impl Client<'_> { +impl Client { /// Internal helper method used to fetch data from an OCI registry async fn fetch_manifest_and_layers( &mut self, @@ -177,7 +177,7 @@ mod tests { use crate::crypto::SigningScheme; use crate::mock_client::test::MockOciClient; - fn build_test_client(mock_client: MockOciClient) -> Client<'static> { + fn build_test_client(mock_client: MockOciClient) -> Client { let rekor_pub_key = CosignVerificationKey::from_pem(REKOR_PUB_KEY.as_bytes(), &SigningScheme::default()) .expect("Cannot create CosignVerificationKey"); diff --git a/src/cosign/client_builder.rs b/src/cosign/client_builder.rs index adfaff1f43..8c60cc6df7 100644 --- a/src/cosign/client_builder.rs +++ b/src/cosign/client_builder.rs @@ -91,7 +91,7 @@ impl<'a> ClientBuilder<'a> { self } - pub fn build(self) -> Result> { + pub fn build(self) -> Result { let rekor_pub_key = match self.rekor_pub_key { None => { info!("Rekor public key not provided. Rekor integration disabled"); diff --git a/src/cosign/mod.rs b/src/cosign/mod.rs index 03d3c0e52d..99e858cdb4 100644 --- a/src/cosign/mod.rs +++ b/src/cosign/mod.rs @@ -337,7 +337,7 @@ TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ #[cfg(feature = "test-registry")] const SIGNED_IMAGE: &str = "busybox:1.34"; - pub(crate) fn get_fulcio_cert_pool() -> CertificatePool<'static> { + pub(crate) fn get_fulcio_cert_pool() -> CertificatePool { fn pem_to_der<'a>(input: &'a str) -> CertificateDer<'a> { let pem_cert = pem::parse(input).unwrap(); assert_eq!(pem_cert.tag(), "CERTIFICATE"); @@ -644,7 +644,7 @@ TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ } #[cfg(feature = "test-registry")] - async fn prepare_image_to_be_signed(client: &mut Client<'_>, image_ref: &OciReference) { + async fn prepare_image_to_be_signed(client: &mut Client, image_ref: &OciReference) { let data = client .registry_client .pull( diff --git a/src/crypto/certificate.rs b/src/crypto/certificate.rs index 0847c3dcc5..9960b27c23 100644 --- a/src/crypto/certificate.rs +++ b/src/crypto/certificate.rs @@ -15,6 +15,7 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use const_oid::db::rfc5912::ID_KP_CODE_SIGNING; +use thiserror::Error; use x509_cert::{ ext::pkix::{constraints, ExtendedKeyUsage, KeyUsage, KeyUsages, SubjectAltName}, Certificate, @@ -120,6 +121,56 @@ fn verify_expiration(certificate: &Certificate, integrated_time: i64) -> Result< Ok(()) } +#[derive(Debug, Error)] +pub enum ExtensionErrorKind { + #[error("certificate missing extension: {0}")] + Missing(&'static str), + + #[error("certificate extension bit not asserted: {0}")] + BitUnset(&'static str), + + #[error("certificate's {0} extension not marked as critical")] + NotCritical(&'static str), +} + +#[derive(Debug, Error)] +pub enum NotLeafErrorKind { + #[error("certificate is a CA: CAs are not leaves")] + IsCA, + + #[error(transparent)] + Extension(#[from] ExtensionErrorKind), +} + +#[derive(Debug, Error)] +pub enum NotCAErrorKind { + #[error("certificate is not a CA: CAs must assert cA and keyCertSign")] + NotCA, + + #[error("certificate is not a root CA")] + NotRootCA, + + #[error("certificate in invalid state: cA={ca}, keyCertSign={key_cert_sign}")] + Invalid { ca: bool, key_cert_sign: bool }, + + #[error(transparent)] + Extension(#[from] ExtensionErrorKind), +} + +#[derive(Debug, Error)] +#[error(transparent)] +pub enum CertificateValidationError { + #[error("only X509 V3 certificates are supported")] + VersionUnsupported, + + #[error("malformed certificate")] + Malformed(#[source] x509_cert::der::Error), + + NotLeaf(#[from] NotLeafErrorKind), + + NotCA(#[from] NotCAErrorKind), +} + /// Check if the given certificate is a leaf in the context of the Sigstore profile. /// /// * It is not a root or intermediate CA; @@ -127,7 +178,9 @@ fn verify_expiration(certificate: &Certificate, integrated_time: i64) -> Result< /// * It has `CODE_SIGNING` as an `ExtendedKeyUsage`. /// /// This function does not evaluate the trustworthiness of the certificate. -pub(crate) fn is_leaf(certificate: &Certificate) -> Result<()> { +pub(crate) fn is_leaf( + certificate: &Certificate, +) -> core::result::Result<(), CertificateValidationError> { // NOTE(jl): following structure of sigstore-python over the slightly different handling found // in `verify_key_usages`. let tbs = &certificate.tbs_certificate; @@ -135,47 +188,47 @@ pub(crate) fn is_leaf(certificate: &Certificate) -> Result<()> { // Only V3 certificates should appear in the context of Sigstore; earlier versions of X.509 lack // extensions and have ambiguous CA behavior. if tbs.version != x509_cert::Version::V3 { - return Err(SigstoreError::CertificateUnsupportedVersionError); + Err(CertificateValidationError::VersionUnsupported)?; } - if is_ca(certificate)? { - return Err(SigstoreError::InvalidCertError( - "invalid signing cert: missing KeyUsage".to_string(), - )); + if is_ca(certificate).is_ok() { + Err(NotLeafErrorKind::IsCA)?; }; - let digital_signature = match tbs.get::()? { - None => { - return Err(SigstoreError::InvalidCertError( - "invalid signing cert: missing KeyUsage".to_string(), - )) - } + let digital_signature = match tbs + .get::() + .map_err(CertificateValidationError::Malformed)? + { + None => Err(NotLeafErrorKind::Extension(ExtensionErrorKind::Missing( + "KeyUsage", + )))?, Some((_, key_usage)) => key_usage.digital_signature(), }; if !digital_signature { - return Err(SigstoreError::InvalidCertError( - "invalid signing cert: missing digital signature usage".to_string(), - )); + Err(NotLeafErrorKind::Extension(ExtensionErrorKind::BitUnset( + "KeyUsage.digitalSignature", + )))?; } // Finally, we check to make sure the leaf has an `ExtendedKeyUsages` // extension that includes a codesigning entitlement. Sigstore should // never issue a leaf that doesn't have this extended usage. - let extended_key_usage = match tbs.get::()? { - None => { - return Err(SigstoreError::InvalidCertError( - "invalid signing cert: missing ExtendedKeyUsage".to_string(), - )) - } + let extended_key_usage = match tbs + .get::() + .map_err(CertificateValidationError::Malformed)? + { + None => Err(NotLeafErrorKind::Extension(ExtensionErrorKind::Missing( + "ExtendedKeyUsage", + )))?, Some((_, extended_key_usage)) => extended_key_usage, }; if !extended_key_usage.0.contains(&ID_KP_CODE_SIGNING) { - return Err(SigstoreError::InvalidCertError( - "invalid signing cert: missing CODE_SIGNING ExtendedKeyUsage".into(), - )); + Err(NotLeafErrorKind::Extension(ExtensionErrorKind::BitUnset( + "ExtendedKeyUsage.digitalSignature", + )))?; } Ok(()) @@ -188,13 +241,15 @@ pub(crate) fn is_leaf(certificate: &Certificate) -> Result<()> { /// /// This function is **not** naively invertible: users **must** use the dedicated `is_leaf` /// utility function to determine whether a particular leaf upholds Sigstore's invariants. -pub(crate) fn is_ca(certificate: &Certificate) -> Result { +pub(crate) fn is_ca( + certificate: &Certificate, +) -> core::result::Result<(), CertificateValidationError> { let tbs = &certificate.tbs_certificate; // Only V3 certificates should appear in the context of Sigstore; earlier versions of X.509 lack // extensions and have ambiguous CA behavior. if tbs.version != x509_cert::Version::V3 { - return Err(SigstoreError::CertificateUnsupportedVersionError); + return Err(CertificateValidationError::VersionUnsupported); } // Valid CA certificates must have the following set: @@ -205,40 +260,43 @@ pub(crate) fn is_ca(certificate: &Certificate) -> Result { // Any other combination of states is inconsistent and invalid, meaning // that we won't consider the certificate a valid non-CA leaf. - let ca = match tbs.get::()? { - None => return Ok(false), + let ca = match tbs + .get::() + .map_err(CertificateValidationError::Malformed)? + { + None => Err(NotCAErrorKind::Extension(ExtensionErrorKind::Missing( + "BasicConstraints", + )))?, Some((false, _)) => { // BasicConstraints must be marked as critical, per RFC 5280 4.2.1.9. - return Err(SigstoreError::InvalidCertError( - "invalid X.509 certificate: non-critical BasicConstraints in CA".to_string(), - )); + Err(NotCAErrorKind::Extension(ExtensionErrorKind::NotCritical( + "BasicConstraints", + )))? } Some((true, v)) => v.ca, }; - let key_cert_sign = match tbs.get::()? { - None => { - return Err(SigstoreError::InvalidCertError( - "invalid X.509 certificate: missing KeyUsage".to_string(), - )) - } + let key_cert_sign = match tbs + .get::() + .map_err(CertificateValidationError::Malformed)? + { + None => Err(NotCAErrorKind::Extension(ExtensionErrorKind::Missing( + "KeyUsage", + )))?, Some((_, v)) => v.key_cert_sign(), }; // both states set, this is a CA. if ca && key_cert_sign { - return Ok(true); + return Ok(()); } if !(ca || key_cert_sign) { - return Ok(false); + Err(NotCAErrorKind::NotCA)?; } // Anything else is an invalid state that should never occur. - Err(SigstoreError::InvalidCertError(format!( - "invalid certificate states: KeyUsage.keyCertSign={}, BasicConstraints.ca={}", - key_cert_sign, ca - ))) + Err(NotCAErrorKind::Invalid { ca, key_cert_sign })? } /// Returns `True` if and only if the given `Certificate` indicates @@ -246,7 +304,9 @@ pub(crate) fn is_ca(certificate: &Certificate) -> Result { /// /// This is **not** a verification function, and it does not establish /// the trustworthiness of the given certificate. -pub(crate) fn is_root_ca(certificate: &Certificate) -> Result { +pub(crate) fn is_root_ca( + certificate: &Certificate, +) -> core::result::Result<(), CertificateValidationError> { // NOTE(ww): This function is obnoxiously long to make the different // states explicit. @@ -255,16 +315,18 @@ pub(crate) fn is_root_ca(certificate: &Certificate) -> Result { // Only V3 certificates should appear in the context of Sigstore; earlier versions of X.509 lack // extensions and have ambiguous CA behavior. if tbs.version != x509_cert::Version::V3 { - return Err(SigstoreError::CertificateUnsupportedVersionError); + return Err(CertificateValidationError::VersionUnsupported); } // Non-CAs can't possibly be root CAs. - if !matches!(is_ca(certificate), Ok(true)) { - return Ok(false); - } + is_ca(certificate)?; // A certificate that is its own issuer and signer is considered a root CA. - Ok(tbs.issuer == tbs.subject) + if tbs.issuer == tbs.subject { + Ok(()) + } else { + Err(NotCAErrorKind::NotRootCA)? + } } #[cfg(test)] diff --git a/src/crypto/certificate_pool.rs b/src/crypto/certificate_pool.rs index cd067aff48..9502fdc208 100644 --- a/src/crypto/certificate_pool.rs +++ b/src/crypto/certificate_pool.rs @@ -19,31 +19,34 @@ use webpki::{ EndEntityCert, KeyUsage, VerifiedPath, }; -use crate::errors::{Result, SigstoreError}; +use crate::errors::{Result as SigstoreResult, SigstoreError}; /// A collection of trusted root certificates. #[derive(Default, Debug)] -pub(crate) struct CertificatePool<'a> { - trusted_roots: Vec>, - intermediates: Vec>, +pub(crate) struct CertificatePool { + trusted_roots: Vec>, + intermediates: Vec>, } -impl<'a> CertificatePool<'a> { +impl CertificatePool { /// Builds a `CertificatePool` instance using the provided list of [`Certificate`]. - pub(crate) fn from_certificates( + pub(crate) fn from_certificates<'r, 'i, R, I>( trusted_roots: R, untrusted_intermediates: I, - ) -> Result> + ) -> SigstoreResult where - R: IntoIterator>, - I: IntoIterator>, + R: IntoIterator>, + I: IntoIterator>, { Ok(CertificatePool { trusted_roots: trusted_roots .into_iter() .map(|x| Ok(webpki::anchor_from_trusted_cert(&x)?.to_owned())) .collect::, webpki::Error>>()?, - intermediates: untrusted_intermediates.into_iter().collect(), + intermediates: untrusted_intermediates + .into_iter() + .map(|i| i.into_owned()) + .collect(), }) } @@ -59,7 +62,7 @@ impl<'a> CertificatePool<'a> { &self, cert_pem: &[u8], verification_time: Option, - ) -> Result<()> { + ) -> SigstoreResult<()> { let cert_pem = pem::parse(cert_pem)?; if cert_pem.tag() != "CERTIFICATE" { return Err(SigstoreError::CertificatePoolError( @@ -82,7 +85,7 @@ impl<'a> CertificatePool<'a> { &self, der: &[u8], verification_time: Option, - ) -> Result<()> { + ) -> SigstoreResult<()> { let der = CertificateDer::from(der); let cert = EndEntityCert::try_from(&der)?; @@ -91,18 +94,18 @@ impl<'a> CertificatePool<'a> { Ok(()) } - pub(crate) fn verify_cert_with_time<'cert>( + pub(crate) fn verify_cert_with_time<'a, 'cert>( &'a self, cert: &'cert EndEntityCert<'cert>, verification_time: UnixTime, - ) -> Result> + ) -> Result, webpki::Error> where 'a: 'cert, { let signing_algs = webpki::ALL_VERIFICATION_ALGS; let eku_code_signing = ID_KP_CODE_SIGNING.as_bytes(); - Ok(cert.verify_for_usage( + cert.verify_for_usage( signing_algs, &self.trusted_roots, self.intermediates.as_slice(), @@ -110,6 +113,6 @@ impl<'a> CertificatePool<'a> { KeyUsage::required(eku_code_signing), None, None, - )?) + ) } } diff --git a/src/tuf/mod.rs b/src/tuf/mod.rs index 6fd4e2cd9f..31818df732 100644 --- a/src/tuf/mod.rs +++ b/src/tuf/mod.rs @@ -23,15 +23,14 @@ //! //! # Example //! -//! The `SigstoreRepository` instance can be created via the [`SigstoreRepository::prefetch`] +//! The `SigstoreRepository` instance can be created via the [`SigstoreRepository::new`] //! method. //! //! ```rust,no_run //! use sigstore::tuf::SigstoreRepository; -//! let repo = SigstoreRepository::new(None).unwrap().prefetch().unwrap(); +//! let repo = SigstoreRepository::new(None).unwrap(); //! ``` use std::{ - cell::OnceCell, io::Read, path::{Path, PathBuf}, }; @@ -51,7 +50,7 @@ use super::errors::{Result, SigstoreError}; /// A `Repository` owns all key material necessary for establishing a root of trust. pub trait Repository { - fn fulcio_certs(&self) -> Result>; + fn fulcio_certs(&self) -> Result>>; fn rekor_keys(&self) -> Result>; fn ctfe_keys(&self) -> Result>; } @@ -59,14 +58,14 @@ pub trait Repository { /// A `ManualRepository` is a [Repository] with out-of-band trust materials. /// As it does not establish a trust root with TUF, users must initialize its materials themselves. #[derive(Debug, Default)] -pub struct ManualRepository<'a> { - pub fulcio_certs: Option>>, +pub struct ManualRepository { + pub fulcio_certs: Option>>, pub rekor_key: Option>, pub ctfe_keys: Option>>, } -impl Repository for ManualRepository<'_> { - fn fulcio_certs(&self) -> Result> { +impl Repository for ManualRepository { + fn fulcio_certs(&self) -> Result>> { Ok(match &self.fulcio_certs { Some(certs) => certs.clone(), None => Vec::new(), @@ -91,20 +90,33 @@ impl Repository for ManualRepository<'_> { /// Securely fetches Rekor public key and Fulcio certificates from Sigstore's TUF repository. #[derive(Debug)] pub struct SigstoreRepository { - repository: tough::Repository, - checkout_dir: Option, - trusted_root: OnceCell, + trusted_root: TrustedRoot, } impl SigstoreRepository { /// Constructs a new trust repository established by a [tough::Repository]. + /// + /// This method synchronously fetches trust materials, from TUF, which is problematic for async + /// callers. Those callers should do the following to fetch the trust root ahead of time. + /// + /// ```rust + /// # use tokio::task::spawn_blocking; + /// # use sigstore::tuf::SigstoreRepository; + /// # use sigstore::errors::Result; + /// # #[tokio::main] + /// # async fn main() -> std::result::Result<(), anyhow::Error> { + /// let repo: Result = spawn_blocking(|| Ok(SigstoreRepository::new(None)?)).await?; + /// // Now, get Fulcio and Rekor trust roots with the returned `SigstoreRepository` + /// # Ok(()) + /// # } + /// ``` pub fn new(checkout_dir: Option<&Path>) -> Result { // These are statically defined and should always parse correctly. let metadata_base = url::Url::parse(constants::SIGSTORE_METADATA_BASE)?; let target_base = url::Url::parse(constants::SIGSTORE_TARGET_BASE)?; let repository = tough::RepositoryLoader::new( - constants::static_resource("root.json").expect("Failed to fetch required resource!"), + constants::static_resource("root.json").expect("Failed to fetch embedded TUF root!"), metadata_base, target_base, ) @@ -112,33 +124,26 @@ impl SigstoreRepository { .load() .map_err(Box::new)?; - Ok(Self { - repository, - checkout_dir: checkout_dir.map(ToOwned::to_owned), - trusted_root: OnceCell::default(), - }) - } - - fn trusted_root(&self) -> Result<&TrustedRoot> { - return if let Some(root) = self.trusted_root.get() { - Ok(root) - } else { - let data = self.fetch_target("trusted_root.json")?; - - debug!("data:\n{}", String::from_utf8_lossy(&data)); + let checkout_dir = checkout_dir.map(ToOwned::to_owned); - let root = serde_json::from_slice(&data[..])?; - - Ok(self.trusted_root.get_or_init(|| root)) + let trusted_root = { + let data = Self::fetch_target(&repository, &checkout_dir, "trusted_root.json")?; + serde_json::from_slice(&data[..])? }; + + Ok(Self { trusted_root }) } - fn fetch_target(&self, name: N) -> Result> + fn fetch_target( + repository: &tough::Repository, + checkout_dir: &Option, + name: N, + ) -> Result> where N: TryInto, { let read_remote_target = |name: &TargetName| -> Result> { - let Some(mut reader) = self.repository.read_target(name).map_err(Box::new)? else { + let Some(mut reader) = repository.read_target(name).map_err(Box::new)? else { return Err(SigstoreError::TufTargetNotFoundError(name.raw().to_owned())); }; @@ -150,7 +155,7 @@ impl SigstoreRepository { }; let name: TargetName = name.try_into().map_err(Box::new)?; - let local_path = self.checkout_dir.as_ref().map(|d| d.join(name.raw())); + let local_path = checkout_dir.as_ref().map(|d| d.join(name.raw())); // Try reading the target from disk cache. let data = if let Some(Ok(local_data)) = local_path.as_ref().map(std::fs::read) { @@ -167,7 +172,7 @@ impl SigstoreRepository { }; // Get metadata (hash) of the target and update the disk copy if it doesn't match. - let Some(target) = self.repository.targets().signed.targets.get(&name) else { + let Some(target) = repository.targets().signed.targets.get(&name) else { return Err(SigstoreError::TufMetadataError(format!( "couldn't get metadata for {}", name.raw() @@ -189,28 +194,6 @@ impl SigstoreRepository { Ok(data) } - /// Prefetches trust materials. - /// - /// [Repository::fulcio_certs()] and [Repository::rekor_keys()] on [SigstoreRepository] lazily - /// fetches the requested data, which is problematic for async callers. Those callers should - /// use this method to fetch the trust root ahead of time. - /// - /// ```rust - /// # use tokio::task::spawn_blocking; - /// # use sigstore::tuf::SigstoreRepository; - /// # use sigstore::errors::Result; - /// # #[tokio::main] - /// # async fn main() -> std::result::Result<(), anyhow::Error> { - /// let repo: Result = spawn_blocking(|| Ok(SigstoreRepository::new(None)?.prefetch()?)).await?; - /// // Now, get Fulcio and Rekor trust roots with the returned `SigstoreRepository` - /// # Ok(()) - /// # } - /// ``` - pub fn prefetch(self) -> Result { - let _ = self.trusted_root()?; - Ok(self) - } - #[inline] fn tlog_keys(tlogs: &[TransparencyLogInstance]) -> impl Iterator { tlogs @@ -242,13 +225,13 @@ impl Repository for SigstoreRepository { /// /// **Warning:** this method needs special handling when invoked from /// an async function because it performs blocking operations. - fn fulcio_certs(&self) -> Result> { - let root = self.trusted_root()?; - + fn fulcio_certs(&self) -> Result>> { // Allow expired certificates: they may have been active when the // certificate was used to sign. - let certs = Self::ca_keys(&root.certificate_authorities, true); - let certs: Vec<_> = certs.map(CertificateDer::from).collect(); + let certs = Self::ca_keys(&self.trusted_root.certificate_authorities, true); + let certs: Vec<_> = certs + .map(|c| CertificateDer::from(c).into_owned()) + .collect(); if certs.is_empty() { Err(SigstoreError::TufMetadataError( @@ -267,8 +250,7 @@ impl Repository for SigstoreRepository { /// **Warning:** this method needs special handling when invoked from /// an async function because it performs blocking operations. fn rekor_keys(&self) -> Result> { - let root = self.trusted_root()?; - let keys: Vec<_> = Self::tlog_keys(&root.tlogs).collect(); + let keys: Vec<_> = Self::tlog_keys(&self.trusted_root.tlogs).collect(); if keys.len() != 1 { Err(SigstoreError::TufMetadataError( @@ -287,8 +269,7 @@ impl Repository for SigstoreRepository { /// **Warning:** this method needs special handling when invoked from /// an async function because it performs blocking operations. fn ctfe_keys(&self) -> Result> { - let root = self.trusted_root()?; - let keys: Vec<_> = Self::tlog_keys(&root.ctlogs).collect(); + let keys: Vec<_> = Self::tlog_keys(&self.trusted_root.ctlogs).collect(); if keys.is_empty() { Err(SigstoreError::TufMetadataError( diff --git a/src/verify/mod.rs b/src/verify/mod.rs index 95e3375ed3..0504d10060 100644 --- a/src/verify/mod.rs +++ b/src/verify/mod.rs @@ -15,10 +15,10 @@ //! Verifier for Sigstore bundles and associated types and policies. mod models; -pub use models::{VerificationError, VerificationMaterials, VerificationResult}; +pub use models::{VerificationError, VerificationResult}; pub mod policy; pub use policy::{PolicyError, VerificationPolicy}; mod verifier; -pub use verifier::Verifier; +pub use verifier::{AsyncVerifier, Verifier}; diff --git a/src/verify/models.rs b/src/verify/models.rs index 8d22497995..5a6777fca4 100644 --- a/src/verify/models.rs +++ b/src/verify/models.rs @@ -13,106 +13,143 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{ - io::{self, Read}, - str::FromStr, -}; +use std::str::FromStr; use crate::{ bundle::Version as BundleVersion, - crypto::certificate::{is_leaf, is_root_ca}, + crypto::certificate::{is_leaf, is_root_ca, CertificateValidationError}, rekor::models as rekor, }; use crate::Bundle; use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; -use pkcs8::der::{Decode, EncodePem}; -use sha2::{Digest, Sha256}; use sigstore_protobuf_specs::dev::sigstore::{ bundle::v1::{bundle, verification_material}, rekor::v1::{InclusionProof, TransparencyLogEntry}, }; use thiserror::Error; use tracing::{debug, error, warn}; -use x509_cert::Certificate; +use x509_cert::{ + der::{Decode, EncodePem}, + Certificate, +}; use super::policy::PolicyError; #[derive(Error, Debug)] -pub enum VerificationError { - #[error("Certificate expired before time of signing")] - CertificateExpired, +pub enum Bundle01ProfileErrorKind { + #[error("bundle must contain inclusion promise")] + InclusionPromiseMissing, +} + +#[derive(Error, Debug)] +pub enum Bundle02ProfileErrorKind { + #[error("bundle must contain inclusion proof")] + InclusionProofMissing, + + #[error("bundle must contain checkpoint")] + CheckpointMissing, +} + +#[derive(Error, Debug)] +#[error(transparent)] +pub enum BundleProfileErrorKind { + Bundle01Profile(#[from] Bundle01ProfileErrorKind), + + Bundle02Profile(#[from] Bundle02ProfileErrorKind), + + #[error("unknown bundle profile {0}")] + Unknown(String), +} + +#[derive(Error, Debug)] +pub enum BundleErrorKind { + #[error("bundle missing VerificationMaterial")] + VerificationMaterialMissing, + + #[error("bundle includes unsupported VerificationMaterial::Content")] + VerificationMaterialContentUnsupported, - #[error("Certificate malformed")] - CertificateMalformed, + #[error("bundle's certificate(s) are malformed")] + CertificateMalformed(#[source] x509_cert::der::Error), - #[error("Failed to verify certificate")] - CertificateVerificationFailure, + #[error("bundle contains a root certificate")] + RootInChain, - #[error("Certificate cannot be used for verification: {0}")] - CertificateTypeError(String), + #[error("bundle does not contain the signing (leaf) certificate")] + NoLeaf(#[source] CertificateValidationError), - #[error("Failed to verify that the signature corresponds to the input")] - SignatureVerificationFailure, + #[error("bundle does not contain any certificates")] + CertificatesMissing, + + #[error("bundle does not contain signature")] + SignatureMissing, + + #[error("bundle includes unsupported DSSE signature")] + DsseUnsupported, + + #[error("bundle needs 1 tlog entry, got {0}")] + TlogEntry(usize), #[error(transparent)] - PolicyFailure(#[from] PolicyError), + BundleProfile(#[from] BundleProfileErrorKind), +} + +#[derive(Error, Debug)] +pub enum CertificateErrorKind { + #[error("certificate malformed")] + Malformed(#[source] webpki::Error), + + #[error("certificate expired before time of signing")] + Expired, + + #[error("certificate verification failed")] + VerificationFailed(#[source] webpki::Error), +} + +#[derive(Error, Debug)] +pub enum SignatureErrorKind { + #[error("unsupported signature algorithm")] + AlgoUnsupported(#[source] crate::errors::SigstoreError), + + #[error("signature verification failed")] + VerificationFailed(#[source] crate::errors::SigstoreError), + + #[error("signature transparency materials are inconsistent")] + Transparency, +} + +#[derive(Error, Debug)] +#[error(transparent)] +pub enum VerificationError { + #[error("unable to read input")] + Input(#[source] std::io::Error), + + Bundle(#[from] BundleErrorKind), + + Certificate(#[from] CertificateErrorKind), + + Signature(#[from] SignatureErrorKind), + + Policy(#[from] PolicyError), } + pub type VerificationResult = Result<(), VerificationError>; -pub struct VerificationMaterials { - pub(crate) input_digest: Vec, +pub struct CheckedBundle { pub(crate) certificate: Certificate, pub(crate) signature: Vec, - rekor_entry: TransparencyLogEntry, - offline: bool, + tlog_entry: TransparencyLogEntry, } -impl VerificationMaterials { - pub fn new( - input: &mut R, - certificate: Certificate, - signature: Vec, - offline: bool, - rekor_entry: TransparencyLogEntry, - ) -> Option { - let mut hasher = Sha256::new(); - io::copy(input, &mut hasher).ok()?; - - if matches!( - rekor_entry, - TransparencyLogEntry { - inclusion_promise: None, - inclusion_proof: None, - .. - } - ) { - error!("encountered TransparencyLogEntry without any inclusion materials"); - return None; - } - - Some(Self { - input_digest: hasher.finalize().to_vec(), - rekor_entry, - certificate, - signature, - offline, - }) - } +impl TryFrom for CheckedBundle { + type Error = BundleErrorKind; - /// Constructs a VerificationMaterials from the given Bundle. - /// - /// For details on bundle semantics, please refer to [VerificationMaterial]. - /// - /// [VerificationMaterial]: sigstore_protobuf_specs::dev::sigstore::bundle::v1::VerificationMaterial - pub fn from_bundle(input: &mut R, bundle: Bundle, offline: bool) -> Option { - let (content, mut tlog_entries) = match bundle.verification_material { + fn try_from(input: Bundle) -> Result { + let (content, mut tlog_entries) = match input.verification_material { Some(m) => (m.content, m.tlog_entries), - _ => { - error!("bundle missing VerificationMaterial"); - return None; - } + _ => return Err(BundleErrorKind::VerificationMaterialMissing), }; // Parse the certificates. The first entry in the chain MUST be a leaf certificate, and the @@ -123,98 +160,98 @@ impl VerificationMaterials { Some(verification_material::Content::Certificate(cert)) => { vec![cert] } - _ => { - error!("bundle includes unsupported VerificationMaterial Content"); - return None; - } + _ => return Err(BundleErrorKind::VerificationMaterialContentUnsupported), }; let certs = certs .iter() .map(|c| c.raw_bytes.as_slice()) .map(Certificate::from_der) .collect::, _>>() - .ok()?; + .map_err(BundleErrorKind::CertificateMalformed)?; let [leaf_cert, chain_certs @ ..] = &certs[..] else { - return None; + return Err(BundleErrorKind::CertificatesMissing); }; - if is_leaf(leaf_cert).is_err() { - return None; - } + is_leaf(leaf_cert).map_err(BundleErrorKind::NoLeaf)?; for chain_cert in chain_certs { - if matches!(is_root_ca(chain_cert), Ok(true)) { - return None; + if is_root_ca(chain_cert).is_ok() { + return Err(BundleErrorKind::RootInChain); } } - let signature = match bundle.content? { + let signature = match input.content.ok_or(BundleErrorKind::SignatureMissing)? { bundle::Content::MessageSignature(s) => s.signature, - _ => { - error!("bundle includes unsupported DSSE signature"); - return None; - } + _ => return Err(BundleErrorKind::DsseUnsupported), }; if tlog_entries.len() != 1 { - error!("bundle expected 1 tlog entry; got {}", tlog_entries.len()); - return None; + return Err(BundleErrorKind::TlogEntry(tlog_entries.len())); } let tlog_entry = tlog_entries.remove(0); let (inclusion_promise, inclusion_proof) = (&tlog_entry.inclusion_promise, &tlog_entry.inclusion_proof); - // `inclusion_proof` is now a required field in the protobuf spec, - // but older versions of Rekor didn't provide inclusion proofs. + // `inclusion_proof` is a required field in the current protobuf spec, + // but older versions of Rekor didn't provide it. Check invariants + // here and selectively allow for this case. // // https://github.com/sigstore/sigstore-python/pull/634#discussion_r1182769140 - match BundleVersion::from_str(&bundle.media_type) { - Ok(BundleVersion::Bundle0_1) => { - if inclusion_promise.is_none() { - error!("bundle must contain inclusion promise"); - return None; - } - - if matches!( - inclusion_proof, - Some(InclusionProof { - checkpoint: None, - .. - }) - ) { - debug!("0.1 bundle contains inclusion proof without checkpoint"); - } + let check_01_bundle = || -> Result<(), BundleProfileErrorKind> { + if inclusion_promise.is_none() { + return Err(Bundle01ProfileErrorKind::InclusionPromiseMissing)?; } - Ok(BundleVersion::Bundle0_2) => { - if inclusion_proof.is_none() { - error!("bundle must contain inclusion proof"); - return None; - } - - if matches!( - inclusion_proof, - Some(InclusionProof { - checkpoint: None, - .. - }) - ) { - error!("bundle must contain checkpoint"); - return None; - } + + if matches!( + inclusion_proof, + Some(InclusionProof { + checkpoint: None, + .. + }) + ) { + debug!("0.1 bundle contains inclusion proof without checkpoint"); } - Err(_) => { - error!("unknown bundle version"); - return None; + + Ok(()) + }; + let check_02_bundle = || -> Result<(), BundleProfileErrorKind> { + if inclusion_proof.is_none() { + error!("bundle must contain inclusion proof"); + return Err(Bundle02ProfileErrorKind::InclusionProofMissing)?; } + + if matches!( + inclusion_proof, + Some(InclusionProof { + checkpoint: None, + .. + }) + ) { + error!("bundle must contain checkpoint"); + return Err(Bundle02ProfileErrorKind::CheckpointMissing)?; + } + + Ok(()) + }; + match BundleVersion::from_str(&input.media_type) { + Ok(BundleVersion::Bundle0_1) => check_01_bundle()?, + Ok(BundleVersion::Bundle0_2) => check_02_bundle()?, + Err(_) => return Err(BundleProfileErrorKind::Unknown(input.media_type))?, } - Self::new(input, leaf_cert.clone(), signature, offline, tlog_entry) + Ok(Self { + certificate: leaf_cert.clone(), + signature, + tlog_entry, + }) } +} - /// Retrieves the [LogEntry] for the materials. - pub fn rekor_entry(&self) -> Option<&TransparencyLogEntry> { +impl CheckedBundle { + /// Retrieves and checks consistency of the bundle's [TransparencyLogEntry]. + pub fn tlog_entry(&self, offline: bool, input_digest: &[u8]) -> Option<&TransparencyLogEntry> { let base64_pem_certificate = base64.encode(self.certificate.to_pem(pkcs8::LineEnding::LF).ok()?); @@ -229,21 +266,21 @@ impl VerificationMaterials { data: rekor::hashedrekord::Data { hash: rekor::hashedrekord::Hash { algorithm: rekor::hashedrekord::AlgorithmKind::sha256, - value: hex::encode(&self.input_digest), + value: hex::encode(input_digest), }, }, }, }; - let entry = if !self.offline && self.rekor_entry.inclusion_proof.is_none() { + let entry = if !offline && self.tlog_entry.inclusion_proof.is_none() { warn!("online rekor fetching is not implemented yet, but is necessary for this bundle"); return None; } else { - &self.rekor_entry + &self.tlog_entry }; let actual: serde_json::Value = - serde_json::from_slice(&self.rekor_entry.canonicalized_body).ok()?; + serde_json::from_slice(&self.tlog_entry.canonicalized_body).ok()?; let expected: serde_json::Value = serde_json::to_value(expected_entry).ok()?; if actual != expected { diff --git a/src/verify/policy.rs b/src/verify/policy.rs index ec6f0409f3..16befbc32d 100644 --- a/src/verify/policy.rs +++ b/src/verify/policy.rs @@ -88,6 +88,8 @@ pub enum PolicyError { AnyOf { total: usize }, } +pub type PolicyResult = Result<(), PolicyError>; + /// A policy that checks a single textual value against a X.509 extension. pub trait SingleX509ExtPolicy { fn new>(val: S) -> Self; @@ -96,13 +98,13 @@ pub trait SingleX509ExtPolicy { } impl VerificationPolicy for T { - fn verify(&self, cert: &x509_cert::Certificate) -> Option { + fn verify(&self, cert: &x509_cert::Certificate) -> PolicyResult { let extensions = cert.tbs_certificate.extensions.as_deref().unwrap_or(&[]); let mut extensions = extensions.iter().filter(|ext| ext.extn_id == T::OID); // Check for exactly one extension. let (Some(ext), None) = (extensions.next(), extensions.next()) else { - return Some(PolicyError::ExtensionNotFound); + return Err(PolicyError::ExtensionNotFound); }; // Parse raw string without DER encoding. @@ -110,13 +112,13 @@ impl VerificationPolicy for T .expect("failed to parse constructed Extension!"); if val != self.value() { - Some(PolicyError::ExtensionCheckFailed { + Err(PolicyError::ExtensionCheckFailed { extension: T::name().to_owned(), expected: self.value().to_owned(), actual: val.to_owned(), }) } else { - None + Ok(()) } } } @@ -159,7 +161,7 @@ impl_policy!( /// An interface that all policies must conform to. pub trait VerificationPolicy { - fn verify(&self, cert: &x509_cert::Certificate) -> Option; + fn verify(&self, cert: &x509_cert::Certificate) -> PolicyResult; } /// The "any of" policy, corresponding to a logical OR between child policies. @@ -178,12 +180,14 @@ impl<'a> AnyOf<'a> { } impl VerificationPolicy for AnyOf<'_> { - fn verify(&self, cert: &x509_cert::Certificate) -> Option { + fn verify(&self, cert: &x509_cert::Certificate) -> PolicyResult { self.children .iter() - .find(|policy| policy.verify(cert).is_some()) - .map(|_| PolicyError::AnyOf { - total: self.children.len(), + .find(|policy| policy.verify(cert).is_err()) + .map_or(Ok(()), |_| { + Err(PolicyError::AnyOf { + total: self.children.len(), + }) }) } } @@ -212,14 +216,14 @@ impl<'a> AllOf<'a> { } impl VerificationPolicy for AllOf<'_> { - fn verify(&self, cert: &x509_cert::Certificate) -> Option { - let results = self.children.iter().map(|policy| policy.verify(cert)); + fn verify(&self, cert: &x509_cert::Certificate) -> PolicyResult { + let results = self.children.iter().map(|policy| policy.verify(cert).err()); let failures: Vec<_> = results.flatten().collect(); if failures.is_empty() { - None + Ok(()) } else { - Some(PolicyError::AllOf { + Err(PolicyError::AllOf { total: self.children.len(), errors: failures, }) @@ -230,9 +234,9 @@ impl VerificationPolicy for AllOf<'_> { pub(crate) struct UnsafeNoOp; impl VerificationPolicy for UnsafeNoOp { - fn verify(&self, _cert: &x509_cert::Certificate) -> Option { + fn verify(&self, _cert: &x509_cert::Certificate) -> PolicyResult { warn!("unsafe (no-op) verification policy used! no verification performed!"); - None + Ok(()) } } @@ -260,14 +264,12 @@ impl Identity { } impl VerificationPolicy for Identity { - fn verify(&self, cert: &x509_cert::Certificate) -> Option { - if let err @ Some(_) = self.issuer.verify(cert) { - return err; - } + fn verify(&self, cert: &x509_cert::Certificate) -> PolicyResult { + self.issuer.verify(cert)?; let (_, san): (bool, SubjectAltName) = match cert.tbs_certificate.get() { Ok(Some(result)) => result, - _ => return Some(PolicyError::ExtensionNotFound), + _ => return Err(PolicyError::ExtensionNotFound), }; let names: Vec<_> = san @@ -284,9 +286,9 @@ impl VerificationPolicy for Identity { .collect(); if names.contains(&self.identity.as_str()) { - None + Ok(()) } else { - Some(PolicyError::ExtensionCheckFailed { + Err(PolicyError::ExtensionCheckFailed { extension: "SubjectAltName".to_owned(), expected: self.identity.clone(), actual: names.join(", "), diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index b41173448c..f542cb029e 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -12,57 +12,56 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::cell::OnceCell; +use std::io::{self, Read}; +use sha2::{Digest, Sha256}; +use tokio::io::{AsyncRead, AsyncReadExt}; use tracing::debug; use webpki::types::{CertificateDer, UnixTime}; - use x509_cert::der::Encode; use crate::{ + bundle::Bundle, crypto::{CertificatePool, CosignVerificationKey, Signature}, errors::Result as SigstoreResult, rekor::apis::configuration::Configuration as RekorConfiguration, tuf::{Repository, SigstoreRepository}, - verify::VerificationError, + verify::{ + models::{CertificateErrorKind, SignatureErrorKind}, + VerificationError, + }, }; -use super::{models::VerificationMaterials, policy::VerificationPolicy, VerificationResult}; +use super::{models::CheckedBundle, policy::VerificationPolicy, VerificationResult}; -pub struct Verifier<'a, R: Repository> { +pub struct AsyncVerifier { #[allow(dead_code)] rekor_config: RekorConfiguration, - trust_repo: R, - cert_pool: OnceCell>, + cert_pool: CertificatePool, } -impl<'a, R: Repository> Verifier<'a, R> { - pub fn new(rekor_config: RekorConfiguration, trust_repo: R) -> SigstoreResult { +impl AsyncVerifier { + pub fn new( + rekor_config: RekorConfiguration, + trust_repo: R, + ) -> SigstoreResult { + let cert_pool = CertificatePool::from_certificates(trust_repo.fulcio_certs()?, [])?; + Ok(Self { rekor_config, - cert_pool: Default::default(), - trust_repo, + cert_pool, }) } - fn cert_pool(&'a self) -> SigstoreResult<&CertificatePool<'a>> { - let init_cert_pool = || { - let certs = self.trust_repo.fulcio_certs()?; - CertificatePool::from_certificates(certs, []) - }; - - let cert_pool = init_cert_pool()?; - Ok(self.cert_pool.get_or_init(|| cert_pool)) - } - - pub fn verify( - &'a self, - materials: VerificationMaterials, + async fn verify_digest( + &self, + input_digest: Sha256, + bundle: Bundle, policy: &impl VerificationPolicy, + offline: bool, ) -> VerificationResult { - let store = self - .cert_pool() - .expect("Failed to construct certificate pool"); + let input_digest = input_digest.finalize(); + let materials: CheckedBundle = bundle.try_into()?; // In order to verify an artifact, we need to achieve the following: // @@ -91,46 +90,38 @@ impl<'a, R: Repository> Verifier<'a, R> { .to_der() .expect("failed to DER-encode constructed Certificate!") .into(); - let Ok(ee_cert) = (&cert_der).try_into() else { - return Err(VerificationError::CertificateVerificationFailure); - }; + let ee_cert = (&cert_der) + .try_into() + .map_err(CertificateErrorKind::Malformed)?; - let Ok(_trusted_chain) = - store.verify_cert_with_time(&ee_cert, UnixTime::since_unix_epoch(issued_at)) - else { - return Err(VerificationError::CertificateVerificationFailure); - }; + let _trusted_chain = self + .cert_pool + .verify_cert_with_time(&ee_cert, UnixTime::since_unix_epoch(issued_at)) + .map_err(CertificateErrorKind::VerificationFailed)?; debug!("signing certificate chains back to trusted root"); // TODO(tnytown): verify SCT here, sigstore-rs#326 // 2) Verify that the signing certificate belongs to the signer. - if let Some(err) = policy.verify(&materials.certificate) { - return Err(err)?; - } + policy.verify(&materials.certificate)?; debug!("signing certificate conforms to policy"); // 3) Verify that the signature was signed by the public key in the signing certificate - let Ok(signing_key): SigstoreResult = - (&tbs_certificate.subject_public_key_info).try_into() - else { - return Err(VerificationError::CertificateMalformed); - }; + let signing_key: CosignVerificationKey = (&tbs_certificate.subject_public_key_info) + .try_into() + .map_err(SignatureErrorKind::AlgoUnsupported)?; + + let verify_sig = + signing_key.verify_prehash(Signature::Raw(&materials.signature), &input_digest); + verify_sig.map_err(SignatureErrorKind::VerificationFailed)?; - let verify_sig = signing_key.verify_prehash( - Signature::Raw(&materials.signature), - &materials.input_digest, - ); - if verify_sig.is_err() { - return Err(VerificationError::SignatureVerificationFailure); - } debug!("signature corresponds to public key"); // 4) Verify that the Rekor entry is consistent with the other signing // materials - let Some(log_entry) = materials.rekor_entry() else { - return Err(VerificationError::CertificateMalformed); + let Some(log_entry) = materials.tlog_entry(offline, &input_digest) else { + return Err(SignatureErrorKind::Transparency)?; }; debug!("log entry is consistent with other materials"); @@ -156,19 +147,85 @@ impl<'a, R: Repository> Verifier<'a, R> { .to_unix_duration() .as_secs(); if !(not_before <= integrated_time && integrated_time <= not_after) { - return Err(VerificationError::CertificateExpired); + return Err(CertificateErrorKind::Expired)?; } debug!("data signed during validity period"); debug!("successfully verified!"); Ok(()) } + + pub async fn verify( + &self, + mut input: R, + bundle: Bundle, + policy: &impl VerificationPolicy, + offline: bool, + ) -> VerificationResult { + // arbitrary buffer size, chosen to be a multiple of the digest size. + let mut buf = [0u8; 1024]; + let mut hasher = Sha256::new(); + + loop { + match input + .read(&mut buf) + .await + .map_err(VerificationError::Input)? + { + 0 => break, + n => hasher.update(&buf[..n]), + } + } + + self.verify_digest(hasher, bundle, policy, offline).await + } +} + +impl AsyncVerifier { + pub fn production() -> SigstoreResult { + let updater = SigstoreRepository::new(None)?; + + AsyncVerifier::new(Default::default(), updater) + } +} + +pub struct Verifier { + inner: AsyncVerifier, + rt: tokio::runtime::Runtime, +} + +impl Verifier { + pub fn new( + rekor_config: RekorConfiguration, + trust_repo: R, + ) -> SigstoreResult { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let inner = AsyncVerifier::new(rekor_config, trust_repo)?; + + Ok(Self { rt, inner }) + } + + pub fn verify( + &self, + mut input: R, + bundle: Bundle, + policy: &impl VerificationPolicy, + offline: bool, + ) -> VerificationResult { + let mut hasher = Sha256::new(); + io::copy(&mut input, &mut hasher).map_err(VerificationError::Input)?; + + self.rt + .block_on(self.inner.verify_digest(hasher, bundle, policy, offline)) + } } -impl<'a> Verifier<'a, SigstoreRepository> { - pub fn production() -> SigstoreResult> { +impl Verifier { + pub fn production() -> SigstoreResult { let updater = SigstoreRepository::new(None)?; - Verifier::<'a, SigstoreRepository>::new(Default::default(), updater) + Verifier::new(Default::default(), updater) } } diff --git a/tests/conformance/conformance.rs b/tests/conformance/conformance.rs index 771c0091bb..200d6c8545 100644 --- a/tests/conformance/conformance.rs +++ b/tests/conformance/conformance.rs @@ -23,7 +23,6 @@ use clap::{Parser, Subcommand}; use sigstore::{ oauth::IdentityToken, sign::SigningContext, - verify::VerificationMaterials, verify::{policy, Verifier}, }; @@ -162,14 +161,13 @@ fn verify_bundle(args: VerifyBundle) -> anyhow::Result<()> { let mut artifact = fs::File::open(artifact)?; let bundle: sigstore::Bundle = serde_json::from_reader(bundle)?; - let materials = VerificationMaterials::from_bundle(&mut artifact, bundle, true) - .ok_or(anyhow!("Unable to construct VerificationMaterials"))?; - let verifier = Verifier::production()?; verifier.verify( - materials, + &mut artifact, + bundle, &policy::Identity::new(certificate_identity, certificate_oidc_issuer), + true, )?; Ok(()) From d9fff7367302989beccf2c5def99a4efa19c57db Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Fri, 23 Feb 2024 16:06:52 -0600 Subject: [PATCH 12/27] verify: relax `'static` bound on async `verify` Signed-off-by: Andrew Pan --- src/verify/verifier.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index f542cb029e..42d37ff255 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -155,7 +155,7 @@ impl AsyncVerifier { Ok(()) } - pub async fn verify( + pub async fn verify( &self, mut input: R, bundle: Bundle, From 8a80a367748207a1ecedfc5dac23f97011a3fbb2 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Fri, 23 Feb 2024 16:22:20 -0600 Subject: [PATCH 13/27] sign, crypto: review comments Signed-off-by: Andrew Pan --- src/crypto/verification_key.rs | 4 +++- src/sign.rs | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/crypto/verification_key.rs b/src/crypto/verification_key.rs index f6881f2574..ea13e10024 100644 --- a/src/crypto/verification_key.rs +++ b/src/crypto/verification_key.rs @@ -388,7 +388,9 @@ impl CosignVerificationKey { .verify_prehash(msg, &sig) .map_err(|_| SigstoreError::PublicKeyVerificationError) } - _ => unimplemented!("Ed25519 doesn't implement verify_prehash"), + CosignVerificationKey::ED25519(_) => { + unimplemented!("Ed25519 doesn't implement verify_prehash") + } } } } diff --git a/src/sign.rs b/src/sign.rs index 124d521591..30f75737c7 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -132,12 +132,8 @@ impl<'ctx> AsyncSigningSession<'ctx> { // Sign artifact. let input_hash: &[u8] = &hasher.clone().finalize(); - let mut signature_bytes = Vec::new(); let artifact_signature: p256::ecdsa::Signature = self.private_key.sign_digest(hasher); - artifact_signature - .to_der() - .encode_to_vec(&mut signature_bytes) - .expect("failed to encode Signature!"); + let signature_bytes = artifact_signature.to_der().as_bytes().to_owned(); let cert = &self.certs.cert; From 526c2a28eac39762441d8ff5db52fada1410f70c Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Fri, 23 Feb 2024 17:14:04 -0600 Subject: [PATCH 14/27] verify: basic `Verifier` / `AsyncVerifier` docs Signed-off-by: Andrew Pan --- src/verify/verifier.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index 42d37ff255..580bb9e24e 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -34,6 +34,9 @@ use crate::{ use super::{models::CheckedBundle, policy::VerificationPolicy, VerificationResult}; +/// An asynchronous Sigstore verifier. +/// +/// For synchronous usage, see [`Verifier`]. pub struct AsyncVerifier { #[allow(dead_code)] rekor_config: RekorConfiguration, @@ -41,6 +44,9 @@ pub struct AsyncVerifier { } impl AsyncVerifier { + /// Constructs an [`AsyncVerifier`]. + /// + /// For verifications against the public-good trust root, use [`AsyncVerifier::production`]. pub fn new( rekor_config: RekorConfiguration, trust_repo: R, @@ -155,6 +161,8 @@ impl AsyncVerifier { Ok(()) } + /// Verifies an input against the given Sigstore Bundle, ensuring conformance to the provided + /// [`VerificationPolicy`]. pub async fn verify( &self, mut input: R, @@ -182,6 +190,7 @@ impl AsyncVerifier { } impl AsyncVerifier { + /// Constructs an [`AsyncVerifier`] against the public-good trust root. pub fn production() -> SigstoreResult { let updater = SigstoreRepository::new(None)?; @@ -189,12 +198,18 @@ impl AsyncVerifier { } } +/// A synchronous Sigstore verifier. +/// +/// Async callers must use [`AsyncVerifier`]. Async usage of [`Verifier`] will result in a deadlock. pub struct Verifier { inner: AsyncVerifier, rt: tokio::runtime::Runtime, } impl Verifier { + /// Constructs a synchronous Sigstore verifier. + /// + /// For verifications against the public-good trust root, use [`Verifier::production`]. pub fn new( rekor_config: RekorConfiguration, trust_repo: R, @@ -207,6 +222,8 @@ impl Verifier { Ok(Self { rt, inner }) } + /// Verifies an input against the given Sigstore Bundle, ensuring conformance to the provided + /// [`VerificationPolicy`]. pub fn verify( &self, mut input: R, @@ -223,6 +240,7 @@ impl Verifier { } impl Verifier { + /// Constructs a synchronous [`Verifier`] against the public-good trust root. pub fn production() -> SigstoreResult { let updater = SigstoreRepository::new(None)?; From 230ead748ea2d3f65cb45375729404e8d0b5e1df Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Fri, 23 Feb 2024 17:24:58 -0600 Subject: [PATCH 15/27] crypto/certificate: move `ExtensionErrorKind` up Signed-off-by: Andrew Pan --- src/crypto/certificate.rs | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/src/crypto/certificate.rs b/src/crypto/certificate.rs index 9960b27c23..917a95e38a 100644 --- a/src/crypto/certificate.rs +++ b/src/crypto/certificate.rs @@ -137,9 +137,6 @@ pub enum ExtensionErrorKind { pub enum NotLeafErrorKind { #[error("certificate is a CA: CAs are not leaves")] IsCA, - - #[error(transparent)] - Extension(#[from] ExtensionErrorKind), } #[derive(Debug, Error)] @@ -152,9 +149,6 @@ pub enum NotCAErrorKind { #[error("certificate in invalid state: cA={ca}, keyCertSign={key_cert_sign}")] Invalid { ca: bool, key_cert_sign: bool }, - - #[error(transparent)] - Extension(#[from] ExtensionErrorKind), } #[derive(Debug, Error)] @@ -169,6 +163,8 @@ pub enum CertificateValidationError { NotLeaf(#[from] NotLeafErrorKind), NotCA(#[from] NotCAErrorKind), + + Extension(#[from] ExtensionErrorKind), } /// Check if the given certificate is a leaf in the context of the Sigstore profile. @@ -199,16 +195,12 @@ pub(crate) fn is_leaf( .get::() .map_err(CertificateValidationError::Malformed)? { - None => Err(NotLeafErrorKind::Extension(ExtensionErrorKind::Missing( - "KeyUsage", - )))?, + None => Err(ExtensionErrorKind::Missing("KeyUsage"))?, Some((_, key_usage)) => key_usage.digital_signature(), }; if !digital_signature { - Err(NotLeafErrorKind::Extension(ExtensionErrorKind::BitUnset( - "KeyUsage.digitalSignature", - )))?; + Err(ExtensionErrorKind::BitUnset("KeyUsage.digitalSignature"))?; } // Finally, we check to make sure the leaf has an `ExtendedKeyUsages` @@ -219,16 +211,14 @@ pub(crate) fn is_leaf( .get::() .map_err(CertificateValidationError::Malformed)? { - None => Err(NotLeafErrorKind::Extension(ExtensionErrorKind::Missing( - "ExtendedKeyUsage", - )))?, + None => Err(ExtensionErrorKind::Missing("ExtendedKeyUsage"))?, Some((_, extended_key_usage)) => extended_key_usage, }; if !extended_key_usage.0.contains(&ID_KP_CODE_SIGNING) { - Err(NotLeafErrorKind::Extension(ExtensionErrorKind::BitUnset( + Err(ExtensionErrorKind::BitUnset( "ExtendedKeyUsage.digitalSignature", - )))?; + ))?; } Ok(()) @@ -264,14 +254,10 @@ pub(crate) fn is_ca( .get::() .map_err(CertificateValidationError::Malformed)? { - None => Err(NotCAErrorKind::Extension(ExtensionErrorKind::Missing( - "BasicConstraints", - )))?, + None => Err(ExtensionErrorKind::Missing("BasicConstraints"))?, Some((false, _)) => { // BasicConstraints must be marked as critical, per RFC 5280 4.2.1.9. - Err(NotCAErrorKind::Extension(ExtensionErrorKind::NotCritical( - "BasicConstraints", - )))? + Err(ExtensionErrorKind::NotCritical("BasicConstraints"))? } Some((true, v)) => v.ca, }; @@ -280,9 +266,7 @@ pub(crate) fn is_ca( .get::() .map_err(CertificateValidationError::Malformed)? { - None => Err(NotCAErrorKind::Extension(ExtensionErrorKind::Missing( - "KeyUsage", - )))?, + None => Err(ExtensionErrorKind::Missing("KeyUsage"))?, Some((_, v)) => v.key_cert_sign(), }; From fe83993e2b3a33603ad318fdc4d7563b97be1923 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Mon, 26 Feb 2024 10:56:26 -0600 Subject: [PATCH 16/27] feedback from review Signed-off-by: Andrew Pan --- src/bundle/mod.rs | 3 +-- src/crypto/certificate.rs | 17 ++++++++--------- src/verify/policy.rs | 28 ++++++++++++++-------------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs index b3b040c15f..c930c0d1c4 100644 --- a/src/bundle/mod.rs +++ b/src/bundle/mod.rs @@ -90,8 +90,7 @@ impl TryFrom for TransparencyLogEntry { fn try_from(value: RekorLogEntry) -> Result { let canonicalized_body = { - let mut body = - json_syntax::to_value(value.body).expect("failed to parse constructed Body!"); + let mut body = json_syntax::to_value(value.body).or(Err(()))?; body.canonicalize(); body.compact_print().to_string().into_bytes() }; diff --git a/src/crypto/certificate.rs b/src/crypto/certificate.rs index 917a95e38a..3d519d1d5a 100644 --- a/src/crypto/certificate.rs +++ b/src/crypto/certificate.rs @@ -248,7 +248,7 @@ pub(crate) fn is_ca( // - `BasicConstraints.ca` // // Any other combination of states is inconsistent and invalid, meaning - // that we won't consider the certificate a valid non-CA leaf. + // that we won't treat the certificate as neither a leaf nor a CA. let ca = match tbs .get::() @@ -270,17 +270,16 @@ pub(crate) fn is_ca( Some((_, v)) => v.key_cert_sign(), }; - // both states set, this is a CA. - if ca && key_cert_sign { - return Ok(()); + if !ca || !key_cert_sign { + Err(NotCAErrorKind::Invalid { ca, key_cert_sign })? } if !(ca || key_cert_sign) { Err(NotCAErrorKind::NotCA)?; } - // Anything else is an invalid state that should never occur. - Err(NotCAErrorKind::Invalid { ca, key_cert_sign })? + // both states set, this is a CA. + return Ok(()); } /// Returns `True` if and only if the given `Certificate` indicates @@ -306,11 +305,11 @@ pub(crate) fn is_root_ca( is_ca(certificate)?; // A certificate that is its own issuer and signer is considered a root CA. - if tbs.issuer == tbs.subject { - Ok(()) - } else { + if tbs.issuer != tbs.subject { Err(NotCAErrorKind::NotRootCA)? } + + Ok(()) } #[cfg(test)] diff --git a/src/verify/policy.rs b/src/verify/policy.rs index 16befbc32d..67389c0290 100644 --- a/src/verify/policy.rs +++ b/src/verify/policy.rs @@ -112,14 +112,14 @@ impl VerificationPolicy for T .expect("failed to parse constructed Extension!"); if val != self.value() { - Err(PolicyError::ExtensionCheckFailed { + return Err(PolicyError::ExtensionCheckFailed { extension: T::name().to_owned(), expected: self.value().to_owned(), actual: val.to_owned(), - }) - } else { - Ok(()) + }); } + + Ok(()) } } @@ -220,14 +220,14 @@ impl VerificationPolicy for AllOf<'_> { let results = self.children.iter().map(|policy| policy.verify(cert).err()); let failures: Vec<_> = results.flatten().collect(); - if failures.is_empty() { - Ok(()) - } else { - Err(PolicyError::AllOf { + if !failures.is_empty() { + return Err(PolicyError::AllOf { total: self.children.len(), errors: failures, - }) + }); } + + Ok(()) } } @@ -285,14 +285,14 @@ impl VerificationPolicy for Identity { }) .collect(); - if names.contains(&self.identity.as_str()) { - Ok(()) - } else { - Err(PolicyError::ExtensionCheckFailed { + if !names.contains(&self.identity.as_str()) { + return Err(PolicyError::ExtensionCheckFailed { extension: "SubjectAltName".to_owned(), expected: self.identity.clone(), actual: names.join(", "), - }) + }); } + + Ok(()) } } From ce0c6cbcb9f885b0284e89317b344699be7b7b7a Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Mon, 26 Feb 2024 11:03:48 -0600 Subject: [PATCH 17/27] certificate: undo is_ca feedback changes Signed-off-by: Andrew Pan --- src/crypto/certificate.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/crypto/certificate.rs b/src/crypto/certificate.rs index 3d519d1d5a..9711d4d948 100644 --- a/src/crypto/certificate.rs +++ b/src/crypto/certificate.rs @@ -270,16 +270,17 @@ pub(crate) fn is_ca( Some((_, v)) => v.key_cert_sign(), }; - if !ca || !key_cert_sign { - Err(NotCAErrorKind::Invalid { ca, key_cert_sign })? + // both states set, this is a CA. + if ca && key_cert_sign { + return Ok(()); } if !(ca || key_cert_sign) { Err(NotCAErrorKind::NotCA)?; } - // both states set, this is a CA. - return Ok(()); + // Anything else is an invalid state that should never occur. + Err(NotCAErrorKind::Invalid { ca, key_cert_sign })? } /// Returns `True` if and only if the given `Certificate` indicates From 126442e8a6a67d08cff4dfb5899039149a268695 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 19:40:37 +0000 Subject: [PATCH 18/27] chore(deps): Update json-syntax requirement from 0.11.1 to 0.12.2 Updates the requirements on [json-syntax](https://github.com/timothee-haudebourg/json-syntax) to permit the latest version. - [Commits](https://github.com/timothee-haudebourg/json-syntax/compare/0.11.1...0.12.2) --- updated-dependencies: - dependency-name: json-syntax dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 46ad8fc908..b482dc4720 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,7 +127,7 @@ zeroize = "1.5.7" rustls-webpki = { version = "0.102.1", features = ["alloc"] } serde_repr = "0.1.16" hex = "0.4.3" -json-syntax = { version = "0.11.1", features = ["canonicalize", "serde"] } +json-syntax = { version = "0.12.2", features = ["canonicalize", "serde"] } tls_codec = { version = "0.4.1", features = ["derive"] } ring = "0.17.6" From caec81c2262a79fe83aeef76be3bca3e00a9ea8f Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Wed, 10 Apr 2024 14:17:07 -0500 Subject: [PATCH 19/27] 2024-04-10 pr feedback Signed-off-by: Andrew Pan --- Cargo.toml | 12 +++++------ src/crypto/verification_key.rs | 2 +- src/errors.rs | 3 --- src/trust/sigstore/constants.rs | 2 +- src/trust/sigstore/mod.rs | 17 ++++++++-------- src/verify/policy.rs | 12 ++++++++--- src/verify/verifier.rs | 35 ++++++++++++++++++++++----------- 7 files changed, 48 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 545e30806d..793ebf7a7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ test-registry = [] fulcio-native-tls = ["oauth-native-tls", "reqwest/native-tls", "fulcio"] fulcio-rustls-tls = ["oauth-rustls-tls", "reqwest/rustls-tls", "fulcio"] -fulcio = [] +fulcio = ["serde_with"] oauth-native-tls = ["openidconnect/native-tls", "oauth"] oauth-rustls-tls = ["openidconnect/rustls-tls", "oauth"] @@ -40,12 +40,12 @@ rekor-native-tls = ["reqwest/native-tls", "rekor"] rekor-rustls-tls = ["reqwest/rustls-tls", "rekor"] rekor = ["reqwest"] -sigstore-trust-root = ["futures-util", "tough", "regex", "tokio/sync"] - -bundle = [] +bundle = ["sigstore_protobuf_specs"] sign = ["bundle"] verify = ["bundle"] +sigstore-trust-root = ["bundle", "futures-util", "tough", "regex", "tokio/sync"] + cosign-native-tls = [ "oci-distribution/native-tls", "cert", @@ -113,10 +113,10 @@ rsa = "0.9.2" scrypt = "0.11.0" serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" -serde_with = { version = "3.4.0", features = ["base64", "json"] } +serde_with = { version = "3.4.0", features = ["base64", "json"], optional = true } sha2 = { version = "0.10.6", features = ["oid"] } signature = { version = "2.0" } -sigstore_protobuf_specs = "0.3.2" +sigstore_protobuf_specs = { version = "0.3.2", optional = true } thiserror = "1.0.30" tokio = { version = "1.17.0", features = ["rt"] } tokio-util = { version = "0.7.10", features = ["io-util"] } diff --git a/src/crypto/verification_key.rs b/src/crypto/verification_key.rs index e787f7e4c8..5d877750c8 100644 --- a/src/crypto/verification_key.rs +++ b/src/crypto/verification_key.rs @@ -331,7 +331,7 @@ impl CosignVerificationKey { /// Verify the signature provided has been actually generated by the given key /// when signing the provided prehashed message. - pub fn verify_prehash(&self, signature: Signature, msg: &[u8]) -> Result<()> { + pub(crate) fn verify_prehash(&self, signature: Signature, msg: &[u8]) -> Result<()> { let sig = match signature { Signature::Raw(data) => data.to_owned(), Signature::Base64Encoded(data) => BASE64_STD_ENGINE.decode(data)?, diff --git a/src/errors.rs b/src/errors.rs index b7de172b5f..5ba05393cb 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -109,9 +109,6 @@ pub enum SigstoreError { #[error("Certificate pool error: {0}")] CertificatePoolError(String), - #[error("Certificate invalid: {0}")] - InvalidCertError(String), - #[error("Signing session expired")] ExpiredSigningSession(), diff --git a/src/trust/sigstore/constants.rs b/src/trust/sigstore/constants.rs index bb08b5b4e9..d4630aaf12 100644 --- a/src/trust/sigstore/constants.rs +++ b/src/trust/sigstore/constants.rs @@ -19,7 +19,7 @@ pub(crate) const SIGSTORE_TARGET_BASE: &str = "https://tuf-repo-cdn.sigstore.dev macro_rules! impl_static_resource { {$($name:literal,)+} => { #[inline] - pub(crate) fn static_resource(name: impl AsRef) -> Option<&'static [u8]> { + pub(crate) fn static_resource(name: N) -> Option<&'static [u8]> where N: AsRef { match name.as_ref() { $( $name => Some(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/trust_root/prod/", $name))) diff --git a/src/trust/sigstore/mod.rs b/src/trust/sigstore/mod.rs index 4cb5a3915b..018b1227a8 100644 --- a/src/trust/sigstore/mod.rs +++ b/src/trust/sigstore/mod.rs @@ -38,7 +38,7 @@ /// ``` use futures_util::TryStreamExt; use sha2::{Digest, Sha256}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use tokio_util::bytes::BytesMut; use sigstore_protobuf_specs::dev::sigstore::{ @@ -62,7 +62,7 @@ pub struct SigstoreTrustRoot { impl SigstoreTrustRoot { /// Constructs a new trust repository established by a [tough::Repository]. - pub async fn new(checkout_dir: Option<&Path>) -> Result { + pub async fn new(checkout_dir: Option) -> Result { // These are statically defined and should always parse correctly. let metadata_base = url::Url::parse(constants::SIGSTORE_METADATA_BASE)?; let target_base = url::Url::parse(constants::SIGSTORE_TARGET_BASE)?; @@ -77,8 +77,6 @@ impl SigstoreTrustRoot { .await .map_err(Box::new)?; - let checkout_dir = checkout_dir.map(ToOwned::to_owned); - let trusted_root = { let data = Self::fetch_target(&repository, &checkout_dir, "trusted_root.json").await?; serde_json::from_slice(&data[..])? @@ -105,12 +103,13 @@ impl SigstoreTrustRoot { } }; - // Try reading the target from disk cache. + // First, try reading the target from disk cache. let data = if let Some(Ok(local_data)) = local_path.as_ref().map(std::fs::read) { + debug!("{}: reading from embedded resources", name.raw()); local_data.to_vec() // Try reading the target embedded into the binary. } else if let Some(embedded_data) = constants::static_resource(name.raw()) { - debug!("read embedded target {}", name.raw()); + debug!("{}: reading from remote", name.raw()); embedded_data.to_vec() // If all else fails, read the data from the TUF repo. } else if let Ok(remote_data) = read_remote_target().await { @@ -128,15 +127,15 @@ impl SigstoreTrustRoot { }; let data = if Sha256::digest(&data)[..] != target.hashes.sha256[..] { + debug!("{}: out of date", name.raw()); read_remote_target().await?.to_vec() } else { data }; - // Write the up-to-date data back to the disk. This doesn't need to succeed, as we can - // always fetch the target again later. + // Write our updated data back to the disk. if let Some(local_path) = local_path { - let _ = std::fs::write(local_path, &data); + std::fs::write(local_path, &data)?; } Ok(data) diff --git a/src/verify/policy.rs b/src/verify/policy.rs index 67389c0290..c6c1806df8 100644 --- a/src/verify/policy.rs +++ b/src/verify/policy.rs @@ -109,7 +109,7 @@ impl VerificationPolicy for T // Parse raw string without DER encoding. let val = std::str::from_utf8(ext.extn_value.as_bytes()) - .expect("failed to parse constructed Extension!"); + .or(Err(PolicyError::ExtensionNotFound))?; if val != self.value() { return Err(PolicyError::ExtensionCheckFailed { @@ -172,7 +172,10 @@ pub struct AnyOf<'a> { } impl<'a> AnyOf<'a> { - pub fn new>(policies: I) -> Self { + pub fn new(policies: I) -> Self + where + I: IntoIterator, + { Self { children: policies.into_iter().collect(), } @@ -200,7 +203,10 @@ pub struct AllOf<'a> { } impl<'a> AllOf<'a> { - pub fn new>(policies: I) -> Option { + pub fn new(policies: I) -> Option + where + I: IntoIterator, + { let children: Vec<_> = policies.into_iter().collect(); // Without this, we'd be able to construct an `AllOf` containing an empty list of child diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index 5d43e96f22..a461b910e7 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -62,13 +62,16 @@ impl AsyncVerifier { }) } - async fn verify_digest( + async fn verify_digest

( &self, input_digest: Sha256, bundle: Bundle, - policy: &impl VerificationPolicy, + policy: &P, offline: bool, - ) -> VerificationResult { + ) -> VerificationResult + where + P: VerificationPolicy, + { let input_digest = input_digest.finalize(); let materials: CheckedBundle = bundle.try_into()?; @@ -129,9 +132,9 @@ impl AsyncVerifier { // 4) Verify that the Rekor entry is consistent with the other signing // materials - let Some(log_entry) = materials.tlog_entry(offline, &input_digest) else { - return Err(SignatureErrorKind::Transparency)?; - }; + let log_entry = materials + .tlog_entry(offline, &input_digest) + .ok_or(SignatureErrorKind::Transparency)?; debug!("log entry is consistent with other materials"); // 5) Verify the inclusion proof supplied by Rekor for this artifact, @@ -166,13 +169,17 @@ impl AsyncVerifier { /// Verifies an input against the given Sigstore Bundle, ensuring conformance to the provided /// [`VerificationPolicy`]. - pub async fn verify( + pub async fn verify( &self, mut input: R, bundle: Bundle, - policy: &impl VerificationPolicy, + policy: &P, offline: bool, - ) -> VerificationResult { + ) -> VerificationResult + where + R: AsyncRead + Unpin + Send, + P: VerificationPolicy, + { // arbitrary buffer size, chosen to be a multiple of the digest size. let mut buf = [0u8; 1024]; let mut hasher = Sha256::new(); @@ -228,13 +235,17 @@ impl Verifier { /// Verifies an input against the given Sigstore Bundle, ensuring conformance to the provided /// [`VerificationPolicy`]. - pub fn verify( + pub fn verify( &self, mut input: R, bundle: Bundle, - policy: &impl VerificationPolicy, + policy: &P, offline: bool, - ) -> VerificationResult { + ) -> VerificationResult + where + R: Read, + P: VerificationPolicy, + { let mut hasher = Sha256::new(); io::copy(&mut input, &mut hasher).map_err(VerificationError::Input)?; From 11970d522f7bdfe8dfb3b202a81618952aa8a72b Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Wed, 10 Apr 2024 14:21:44 -0500 Subject: [PATCH 20/27] verify: publicize `verify_digest` Signed-off-by: Andrew Pan --- src/verify/verifier.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index a461b910e7..45371e79af 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -62,7 +62,7 @@ impl AsyncVerifier { }) } - async fn verify_digest

( + pub async fn verify_digest

( &self, input_digest: Sha256, bundle: Bundle, @@ -233,6 +233,20 @@ impl Verifier { Ok(Self { rt, inner }) } + pub fn verify_digest

( + &self, + input_digest: Sha256, + bundle: Bundle, + policy: &P, + offline: bool, + ) -> VerificationResult + where + P: VerificationPolicy, + { + self.rt + .block_on(self.inner.verify_digest(input_digest, bundle, policy, offline)) + } + /// Verifies an input against the given Sigstore Bundle, ensuring conformance to the provided /// [`VerificationPolicy`]. pub fn verify( @@ -249,8 +263,7 @@ impl Verifier { let mut hasher = Sha256::new(); io::copy(&mut input, &mut hasher).map_err(VerificationError::Input)?; - self.rt - .block_on(self.inner.verify_digest(hasher, bundle, policy, offline)) + self.verify_digest(hasher, bundle, policy, offline) } } From 39ea179c112685196ee657a30b5c940adaf7e92c Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Wed, 10 Apr 2024 14:26:06 -0500 Subject: [PATCH 21/27] `cargo fmt` Signed-off-by: Andrew Pan --- src/verify/verifier.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index 45371e79af..8cf68584f1 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -243,8 +243,10 @@ impl Verifier { where P: VerificationPolicy, { - self.rt - .block_on(self.inner.verify_digest(input_digest, bundle, policy, offline)) + self.rt.block_on( + self.inner + .verify_digest(input_digest, bundle, policy, offline), + ) } /// Verifies an input against the given Sigstore Bundle, ensuring conformance to the provided From ecf8995354e2a739c96d4643aee421627fb451c8 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Wed, 10 Apr 2024 18:21:37 -0500 Subject: [PATCH 22/27] adjust `bundle`, `sign`, `verify` feature flags Signed-off-by: Andrew Pan --- Cargo.toml | 26 +++++++++++++------------- src/bundle/mod.rs | 2 +- src/errors.rs | 3 ++- src/lib.rs | 7 ++----- src/trust/sigstore/mod.rs | 5 +++-- src/verify/models.rs | 3 +-- src/verify/verifier.rs | 4 ++++ tests/conformance/Cargo.toml | 3 ++- tests/conformance/conformance.rs | 4 ++-- 9 files changed, 30 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 793ebf7a7f..e5e6640f62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" repository = "https://github.com/sigstore/sigstore-rs" [features] -default = ["full-native-tls", "cached-client", "sigstore-trust-root", "sign", "verify"] +default = ["full-native-tls", "cached-client", "sigstore-trust-root", "bundle"] wasm = ["getrandom/js", "ring/wasm32_unknown_unknown_js"] full-native-tls = [ @@ -28,23 +28,23 @@ full-rustls-tls = [ # This features is used by tests that use docker to create a registry test-registry = [] -fulcio-native-tls = ["oauth-native-tls", "reqwest/native-tls", "fulcio"] -fulcio-rustls-tls = ["oauth-rustls-tls", "reqwest/rustls-tls", "fulcio"] -fulcio = ["serde_with"] - oauth-native-tls = ["openidconnect/native-tls", "oauth"] oauth-rustls-tls = ["openidconnect/rustls-tls", "oauth"] -oauth = [] +oauth = ["openidconnect"] + +fulcio-native-tls = ["oauth-native-tls", "reqwest/native-tls", "fulcio"] +fulcio-rustls-tls = ["oauth-rustls-tls", "reqwest/rustls-tls", "fulcio"] +fulcio = ["oauth", "serde_with"] rekor-native-tls = ["reqwest/native-tls", "rekor"] rekor-rustls-tls = ["reqwest/rustls-tls", "rekor"] rekor = ["reqwest"] -bundle = ["sigstore_protobuf_specs"] -sign = ["bundle"] -verify = ["bundle"] +sign = ["sigstore_protobuf_specs", "fulcio", "rekor", "cert"] +verify = ["sigstore_protobuf_specs", "fulcio", "rekor", "cert"] +bundle = ["sign", "verify"] -sigstore-trust-root = ["bundle", "futures-util", "tough", "regex", "tokio/sync"] +sigstore-trust-root = ["sigstore_protobuf_specs", "futures-util", "tough", "regex", "tokio/sync"] cosign-native-tls = [ "oci-distribution/native-tls", @@ -58,12 +58,12 @@ cosign-rustls-tls = [ "cosign", "registry-rustls-tls", ] -cosign = [] +cosign = ["olpc-cjson"] cert = [] registry-native-tls = ["oci-distribution/native-tls", "registry"] registry-rustls-tls = ["oci-distribution/rustls-tls", "registry"] -registry = [] +registry = ["olpc-cjson"] mock-client-native-tls = ["oci-distribution/native-tls", "mock-client"] mock-client-rustls-tls = ["oci-distribution/rustls-tls", "mock-client"] @@ -87,7 +87,7 @@ futures = "0.3" futures-util = { version = "0.3.30", optional = true } lazy_static = "1.4.0" oci-distribution = { version = "0.11", default-features = false, optional = true } -olpc-cjson = "0.1" +olpc-cjson = { version = "0.1", optional = true } openidconnect = { version = "3.0", default-features = false, features = [ "reqwest", ], optional = true } diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs index c930c0d1c4..c8c8ea64b0 100644 --- a/src/bundle/mod.rs +++ b/src/bundle/mod.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Useful types for Sigstore bundles. +//! Common types for Sigstore Bundle support. use std::fmt::Display; use std::str::FromStr; diff --git a/src/errors.rs b/src/errors.rs index 5ba05393cb..56a3f93856 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -133,7 +133,8 @@ pub enum SigstoreError { #[error(transparent)] JoinError(#[from] tokio::task::JoinError), - #[cfg(feature = "sign")] + // HACK(tnytown): Remove when we rework the Fulcio V2 endpoint. + #[cfg(feature = "fulcio")] #[error(transparent)] ReqwestError(#[from] reqwest::Error), diff --git a/src/lib.rs b/src/lib.rs index 33c5f3eebb..1fee312ca5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -282,11 +282,8 @@ pub mod registry; #[cfg(feature = "rekor")] pub mod rekor; -// Don't export yet -- these types should only be useful internally. -#[cfg(feature = "bundle")] -mod bundle; -#[cfg(feature = "bundle")] -pub use bundle::Bundle; +#[cfg(any(feature = "sign", feature = "verify"))] +pub mod bundle; #[cfg(feature = "verify")] pub mod verify; diff --git a/src/trust/sigstore/mod.rs b/src/trust/sigstore/mod.rs index 018b1227a8..b4a27d1712 100644 --- a/src/trust/sigstore/mod.rs +++ b/src/trust/sigstore/mod.rs @@ -105,14 +105,15 @@ impl SigstoreTrustRoot { // First, try reading the target from disk cache. let data = if let Some(Ok(local_data)) = local_path.as_ref().map(std::fs::read) { - debug!("{}: reading from embedded resources", name.raw()); + debug!("{}: reading from disk cache", name.raw()); local_data.to_vec() // Try reading the target embedded into the binary. } else if let Some(embedded_data) = constants::static_resource(name.raw()) { - debug!("{}: reading from remote", name.raw()); + debug!("{}: reading from embedded resources", name.raw()); embedded_data.to_vec() // If all else fails, read the data from the TUF repo. } else if let Ok(remote_data) = read_remote_target().await { + debug!("{}: reading from remote", name.raw()); remote_data.to_vec() } else { return Err(SigstoreError::TufTargetNotFoundError(name.raw().to_owned())); diff --git a/src/verify/models.rs b/src/verify/models.rs index 5a6777fca4..01fdc8b660 100644 --- a/src/verify/models.rs +++ b/src/verify/models.rs @@ -16,12 +16,11 @@ use std::str::FromStr; use crate::{ - bundle::Version as BundleVersion, + bundle::{Bundle, Version as BundleVersion}, crypto::certificate::{is_leaf, is_root_ca, CertificateValidationError}, rekor::models as rekor, }; -use crate::Bundle; use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; use sigstore_protobuf_specs::dev::sigstore::{ bundle::v1::{bundle, verification_material}, diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index 8cf68584f1..818888996a 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -62,6 +62,8 @@ impl AsyncVerifier { }) } + /// Verifies an input digest against the given Sigstore Bundle, ensuring conformance to the + /// provided [`VerificationPolicy`]. pub async fn verify_digest

( &self, input_digest: Sha256, @@ -233,6 +235,8 @@ impl Verifier { Ok(Self { rt, inner }) } + /// Verifies an input digest against the given Sigstore Bundle, ensuring conformance to the + /// provided [`VerificationPolicy`]. pub fn verify_digest

( &self, input_digest: Sha256, diff --git a/tests/conformance/Cargo.toml b/tests/conformance/Cargo.toml index b5d896c3e3..1f2cc3d92e 100644 --- a/tests/conformance/Cargo.toml +++ b/tests/conformance/Cargo.toml @@ -10,7 +10,8 @@ license = "Apache-2.0" clap = { version = "4.0.8", features = ["derive"] } anyhow = "1.0.75" serde_json = "1.0.107" -sigstore = { path = "../../" } +sigstore = { path = "../../", default-features = false, features = ["bundle", "sigstore-trust-root", "full-native-tls"] } +tracing-subscriber = "0.3" [[bin]] name = "sigstore" diff --git a/tests/conformance/conformance.rs b/tests/conformance/conformance.rs index 200d6c8545..e8ab9d3697 100644 --- a/tests/conformance/conformance.rs +++ b/tests/conformance/conformance.rs @@ -18,7 +18,6 @@ use std::{fs, process::exit}; -use anyhow::anyhow; use clap::{Parser, Subcommand}; use sigstore::{ oauth::IdentityToken, @@ -113,6 +112,7 @@ struct VerifyBundle { } fn main() { + tracing_subscriber::fmt::init(); let cli = Cli::parse(); let result = match cli.command { @@ -160,7 +160,7 @@ fn verify_bundle(args: VerifyBundle) -> anyhow::Result<()> { let bundle = fs::File::open(bundle)?; let mut artifact = fs::File::open(artifact)?; - let bundle: sigstore::Bundle = serde_json::from_reader(bundle)?; + let bundle: sigstore::bundle::Bundle = serde_json::from_reader(bundle)?; let verifier = Verifier::production()?; verifier.verify( From 3f150a207e8a1a8a546eb67472886f080ff0b299 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Wed, 10 Apr 2024 18:38:42 -0500 Subject: [PATCH 23/27] fixup `cargo build` on features `bundle, wasm` Signed-off-by: Andrew Pan --- Cargo.toml | 4 ++-- src/crypto/certificate_pool.rs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e5e6640f62..dd9c659f61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/sigstore/sigstore-rs" [features] default = ["full-native-tls", "cached-client", "sigstore-trust-root", "bundle"] -wasm = ["getrandom/js", "ring/wasm32_unknown_unknown_js"] +wasm = ["getrandom/js", "ring/wasm32_unknown_unknown_js", "chrono/wasmbind"] full-native-tls = [ "fulcio-native-tls", @@ -76,7 +76,7 @@ async-trait = "0.1.52" base64 = "0.22.0" cached = { version = "0.49.2", optional = true, features = ["async"] } cfg-if = "1.0.0" -chrono = { version = "0.4.27", default-features = false, features = ["serde"] } +chrono = { version = "0.4.27", default-features = false, features = ["now", "serde"] } const-oid = { version = "0.9.6", features = ["db"] } digest = { version = "0.10.3", default-features = false } ecdsa = { version = "0.16.7", features = ["pkcs8", "digest", "der", "signing"] } diff --git a/src/crypto/certificate_pool.rs b/src/crypto/certificate_pool.rs index 9502fdc208..1fddead331 100644 --- a/src/crypto/certificate_pool.rs +++ b/src/crypto/certificate_pool.rs @@ -88,8 +88,12 @@ impl CertificatePool { ) -> SigstoreResult<()> { let der = CertificateDer::from(der); let cert = EndEntityCert::try_from(&der)?; + let time = std::time::Duration::from_secs(chrono::Utc::now().timestamp() as u64); - self.verify_cert_with_time(&cert, verification_time.unwrap_or(UnixTime::now()))?; + self.verify_cert_with_time( + &cert, + verification_time.unwrap_or(UnixTime::since_unix_epoch(time)), + )?; Ok(()) } From 81ae53920e78f3d39d1429b62e24a2800931ba86 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Wed, 10 Apr 2024 21:17:08 -0500 Subject: [PATCH 24/27] verifier: fixup time range check Signed-off-by: Andrew Pan --- src/verify/verifier.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/verify/verifier.rs b/src/verify/verifier.rs index 818888996a..ac4d4f62b3 100644 --- a/src/verify/verifier.rs +++ b/src/verify/verifier.rs @@ -160,7 +160,7 @@ impl AsyncVerifier { .not_after .to_unix_duration() .as_secs(); - if !(not_before <= integrated_time && integrated_time <= not_after) { + if integrated_time < not_before || integrated_time > not_after { return Err(CertificateErrorKind::Expired)?; } debug!("data signed during validity period"); From 71f5161a6b92ae9cf8276149f8bf1deb023c32ac Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Thu, 11 Apr 2024 16:38:58 -0500 Subject: [PATCH 25/27] trust/sigstore: test caching behavior Signed-off-by: Andrew Pan --- src/trust/sigstore/mod.rs | 151 +++++++++++++++++- .../data/repository/1.registry.npmjs.org.json | 23 --- tests/data/repository/1.root.json | 65 -------- tests/data/repository/1.snapshot.json | 32 ---- tests/data/repository/1.targets.json | 148 ----------------- .../data/repository/2.registry.npmjs.org.json | 23 --- tests/data/repository/2.root.json | 65 -------- tests/data/repository/2.snapshot.json | 32 ---- tests/data/repository/2.targets.json | 135 ---------------- tests/data/repository/registry.npmjs.org.json | 23 --- tests/data/repository/root.json | 65 -------- tests/data/repository/snapshot.json | 32 ---- tests/data/repository/targets.json | 135 ---------------- ...67072b6f89ddf1032273a78b.trusted_root.json | 86 ---------- ...40ca812d8ac47a128bf84963.trusted_root.json | 86 ---------- ...8f8df61bc7274189122c123446248426.keys.json | 26 --- ...2551fcaa870a30d4601ba1caf6f63699.keys.json | 26 --- tests/data/repository/timestamp.json | 24 --- 18 files changed, 143 insertions(+), 1034 deletions(-) delete mode 100644 tests/data/repository/1.registry.npmjs.org.json delete mode 100644 tests/data/repository/1.root.json delete mode 100644 tests/data/repository/1.snapshot.json delete mode 100644 tests/data/repository/1.targets.json delete mode 100644 tests/data/repository/2.registry.npmjs.org.json delete mode 100644 tests/data/repository/2.root.json delete mode 100644 tests/data/repository/2.snapshot.json delete mode 100644 tests/data/repository/2.targets.json delete mode 100644 tests/data/repository/registry.npmjs.org.json delete mode 100644 tests/data/repository/root.json delete mode 100644 tests/data/repository/snapshot.json delete mode 100644 tests/data/repository/targets.json delete mode 100644 tests/data/repository/targets/6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b.trusted_root.json delete mode 100644 tests/data/repository/targets/fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963.trusted_root.json delete mode 100644 tests/data/repository/targets/registry.npmjs.org/7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426.keys.json delete mode 100644 tests/data/repository/targets/registry.npmjs.org/881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699.keys.json delete mode 100644 tests/data/repository/timestamp.json diff --git a/src/trust/sigstore/mod.rs b/src/trust/sigstore/mod.rs index b4a27d1712..81954a9891 100644 --- a/src/trust/sigstore/mod.rs +++ b/src/trust/sigstore/mod.rs @@ -61,8 +61,21 @@ pub struct SigstoreTrustRoot { } impl SigstoreTrustRoot { - /// Constructs a new trust repository established by a [tough::Repository]. - pub async fn new(checkout_dir: Option) -> Result { + /// Constructs a new trust root from a [`tough::Repository`]. + async fn from_tough( + repository: &tough::Repository, + checkout_dir: Option, + ) -> Result { + let trusted_root = { + let data = Self::fetch_target(&repository, &checkout_dir, "trusted_root.json").await?; + serde_json::from_slice(&data[..])? + }; + + Ok(Self { trusted_root }) + } + + /// Constructs a new trust root backed by the Sigstore Public Good Instance. + pub async fn new(cache_dir: Option) -> Result { // These are statically defined and should always parse correctly. let metadata_base = url::Url::parse(constants::SIGSTORE_METADATA_BASE)?; let target_base = url::Url::parse(constants::SIGSTORE_TARGET_BASE)?; @@ -77,12 +90,7 @@ impl SigstoreTrustRoot { .await .map_err(Box::new)?; - let trusted_root = { - let data = Self::fetch_target(&repository, &checkout_dir, "trusted_root.json").await?; - serde_json::from_slice(&data[..])? - }; - - Ok(Self { trusted_root }) + Self::from_tough(&repository, cache_dir).await } async fn fetch_target( @@ -242,3 +250,130 @@ fn is_timerange_valid(range: Option<&TimeRange>, allow_expired: bool) -> bool { (_, Some(end)) => now <= end, } } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::Path; + use std::time::SystemTime; + use tempfile::TempDir; + + fn verify(root: &SigstoreTrustRoot, cache_dir: Option<&Path>) { + if let Some(cache_dir) = cache_dir { + assert!( + cache_dir.join("trusted_root.json").exists(), + "the trusted root was not cached" + ); + } + + assert!( + root.fulcio_certs().is_ok_and(|v| !v.is_empty()), + "no Fulcio certs established" + ); + assert!( + root.rekor_keys().is_ok_and(|v| !v.is_empty()), + "no Rekor keys established" + ); + assert!( + root.ctfe_keys().is_ok_and(|v| !v.is_empty()), + "no CTFE keys established" + ); + } + + macro_rules! impl_test { + ($name:ident, cache=$cache:literal $(,setup=$setup:expr)? $(,verify=$verify:expr)?) => { + #[test] + fn $name() -> Result<()> { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + let cache_dir = if $cache { + let tmp = TempDir::new().expect("cannot create temp cache dir"); + Some(tmp.into_path()) + } else { + None + }; + let cache_dir_borrowed = cache_dir.as_ref().map(|p| p.as_path()); + + $( + $setup(cache_dir_borrowed); + )? + + let root = rt + .block_on(SigstoreTrustRoot::new(cache_dir.clone())) + .expect("failed to create trust root"); + + verify(&root, cache_dir_borrowed); + + $($verify(&root, cache_dir_borrowed);)? + + Ok(()) + } + } + } + + impl_test!(no_local_cache, cache = false); + impl_test!( + current_local_cache, + cache = true, + verify = |_, cache_dir: Option<&Path>| { + assert!( + cache_dir.unwrap().join("trusted_root.json").exists(), + "TUF cache was requested but not used" + ); + } + ); + impl_test!( + outdated_local_cache, + cache = true, + setup = |cache_dir: Option<&Path>| { + fs::write( + cache_dir.unwrap().join("trusted_root.json"), + b"fake trusted root", + ) + .expect("cannot write file to cache dir"); + }, + verify = |_, cache_dir: Option<&Path>| { + let data = fs::read(cache_dir.unwrap().join("trusted_root.json")) + .expect("cannot read trusted_root.json from cache"); + + assert_ne!( + data, b"fake trusted root", + "TUF cache was not properly updated" + ); + } + ); + + #[test] + fn test_is_timerange_valid() { + fn range_from(start: i64, end: i64) -> TimeRange { + let base = chrono::Utc::now(); + let start: SystemTime = (base + chrono::TimeDelta::seconds(start)).into(); + let end: SystemTime = (base + chrono::TimeDelta::seconds(end)).into(); + + TimeRange { + start: Some(start.into()), + end: Some(end.into()), + } + } + + assert!(is_timerange_valid(None, true)); + assert!(is_timerange_valid(None, false)); + + // Test lower bound conditions + + // Valid: 1 ago, 1 from now + assert!(is_timerange_valid(Some(&range_from(-1, 1)), false)); + // Invalid: 1 from now, 1 from now + assert!(!is_timerange_valid(Some(&range_from(1, 1)), false)); + + // Test upper bound conditions + + // Invalid: 1 ago, 1 ago + assert!(!is_timerange_valid(Some(&range_from(-1, -1)), false)); + // Valid: 1 ago, 1 ago + assert!(is_timerange_valid(Some(&range_from(-1, -1)), true)) + } +} diff --git a/tests/data/repository/1.registry.npmjs.org.json b/tests/data/repository/1.registry.npmjs.org.json deleted file mode 100644 index 1c8ec2165b..0000000000 --- a/tests/data/repository/1.registry.npmjs.org.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "signed": { - "_type": "targets", - "spec_version": "1.0", - "version": 1, - "expires": "2024-09-29T16:47:20Z", - "targets": { - "registry.npmjs.org/keys.json": { - "length": 1017, - "hashes": { - "sha256": "7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426", - "sha512": "881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699" - } - } - } - }, - "signatures": [ - { - "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", - "sig": "3044022059bf01a64dd2793d5b630e26d7b6e455b0d6d8b47c23049ae856a122e5cec2ab022068b99b8bb39457e53d500f698cb43f9e640958ed26e5d3a47c29619df61889bc" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/1.root.json b/tests/data/repository/1.root.json deleted file mode 100644 index 4ca0da87fe..0000000000 --- a/tests/data/repository/1.root.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "signed": { - "_type": "root", - "spec_version": "1.0", - "version": 1, - "expires": "2024-09-29T16:47:17Z", - "keys": { - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { - "keytype": "ecdsa-sha2-nistp256", - "scheme": "ecdsa-sha2-nistp256", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" - } - }, - "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda": { - "keytype": "ecdsa-sha2-nistp256", - "scheme": "ecdsa-sha2-nistp256", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL3vL/VeaH6nBbo4rekyO4cc/QthS\n+nlyJXCXSnyIMAtLmVTa8Pf0qG6YIVaR0TmLkyk9YoSVsZakxuMTuaEwrg==\n-----END PUBLIC KEY-----\n" - } - } - }, - "roles": { - "root": { - "keyids": [ - "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" - ], - "threshold": 1 - }, - "snapshot": { - "keyids": [ - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" - ], - "threshold": 1 - }, - "targets": { - "keyids": [ - "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" - ], - "threshold": 1 - }, - "timestamp": { - "keyids": [ - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" - ], - "threshold": 1 - } - }, - "consistent_snapshot": true - }, - "signatures": [ - { - "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", - "sig": "304602210085927cdb96e1d9d0876bfc26b6ceea7421a54f959e30b9af3e12d31f6c750543022100dde611b58a1f2b9fb26c43767138c68f4422cdeb898c8b63f3f0193791030d12" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/1.snapshot.json b/tests/data/repository/1.snapshot.json deleted file mode 100644 index fcb179878c..0000000000 --- a/tests/data/repository/1.snapshot.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "signed": { - "_type": "snapshot", - "spec_version": "1.0", - "version": 1, - "expires": "2024-04-19T16:47:48Z", - "meta": { - "registry.npmjs.org.json": { - "length": 713, - "hashes": { - "sha256": "17b361687dbb401c2d51d7ce21688d13547eae7f8e7b2183b7dd6d94fa675705", - "sha512": "3f60a08cdbab650ece48ded43b54943dc816580fdb2f5a2a20c30e878eb2489ab817f0308666cac80da03d75d6f5b71959431b1ba7794335fece8a4ed635eb4d" - }, - "version": 1 - }, - "targets.json": { - "length": 4518, - "hashes": { - "sha256": "cc62e5fb1644717c7429c82b6a1cbd085008f9a2e07aad38573f8fdf9d55386c", - "sha512": "5709bc76bc35da403a9a0a5ec96890db49e797c986eda9e5f7973938dbccad96838c8136617c91f5218cfd919d93745d3942ca6d50a52b5fd0e662e6876b395f" - }, - "version": 1 - } - } - }, - "signatures": [ - { - "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", - "sig": "304602210082d244d5dab0c20ee07b3229964beffaa8bb0bdf4c5107e2f764619878d124a2022100e7c50116ef636c41348ec49a7502f1c98037238b9c717ee781b62c5154f5a1f0" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/1.targets.json b/tests/data/repository/1.targets.json deleted file mode 100644 index 6844bad771..0000000000 --- a/tests/data/repository/1.targets.json +++ /dev/null @@ -1,148 +0,0 @@ -{ - "signed": { - "_type": "targets", - "spec_version": "1.0", - "version": 1, - "expires": "2024-09-29T16:47:20Z", - "targets": { - "artifact.pub": { - "length": 177, - "hashes": { - "sha256": "59ebf97a9850aecec4bc39c1f5c1dc46e6490a6b5fd2a6cacdcac0c3a6fc4cbf", - "sha512": "308fd1d1d95d7f80aa33b837795251cc3e886792982275e062409e13e4e236ffc34d676682aa96fdc751414de99c864bf132dde71581fa651c6343905e3bf988" - }, - "custom": { - "sigstore": { - "status": "Active", - "usage": "Unknown" - } - } - }, - "ctfe.pub": { - "length": 177, - "hashes": { - "sha256": "7fcb94a5d0ed541260473b990b99a6c39864c1fb16f3f3e594a5a3cebbfe138a", - "sha512": "4b20747d1afe2544238ad38cc0cc3010921b177d60ac743767e0ef675b915489bd01a36606c0ff83c06448622d7160f0d866c83d20f0c0f44653dcc3f9aa0bd4" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://ctfe.sigstore.dev/test", - "usage": "CTFE" - } - } - }, - "ctfe_2022.pub": { - "length": 178, - "hashes": { - "sha256": "270488a309d22e804eeb245493e87c667658d749006b9fee9cc614572d4fbbdc", - "sha512": "e83fa4f427b24ee7728637fad1b4aa45ebde2ba02751fa860694b1bb16059a490328f9985e51cc70e4d237545315a1bc866dc4fdeef2f6248d99cc7a6077bf85" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://ctfe.sigstore.dev/2022", - "usage": "CTFE" - } - } - }, - "fulcio.crt.pem": { - "length": 744, - "hashes": { - "sha256": "f360c53b2e13495a628b9b8096455badcb6d375b185c4816d95a5d746ff29908", - "sha512": "0713252a7fd17f7f3ab12f88a64accf2eb14b8ad40ca711d7fe8b4ecba3b24db9e9dffadb997b196d3867b8f9ff217faf930d80e4dab4e235c7fc3f07be69224" - }, - "custom": { - "sigstore": { - "status": "Expired", - "uri": "https://fulcio.sigstore.dev", - "usage": "Fulcio" - } - } - }, - "fulcio_intermediate_v1.crt.pem": { - "length": 789, - "hashes": { - "sha256": "f8cbecf186db7714624a5f4e99da31a917cbef70a94dd6921f5c3ca969dfe30a", - "sha512": "0f99f47dbc26c5f1e3cba0bfd9af4245a26e5cb735d6ef005792ec7e603f66fdb897de985973a6e50940ca7eff5e1849719e967b5ad2dac74a29115a41cf6f21" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://fulcio.sigstore.dev", - "usage": "Fulcio" - } - } - }, - "fulcio_v1.crt.pem": { - "length": 740, - "hashes": { - "sha256": "f989aa23def87c549404eadba767768d2a3c8d6d30a8b793f9f518a8eafd2cf5", - "sha512": "f2e33a6dc208cee1f51d33bbea675ab0f0ced269617497985f9a0680689ee7073e4b6f8fef64c91bda590d30c129b3070dddce824c05bc165ac9802f0705cab6" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://fulcio.sigstore.dev", - "usage": "Fulcio" - } - } - }, - "rekor.pub": { - "length": 178, - "hashes": { - "sha256": "dce5ef715502ec9f3cdfd11f8cc384b31a6141023d3e7595e9908a81cb6241bd", - "sha512": "0ae7705e02db33e814329746a4a0e5603c5bdcd91c96d072158d71011a2695788866565a2fec0fe363eb72cbcaeda39e54c5fe8d416daf9f3101fdba4217ef35" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://rekor.sigstore.dev", - "usage": "Rekor" - } - } - }, - "trusted_root.json": { - "length": 4567, - "hashes": { - "sha256": "cec894ad77f79b1cb324150f6363012bcef7492954f3ab9134f932e6aa2e2e20", - "sha512": "08be2fd75c19e654caad30852847c566f97e6245f2bbcc54d347d6bdec7e879135e3395b5633b9e3b85d739fdb9b4eb8c09ddc70495792bc2ea65c8caf770d27" - } - } - }, - "delegations": { - "keys": { - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { - "keytype": "ecdsa-sha2-nistp256", - "scheme": "ecdsa-sha2-nistp256", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" - } - } - }, - "roles": [ - { - "name": "registry.npmjs.org", - "keyids": [ - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" - ], - "threshold": 1, - "terminating": true, - "paths": [ - "registry.npmjs.org/*" - ] - } - ] - } - }, - "signatures": [ - { - "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", - "sig": "304402201662b260e99e59f7271bd9e3fb01aa47a399bef8c5ec808bea6d40ae2d93625d022042fd2a275d84196dc50e17ca9c9408a34349372410febc7217415b11eb978bbb" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/2.registry.npmjs.org.json b/tests/data/repository/2.registry.npmjs.org.json deleted file mode 100644 index d53f15267b..0000000000 --- a/tests/data/repository/2.registry.npmjs.org.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "signed": { - "_type": "targets", - "spec_version": "1.0", - "version": 2, - "expires": "2028-09-29T21:10:55Z", - "targets": { - "registry.npmjs.org/keys.json": { - "length": 1017, - "hashes": { - "sha256": "7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426", - "sha512": "881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699" - } - } - } - }, - "signatures": [ - { - "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", - "sig": "3045022057b9fc8afd9feaf45cf3173d3420fdcd6b68c22e4ef7b47e80a6887e1f20246c0221009f39c42fac630ab354c5197288c9a82ab6d46a59b423f81fff719da57cff16ab" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/2.root.json b/tests/data/repository/2.root.json deleted file mode 100644 index f848d7d846..0000000000 --- a/tests/data/repository/2.root.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "signed": { - "_type": "root", - "spec_version": "1.0", - "version": 2, - "expires": "2028-09-29T21:10:11Z", - "keys": { - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { - "keytype": "ecdsa-sha2-nistp256", - "scheme": "ecdsa-sha2-nistp256", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" - } - }, - "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda": { - "keytype": "ecdsa-sha2-nistp256", - "scheme": "ecdsa-sha2-nistp256", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL3vL/VeaH6nBbo4rekyO4cc/QthS\n+nlyJXCXSnyIMAtLmVTa8Pf0qG6YIVaR0TmLkyk9YoSVsZakxuMTuaEwrg==\n-----END PUBLIC KEY-----\n" - } - } - }, - "roles": { - "root": { - "keyids": [ - "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" - ], - "threshold": 1 - }, - "snapshot": { - "keyids": [ - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" - ], - "threshold": 1 - }, - "targets": { - "keyids": [ - "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" - ], - "threshold": 1 - }, - "timestamp": { - "keyids": [ - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" - ], - "threshold": 1 - } - }, - "consistent_snapshot": true - }, - "signatures": [ - { - "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", - "sig": "3045022057bbd23dd9f69f8280c5e5d2b0a0b1ace98d6d8efa0f59ef0a3190188f6e2c89022100b39e6c24091c4271d2b8b4cfa75e6120638b276fbffddda8da5bca1778c8f08c" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/2.snapshot.json b/tests/data/repository/2.snapshot.json deleted file mode 100644 index 6c1e4dd147..0000000000 --- a/tests/data/repository/2.snapshot.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "signed": { - "_type": "snapshot", - "spec_version": "1.0", - "version": 2, - "expires": "2028-04-19T21:11:16Z", - "meta": { - "registry.npmjs.org.json": { - "length": 715, - "hashes": { - "sha256": "4dc55b2b468b0d1c9629c457c5cfce2cc1c330c59c5a7cf71cb7549f1ef76f1d", - "sha512": "278f4b6112db9d4bd9366e1717cf710ad7eacf44605fd4f894c3374fc5dff850a1a03c24c4a885d050a4ac1a86fa6929537fae12d8c2864c8e0c239b382d5556" - }, - "version": 2 - }, - "targets.json": { - "length": 4120, - "hashes": { - "sha256": "095d093de09350cec021828f49361688b5dd692486ad7bfb03d4150b3269ef8a", - "sha512": "97b9c75f49fb41eaf2f33c5f58b125febc3bbecd4c97f6edd0901423a231e4d0c5760d4780bc180a364d7198b5e0710f07ee0abf84dcd163fe3348d6bce26fab" - }, - "version": 2 - } - } - }, - "signatures": [ - { - "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", - "sig": "3044022013bf1032cf0a37d9f88ab9d33d0abb7a932efd95fadbc354fc21a807c7be29ef0220677e651c3e67e0728591faa20c5a09b8a16953038c3ceeffb7d9cfec766b3245" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/2.targets.json b/tests/data/repository/2.targets.json deleted file mode 100644 index dea42487f0..0000000000 --- a/tests/data/repository/2.targets.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "signed": { - "_type": "targets", - "spec_version": "1.0", - "version": 2, - "expires": "2028-09-29T21:10:55Z", - "targets": { - "ctfe.pub": { - "length": 775, - "hashes": { - "sha256": "bd7a6812a1f239dfddbbb19d36c7423d21510da56d466ba5018401959cd66037", - "sha512": "b861189e48df51186a39612230fba6b02af951f7b35ad9375e8ca182d0e085d470e26d69f7cd4d7450a0f223991e8e5a4ddf8f1968caa15255de8e37035af43a" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://ctfe.sigstage.dev/test", - "usage": "CTFE" - } - } - }, - "ctfe_2022.pub": { - "length": 178, - "hashes": { - "sha256": "910d899c7763563095a0fe684c8477573fedc19a78586de6ecfbfd8f289f5423", - "sha512": "ab975a75600fc366a837536d0dcba841b755552d21bb114498ff8ac9d2403f76643f5b91269bce5d124a365514719a3edee9dcc2b046cb173f51af659911fcd3" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://ctfe.sigstage.dev/2022", - "usage": "CTFE" - } - } - }, - "ctfe_2022_2.pub": { - "length": 178, - "hashes": { - "sha256": "7054b4f15f969daca1c242bb9e77527abaf0b9acf9818a2a35144e4b32b20dc6", - "sha512": "3d035f94e1b14ac84627a28afdbed9a34861fb84239f76d73aa1a99f52262bfd95c4fa0ee71f1fd7e3bfb998d89cd5e0f0eafcff9fa7fa87c6e23484fc1e0cec" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://ctfe.sigstage.dev/2022-2", - "usage": "CTFE" - } - } - }, - "fulcio.crt.pem": { - "length": 741, - "hashes": { - "sha256": "0e6b0442485ad552bea5f62f11c29e2acfda35307d7538430b4cc1dbef49bff1", - "sha512": "c69ae618883a0c89c282c0943a1ad0c16b0a7788f74e47a1adefc631dac48a0c4449d8c3de7455ae7d772e43c4a87e341f180b0614a46a86006969f8a7b84532" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://fulcio.sigstage.dev", - "usage": "Fulcio" - } - } - }, - "fulcio_intermediate.crt.pem": { - "length": 790, - "hashes": { - "sha256": "782868913fe13c385105ddf33e827191386f58da40a931f2075a7e27b1b6ac7b", - "sha512": "90659875a02f73d1026055427c6d857c556e410e23748ff88aeb493227610fd2f5fbdd95ef2a21565f91438dfb3e073f50c4c9dd06f9a601b5d9b064d5cb60b4" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://fulcio.sigstage.dev", - "usage": "Fulcio" - } - } - }, - "rekor.pub": { - "length": 178, - "hashes": { - "sha256": "1d80b8f72505a43e65e6e125247cd508f61b459dc457c1d1bcb78d96e1760959", - "sha512": "09ab08698a67354a95d3b8897d9ce7eaef05f06f5ed5f0202d79c228579858ecc5816b7e1b7cc6786abe7d6aaa758e1fcb05900cb749235186c3bf9522d6d7ce" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://rekor.sigstage.dev", - "usage": "Rekor" - } - } - }, - "trusted_root.json": { - "length": 4521, - "hashes": { - "sha256": "6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b", - "sha512": "fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963" - } - } - }, - "delegations": { - "keys": { - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { - "keytype": "ecdsa-sha2-nistp256", - "scheme": "ecdsa-sha2-nistp256", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" - } - } - }, - "roles": [ - { - "name": "registry.npmjs.org", - "keyids": [ - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" - ], - "threshold": 1, - "terminating": true, - "paths": [ - "registry.npmjs.org/*" - ] - } - ] - } - }, - "signatures": [ - { - "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", - "sig": "304502210090b089087d1b17b2517c464b7774d76d3ea558ffca874eed63ccbee8f6bc3b76022022b56f551bcd0ac8a9c35cd0724ac5b00b4984544cbf812f47f276a9b48db8db" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/registry.npmjs.org.json b/tests/data/repository/registry.npmjs.org.json deleted file mode 100644 index d53f15267b..0000000000 --- a/tests/data/repository/registry.npmjs.org.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "signed": { - "_type": "targets", - "spec_version": "1.0", - "version": 2, - "expires": "2028-09-29T21:10:55Z", - "targets": { - "registry.npmjs.org/keys.json": { - "length": 1017, - "hashes": { - "sha256": "7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426", - "sha512": "881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699" - } - } - } - }, - "signatures": [ - { - "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", - "sig": "3045022057b9fc8afd9feaf45cf3173d3420fdcd6b68c22e4ef7b47e80a6887e1f20246c0221009f39c42fac630ab354c5197288c9a82ab6d46a59b423f81fff719da57cff16ab" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/root.json b/tests/data/repository/root.json deleted file mode 100644 index f848d7d846..0000000000 --- a/tests/data/repository/root.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "signed": { - "_type": "root", - "spec_version": "1.0", - "version": 2, - "expires": "2028-09-29T21:10:11Z", - "keys": { - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { - "keytype": "ecdsa-sha2-nistp256", - "scheme": "ecdsa-sha2-nistp256", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" - } - }, - "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda": { - "keytype": "ecdsa-sha2-nistp256", - "scheme": "ecdsa-sha2-nistp256", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL3vL/VeaH6nBbo4rekyO4cc/QthS\n+nlyJXCXSnyIMAtLmVTa8Pf0qG6YIVaR0TmLkyk9YoSVsZakxuMTuaEwrg==\n-----END PUBLIC KEY-----\n" - } - } - }, - "roles": { - "root": { - "keyids": [ - "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" - ], - "threshold": 1 - }, - "snapshot": { - "keyids": [ - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" - ], - "threshold": 1 - }, - "targets": { - "keyids": [ - "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda" - ], - "threshold": 1 - }, - "timestamp": { - "keyids": [ - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" - ], - "threshold": 1 - } - }, - "consistent_snapshot": true - }, - "signatures": [ - { - "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", - "sig": "3045022057bbd23dd9f69f8280c5e5d2b0a0b1ace98d6d8efa0f59ef0a3190188f6e2c89022100b39e6c24091c4271d2b8b4cfa75e6120638b276fbffddda8da5bca1778c8f08c" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/snapshot.json b/tests/data/repository/snapshot.json deleted file mode 100644 index 6c1e4dd147..0000000000 --- a/tests/data/repository/snapshot.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "signed": { - "_type": "snapshot", - "spec_version": "1.0", - "version": 2, - "expires": "2028-04-19T21:11:16Z", - "meta": { - "registry.npmjs.org.json": { - "length": 715, - "hashes": { - "sha256": "4dc55b2b468b0d1c9629c457c5cfce2cc1c330c59c5a7cf71cb7549f1ef76f1d", - "sha512": "278f4b6112db9d4bd9366e1717cf710ad7eacf44605fd4f894c3374fc5dff850a1a03c24c4a885d050a4ac1a86fa6929537fae12d8c2864c8e0c239b382d5556" - }, - "version": 2 - }, - "targets.json": { - "length": 4120, - "hashes": { - "sha256": "095d093de09350cec021828f49361688b5dd692486ad7bfb03d4150b3269ef8a", - "sha512": "97b9c75f49fb41eaf2f33c5f58b125febc3bbecd4c97f6edd0901423a231e4d0c5760d4780bc180a364d7198b5e0710f07ee0abf84dcd163fe3348d6bce26fab" - }, - "version": 2 - } - } - }, - "signatures": [ - { - "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", - "sig": "3044022013bf1032cf0a37d9f88ab9d33d0abb7a932efd95fadbc354fc21a807c7be29ef0220677e651c3e67e0728591faa20c5a09b8a16953038c3ceeffb7d9cfec766b3245" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/targets.json b/tests/data/repository/targets.json deleted file mode 100644 index dea42487f0..0000000000 --- a/tests/data/repository/targets.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "signed": { - "_type": "targets", - "spec_version": "1.0", - "version": 2, - "expires": "2028-09-29T21:10:55Z", - "targets": { - "ctfe.pub": { - "length": 775, - "hashes": { - "sha256": "bd7a6812a1f239dfddbbb19d36c7423d21510da56d466ba5018401959cd66037", - "sha512": "b861189e48df51186a39612230fba6b02af951f7b35ad9375e8ca182d0e085d470e26d69f7cd4d7450a0f223991e8e5a4ddf8f1968caa15255de8e37035af43a" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://ctfe.sigstage.dev/test", - "usage": "CTFE" - } - } - }, - "ctfe_2022.pub": { - "length": 178, - "hashes": { - "sha256": "910d899c7763563095a0fe684c8477573fedc19a78586de6ecfbfd8f289f5423", - "sha512": "ab975a75600fc366a837536d0dcba841b755552d21bb114498ff8ac9d2403f76643f5b91269bce5d124a365514719a3edee9dcc2b046cb173f51af659911fcd3" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://ctfe.sigstage.dev/2022", - "usage": "CTFE" - } - } - }, - "ctfe_2022_2.pub": { - "length": 178, - "hashes": { - "sha256": "7054b4f15f969daca1c242bb9e77527abaf0b9acf9818a2a35144e4b32b20dc6", - "sha512": "3d035f94e1b14ac84627a28afdbed9a34861fb84239f76d73aa1a99f52262bfd95c4fa0ee71f1fd7e3bfb998d89cd5e0f0eafcff9fa7fa87c6e23484fc1e0cec" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://ctfe.sigstage.dev/2022-2", - "usage": "CTFE" - } - } - }, - "fulcio.crt.pem": { - "length": 741, - "hashes": { - "sha256": "0e6b0442485ad552bea5f62f11c29e2acfda35307d7538430b4cc1dbef49bff1", - "sha512": "c69ae618883a0c89c282c0943a1ad0c16b0a7788f74e47a1adefc631dac48a0c4449d8c3de7455ae7d772e43c4a87e341f180b0614a46a86006969f8a7b84532" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://fulcio.sigstage.dev", - "usage": "Fulcio" - } - } - }, - "fulcio_intermediate.crt.pem": { - "length": 790, - "hashes": { - "sha256": "782868913fe13c385105ddf33e827191386f58da40a931f2075a7e27b1b6ac7b", - "sha512": "90659875a02f73d1026055427c6d857c556e410e23748ff88aeb493227610fd2f5fbdd95ef2a21565f91438dfb3e073f50c4c9dd06f9a601b5d9b064d5cb60b4" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://fulcio.sigstage.dev", - "usage": "Fulcio" - } - } - }, - "rekor.pub": { - "length": 178, - "hashes": { - "sha256": "1d80b8f72505a43e65e6e125247cd508f61b459dc457c1d1bcb78d96e1760959", - "sha512": "09ab08698a67354a95d3b8897d9ce7eaef05f06f5ed5f0202d79c228579858ecc5816b7e1b7cc6786abe7d6aaa758e1fcb05900cb749235186c3bf9522d6d7ce" - }, - "custom": { - "sigstore": { - "status": "Active", - "uri": "https://rekor.sigstage.dev", - "usage": "Rekor" - } - } - }, - "trusted_root.json": { - "length": 4521, - "hashes": { - "sha256": "6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b", - "sha512": "fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963" - } - } - }, - "delegations": { - "keys": { - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { - "keytype": "ecdsa-sha2-nistp256", - "scheme": "ecdsa-sha2-nistp256", - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keyval": { - "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" - } - } - }, - "roles": [ - { - "name": "registry.npmjs.org", - "keyids": [ - "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" - ], - "threshold": 1, - "terminating": true, - "paths": [ - "registry.npmjs.org/*" - ] - } - ] - } - }, - "signatures": [ - { - "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", - "sig": "304502210090b089087d1b17b2517c464b7774d76d3ea558ffca874eed63ccbee8f6bc3b76022022b56f551bcd0ac8a9c35cd0724ac5b00b4984544cbf812f47f276a9b48db8db" - } - ] -} \ No newline at end of file diff --git a/tests/data/repository/targets/6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b.trusted_root.json b/tests/data/repository/targets/6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b.trusted_root.json deleted file mode 100644 index 6a1c1f5a40..0000000000 --- a/tests/data/repository/targets/6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b.trusted_root.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", - "tlogs": [ - { - "baseUrl": "https://rekor.sigstage.dev", - "hashAlgorithm": "SHA2_256", - "publicKey": { - "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==", - "keyDetails": "PKIX_ECDSA_P256_SHA_256", - "validFor": { - "start": "2021-01-12T11:53:27.000Z" - } - }, - "logId": { - "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" - } - } - ], - "certificateAuthorities": [ - { - "subject": { - "organization": "sigstore.dev", - "commonName": "sigstore" - }, - "uri": "https://fulcio.sigstage.dev", - "certChain": { - "certificates": [ - { - "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=" - }, - { - "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==" - } - ] - }, - "validFor": { - "start": "2022-03-25T16:50:46.000Z" - } - } - ], - "ctlogs": [ - { - "baseUrl": "https://ctfe.sigstage.dev/test", - "hashAlgorithm": "SHA2_256", - "publicKey": { - "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==", - "keyDetails": "PKCS1_RSA_PKCS1V5", - "validFor": { - "start": "2021-03-14T00:00:00.000Z" - } - }, - "logId": { - "keyId": "s9AOb93xWxr+a4ztxJnxxJCX7VZ0V3IF4jTu/OoL84A=" - } - }, - { - "baseUrl": "https://ctfe.sigstage.dev/2022", - "hashAlgorithm": "SHA2_256", - "publicKey": { - "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==", - "keyDetails": "PKIX_ECDSA_P256_SHA_256", - "validFor": { - "start": "2022-07-01T00:00:00.000Z" - } - }, - "logId": { - "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=" - } - }, - { - "baseUrl": "https://ctfe.sigstage.dev/2022-2", - "hashAlgorithm": "SHA2_256", - "publicKey": { - "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==", - "keyDetails": "PKIX_ECDSA_P256_SHA_256", - "validFor": { - "start": "2022-07-01T00:00:00.000Z" - } - }, - "logId": { - "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=" - } - } - ], - "timestampAuthorities": [] -} diff --git a/tests/data/repository/targets/fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963.trusted_root.json b/tests/data/repository/targets/fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963.trusted_root.json deleted file mode 100644 index 6a1c1f5a40..0000000000 --- a/tests/data/repository/targets/fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963.trusted_root.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", - "tlogs": [ - { - "baseUrl": "https://rekor.sigstage.dev", - "hashAlgorithm": "SHA2_256", - "publicKey": { - "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==", - "keyDetails": "PKIX_ECDSA_P256_SHA_256", - "validFor": { - "start": "2021-01-12T11:53:27.000Z" - } - }, - "logId": { - "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" - } - } - ], - "certificateAuthorities": [ - { - "subject": { - "organization": "sigstore.dev", - "commonName": "sigstore" - }, - "uri": "https://fulcio.sigstage.dev", - "certChain": { - "certificates": [ - { - "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=" - }, - { - "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==" - } - ] - }, - "validFor": { - "start": "2022-03-25T16:50:46.000Z" - } - } - ], - "ctlogs": [ - { - "baseUrl": "https://ctfe.sigstage.dev/test", - "hashAlgorithm": "SHA2_256", - "publicKey": { - "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==", - "keyDetails": "PKCS1_RSA_PKCS1V5", - "validFor": { - "start": "2021-03-14T00:00:00.000Z" - } - }, - "logId": { - "keyId": "s9AOb93xWxr+a4ztxJnxxJCX7VZ0V3IF4jTu/OoL84A=" - } - }, - { - "baseUrl": "https://ctfe.sigstage.dev/2022", - "hashAlgorithm": "SHA2_256", - "publicKey": { - "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==", - "keyDetails": "PKIX_ECDSA_P256_SHA_256", - "validFor": { - "start": "2022-07-01T00:00:00.000Z" - } - }, - "logId": { - "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=" - } - }, - { - "baseUrl": "https://ctfe.sigstage.dev/2022-2", - "hashAlgorithm": "SHA2_256", - "publicKey": { - "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==", - "keyDetails": "PKIX_ECDSA_P256_SHA_256", - "validFor": { - "start": "2022-07-01T00:00:00.000Z" - } - }, - "logId": { - "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=" - } - } - ], - "timestampAuthorities": [] -} diff --git a/tests/data/repository/targets/registry.npmjs.org/7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426.keys.json b/tests/data/repository/targets/registry.npmjs.org/7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426.keys.json deleted file mode 100644 index f5667a5f0e..0000000000 --- a/tests/data/repository/targets/registry.npmjs.org/7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426.keys.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "keys": [ - { - "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", - "keyUsage": "npm:signatures", - "publicKey": { - "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", - "keyDetails": "PKIX_ECDSA_P256_SHA_256", - "validFor": { - "start": "1999-01-01T00:00:00.000Z" - } - } - }, - { - "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", - "keyUsage": "npm:attestations", - "publicKey": { - "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", - "keyDetails": "PKIX_ECDSA_P256_SHA_256", - "validFor": { - "start": "2022-12-01T00:00:00.000Z" - } - } - } - ] -} diff --git a/tests/data/repository/targets/registry.npmjs.org/881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699.keys.json b/tests/data/repository/targets/registry.npmjs.org/881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699.keys.json deleted file mode 100644 index f5667a5f0e..0000000000 --- a/tests/data/repository/targets/registry.npmjs.org/881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699.keys.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "keys": [ - { - "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", - "keyUsage": "npm:signatures", - "publicKey": { - "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", - "keyDetails": "PKIX_ECDSA_P256_SHA_256", - "validFor": { - "start": "1999-01-01T00:00:00.000Z" - } - } - }, - { - "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", - "keyUsage": "npm:attestations", - "publicKey": { - "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", - "keyDetails": "PKIX_ECDSA_P256_SHA_256", - "validFor": { - "start": "2022-12-01T00:00:00.000Z" - } - } - } - ] -} diff --git a/tests/data/repository/timestamp.json b/tests/data/repository/timestamp.json deleted file mode 100644 index 4b4a4dec2c..0000000000 --- a/tests/data/repository/timestamp.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "signed": { - "_type": "timestamp", - "spec_version": "1.0", - "version": 2, - "expires": "2028-04-12T21:11:28Z", - "meta": { - "snapshot.json": { - "length": 1039, - "hashes": { - "sha256": "b480856ab72c80fe10902ffac69ec10340e827e02b2bd114d6f141de910a96c5", - "sha512": "da06f65c1ee242d63820ba646fb1b4037fe355460309d89f98a923d1d009e7d46f11d4272a0d8e07829734baea655f7692d8c23383d6044b4f72263a4dbf3057" - }, - "version": 2 - } - } - }, - "signatures": [ - { - "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", - "sig": "3044022040e243b1bc8edb798df66803c2460471a4129704421d59f55c825dc549493f840220267e4684875d4803ae0948140af32fc9f560453efb84d9728ee66619e8767d8c" - } - ] -} \ No newline at end of file From 1e737836204e868e25f8ae58a20a00f22c6627cc Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Thu, 11 Apr 2024 18:05:33 -0500 Subject: [PATCH 26/27] `sign`, `verify`: reparent to `bundle`, doc fixups Signed-off-by: Andrew Pan --- src/bundle/mod.rs | 110 ++----------------- src/bundle/models.rs | 107 +++++++++++++++++++ src/{ => bundle}/sign.rs | 114 ++++++++++---------- src/{ => bundle}/verify/mod.rs | 6 +- src/{ => bundle}/verify/models.rs | 2 +- src/{ => bundle}/verify/policy.rs | 2 +- src/{ => bundle}/verify/verifier.rs | 158 ++++++++++++++-------------- src/cosign/client_builder.rs | 2 +- src/fulcio/mod.rs | 2 +- src/lib.rs | 6 -- src/trust/sigstore/mod.rs | 18 +--- tests/conformance/conformance.rs | 6 +- 12 files changed, 265 insertions(+), 268 deletions(-) create mode 100644 src/bundle/models.rs rename src/{ => bundle}/sign.rs (76%) rename src/{ => bundle}/verify/mod.rs (86%) rename src/{ => bundle}/verify/models.rs (99%) rename src/{ => bundle}/verify/policy.rs (99%) rename src/{ => bundle}/verify/verifier.rs (72%) diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs index c8c8ea64b0..61ad0c48b3 100644 --- a/src/bundle/mod.rs +++ b/src/bundle/mod.rs @@ -12,112 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Common types for Sigstore Bundle support. +//! Sigstore bundle support. -use std::fmt::Display; -use std::str::FromStr; - -use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; -use json_syntax::Print; pub use sigstore_protobuf_specs::dev::sigstore::bundle::v1::Bundle; -use sigstore_protobuf_specs::dev::sigstore::{ - common::v1::LogId, - rekor::v1::{Checkpoint, InclusionPromise, InclusionProof, KindVersion, TransparencyLogEntry}, -}; - -use crate::rekor::models::{ - log_entry::InclusionProof as RekorInclusionProof, LogEntry as RekorLogEntry, -}; - -// Known Sigstore bundle media types. -#[derive(Clone, Copy, Debug)] -pub enum Version { - Bundle0_1, - Bundle0_2, -} - -impl Display for Version { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match &self { - Version::Bundle0_1 => "application/vnd.dev.sigstore.bundle+json;version=0.1", - Version::Bundle0_2 => "application/vnd.dev.sigstore.bundle+json;version=0.2", - }) - } -} - -impl FromStr for Version { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "application/vnd.dev.sigstore.bundle+json;version=0.1" => Ok(Version::Bundle0_1), - "application/vnd.dev.sigstore.bundle+json;version=0.2" => Ok(Version::Bundle0_2), - _ => Err(()), - } - } -} - -#[inline] -fn decode_hex>(hex: S) -> Result, ()> { - hex::decode(hex.as_ref()).or(Err(())) -} - -impl TryFrom for InclusionProof { - type Error = (); - - fn try_from(value: RekorInclusionProof) -> Result { - let hashes = value - .hashes - .iter() - .map(decode_hex) - .collect::, _>>()?; - - Ok(InclusionProof { - checkpoint: Some(Checkpoint { - envelope: value.checkpoint, - }), - hashes, - log_index: value.log_index, - root_hash: decode_hex(value.root_hash)?, - tree_size: value.tree_size, - }) - } -} -/// Convert log entries returned from Rekor into Sigstore Bundle format entries. -impl TryFrom for TransparencyLogEntry { - type Error = (); +mod models; - fn try_from(value: RekorLogEntry) -> Result { - let canonicalized_body = { - let mut body = json_syntax::to_value(value.body).or(Err(()))?; - body.canonicalize(); - body.compact_print().to_string().into_bytes() - }; - let inclusion_promise = Some(InclusionPromise { - signed_entry_timestamp: base64 - .decode(value.verification.signed_entry_timestamp) - .or(Err(()))?, - }); - let inclusion_proof = value - .verification - .inclusion_proof - .map(|p| p.try_into()) - .transpose()?; +#[cfg(feature = "sign")] +pub mod sign; - Ok(TransparencyLogEntry { - canonicalized_body, - inclusion_promise, - inclusion_proof, - integrated_time: value.integrated_time, - kind_version: Some(KindVersion { - kind: "hashedrekord".to_owned(), - version: "0.0.1".to_owned(), - }), - log_id: Some(LogId { - key_id: decode_hex(value.log_i_d)?, - }), - log_index: value.log_index, - }) - } -} +#[cfg(feature = "verify")] +pub mod verify; diff --git a/src/bundle/models.rs b/src/bundle/models.rs new file mode 100644 index 0000000000..79d27933dc --- /dev/null +++ b/src/bundle/models.rs @@ -0,0 +1,107 @@ +use std::fmt::Display; +use std::str::FromStr; + +use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; +use json_syntax::Print; + +use sigstore_protobuf_specs::dev::sigstore::{ + common::v1::LogId, + rekor::v1::{Checkpoint, InclusionPromise, InclusionProof, KindVersion, TransparencyLogEntry}, +}; + +use crate::rekor::models::{ + log_entry::InclusionProof as RekorInclusionProof, LogEntry as RekorLogEntry, +}; + +// Known Sigstore bundle media types. +#[derive(Clone, Copy, Debug)] +pub enum Version { + Bundle0_1, + Bundle0_2, +} + +impl Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match &self { + Version::Bundle0_1 => "application/vnd.dev.sigstore.bundle+json;version=0.1", + Version::Bundle0_2 => "application/vnd.dev.sigstore.bundle+json;version=0.2", + }) + } +} + +impl FromStr for Version { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "application/vnd.dev.sigstore.bundle+json;version=0.1" => Ok(Version::Bundle0_1), + "application/vnd.dev.sigstore.bundle+json;version=0.2" => Ok(Version::Bundle0_2), + _ => Err(()), + } + } +} + +#[inline] +fn decode_hex>(hex: S) -> Result, ()> { + hex::decode(hex.as_ref()).or(Err(())) +} + +impl TryFrom for InclusionProof { + type Error = (); + + fn try_from(value: RekorInclusionProof) -> Result { + let hashes = value + .hashes + .iter() + .map(decode_hex) + .collect::, _>>()?; + + Ok(InclusionProof { + checkpoint: Some(Checkpoint { + envelope: value.checkpoint, + }), + hashes, + log_index: value.log_index, + root_hash: decode_hex(value.root_hash)?, + tree_size: value.tree_size, + }) + } +} + +/// Convert log entries returned from Rekor into Sigstore Bundle format entries. +impl TryFrom for TransparencyLogEntry { + type Error = (); + + fn try_from(value: RekorLogEntry) -> Result { + let canonicalized_body = { + let mut body = json_syntax::to_value(value.body).or(Err(()))?; + body.canonicalize(); + body.compact_print().to_string().into_bytes() + }; + let inclusion_promise = Some(InclusionPromise { + signed_entry_timestamp: base64 + .decode(value.verification.signed_entry_timestamp) + .or(Err(()))?, + }); + let inclusion_proof = value + .verification + .inclusion_proof + .map(|p| p.try_into()) + .transpose()?; + + Ok(TransparencyLogEntry { + canonicalized_body, + inclusion_promise, + inclusion_proof, + integrated_time: value.integrated_time, + kind_version: Some(KindVersion { + kind: "hashedrekord".to_owned(), + version: "0.0.1".to_owned(), + }), + log_id: Some(LogId { + key_id: decode_hex(value.log_i_d)?, + }), + log_index: value.log_index, + }) + } +} diff --git a/src/sign.rs b/src/bundle/sign.rs similarity index 76% rename from src/sign.rs rename to src/bundle/sign.rs index 30f75737c7..b60472c568 100644 --- a/src/sign.rs +++ b/src/bundle/sign.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Types for signing artifacts and producing Sigstore Bundles. +//! Types for signing artifacts and producing Sigstore bundles. use std::io::{self, Read}; use std::time::SystemTime; @@ -38,7 +38,7 @@ use x509_cert::attr::{AttributeTypeAndValue, AttributeValue}; use x509_cert::builder::{Builder, RequestBuilder as CertRequestBuilder}; use x509_cert::ext::pkix as x509_ext; -use crate::bundle::Version; +use crate::bundle::models::Version; use crate::errors::{Result as SigstoreResult, SigstoreError}; use crate::fulcio::oauth::OauthTokenProvider; use crate::fulcio::{self, FulcioClient, FULCIO_ROOT}; @@ -50,22 +50,22 @@ use crate::rekor::models::{hashedrekord, proposed_entry::ProposedEntry as Propos /// An asynchronous Sigstore signing session. /// /// Sessions hold a provided user identity and key materials tied to that identity. A single -/// session may be used to sign multiple items. For more information, see [`AsyncSigningSession::sign`](Self::sign). +/// session may be used to sign multiple items. For more information, see [`SigningSession::sign`]. /// -/// This signing session operates asynchronously. To construct a synchronous [SigningSession], -/// use [`SigningContext::signer()`]. -pub struct AsyncSigningSession<'ctx> { +/// This signing session operates asynchronously. To construct a synchronous [`blocking::SigningSession`], +/// use [`SigningContext::blocking_signer()`]. +pub struct SigningSession<'ctx> { context: &'ctx SigningContext, identity_token: IdentityToken, private_key: ecdsa::SigningKey, certs: fulcio::CertificateResponse, } -impl<'ctx> AsyncSigningSession<'ctx> { +impl<'ctx> SigningSession<'ctx> { async fn new( context: &'ctx SigningContext, identity_token: IdentityToken, - ) -> SigstoreResult> { + ) -> SigstoreResult> { let (private_key, certs) = Self::materials(&context.fulcio, &identity_token).await?; Ok(Self { context, @@ -176,7 +176,7 @@ impl<'ctx> AsyncSigningSession<'ctx> { } /// Signs for the input with the session's identity. If the identity is expired, - /// [SigstoreError::ExpiredSigningSession] is returned. + /// [`SigstoreError::ExpiredSigningSession`] is returned. pub async fn sign( &self, input: R, @@ -197,48 +197,52 @@ impl<'ctx> AsyncSigningSession<'ctx> { } } -/// A synchronous Sigstore signing session. -/// -/// Sessions hold a provided user identity and key materials tied to that identity. A single -/// session may be used to sign multiple items. For more information, see [`SigningSession::sign`](Self::sign). -/// -/// This signing session operates synchronously, thus it cannot be used in an asynchronous context. -/// To construct an asynchronous [SigningSession], use [`SigningContext::async_signer()`]. -pub struct SigningSession<'ctx> { - inner: AsyncSigningSession<'ctx>, - rt: tokio::runtime::Runtime, -} +pub mod blocking { + use super::{SigningSession as AsyncSigningSession, *}; -impl<'ctx> SigningSession<'ctx> { - fn new(ctx: &'ctx SigningContext, token: IdentityToken) -> SigstoreResult { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - let inner = rt.block_on(AsyncSigningSession::new(ctx, token))?; - Ok(Self { inner, rt }) - } - - /// Check if the session's identity token or key material is expired. + /// A synchronous Sigstore signing session. /// - /// If the session is expired, it cannot be used for signing operations, and a new session - /// must be created with a fresh identity token. - pub fn is_expired(&self) -> bool { - self.inner.is_expired() + /// Sessions hold a provided user identity and key materials tied to that identity. A single + /// session may be used to sign multiple items. For more information, see [`SigningSession::sign`]. + /// + /// This signing session operates synchronously, thus it cannot be used in an asynchronous context. + /// To construct an asynchronous [`SigningSession`], use [`SigningContext::signer()`]. + pub struct SigningSession<'ctx> { + inner: AsyncSigningSession<'ctx>, + rt: tokio::runtime::Runtime, } - /// Signs for the input with the session's identity. If the identity is expired, - /// [SigstoreError::ExpiredSigningSession] is returned. - pub fn sign(&self, mut input: R) -> SigstoreResult { - let mut hasher = Sha256::new(); - io::copy(&mut input, &mut hasher)?; - self.rt.block_on(self.inner.sign_digest(hasher)) + impl<'ctx> SigningSession<'ctx> { + pub(crate) fn new(ctx: &'ctx SigningContext, token: IdentityToken) -> SigstoreResult { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let inner = rt.block_on(AsyncSigningSession::new(ctx, token))?; + Ok(Self { inner, rt }) + } + + /// Check if the session's identity token or key material is expired. + /// + /// If the session is expired, it cannot be used for signing operations, and a new session + /// must be created with a fresh identity token. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// Signs for the input with the session's identity. If the identity is expired, + /// [`SigstoreError::ExpiredSigningSession`] is returned. + pub fn sign(&self, mut input: R) -> SigstoreResult { + let mut hasher = Sha256::new(); + io::copy(&mut input, &mut hasher)?; + self.rt.block_on(self.inner.sign_digest(hasher)) + } } } /// A Sigstore signing context. /// /// Contexts hold Fulcio (CA) and Rekor (CT) configurations which signing sessions can be -/// constructed against. Use [`SigningContext::production`](Self::production) to create a context against +/// constructed against. Use [`SigningContext::production`] to create a context against /// the public-good Sigstore infrastructure. pub struct SigningContext { fulcio: FulcioClient, @@ -246,7 +250,7 @@ pub struct SigningContext { } impl SigningContext { - /// Manually constructs a [SigningContext] from its constituent data. + /// Manually constructs a [`SigningContext`] from its constituent data. pub fn new(fulcio: FulcioClient, rekor_config: RekorConfiguration) -> Self { Self { fulcio, @@ -254,7 +258,7 @@ impl SigningContext { } } - /// Returns a [SigningContext] configured against the public-good production Sigstore + /// Returns a [`SigningContext`] configured against the public-good production Sigstore /// infrastructure. pub fn production() -> SigstoreResult { Ok(Self::new( @@ -266,19 +270,19 @@ impl SigningContext { )) } - /// Configures and returns an [AsyncSigningSession] with the held context. - pub async fn async_signer( - &self, - identity_token: IdentityToken, - ) -> SigstoreResult { - AsyncSigningSession::new(self, identity_token).await + /// Configures and returns a [`SigningSession`] with the held context. + pub async fn signer(&self, identity_token: IdentityToken) -> SigstoreResult { + SigningSession::new(self, identity_token).await } - /// Configures and returns a [SigningContext] with the held context. + /// Configures and returns a [`blocking::SigningSession`] with the held context. /// - /// Async contexts must use [`SigningContext::async_signer`](Self::async_signer). - pub fn signer(&self, identity_token: IdentityToken) -> SigstoreResult { - SigningSession::new(self, identity_token) + /// Async contexts must use [`SigningContext::signer`]. + pub fn blocking_signer( + &self, + identity_token: IdentityToken, + ) -> SigstoreResult { + blocking::SigningSession::new(self, identity_token) } } @@ -291,9 +295,9 @@ pub struct SigningArtifact { } impl SigningArtifact { - /// Consumes the signing artifact and produces a Sigstore [Bundle]. + /// Consumes the signing artifact and produces a Sigstore [`Bundle`]. /// - /// The resulting bundle can be serialized with [serde_json]. + /// The resulting bundle can be serialized with [`serde_json`]. pub fn to_bundle(self) -> Bundle { // NOTE: We explicitly only include the leaf certificate in the bundle's "chain" // here: the specs explicitly forbid the inclusion of the root certificate, diff --git a/src/verify/mod.rs b/src/bundle/verify/mod.rs similarity index 86% rename from src/verify/mod.rs rename to src/bundle/verify/mod.rs index 0504d10060..a6ba64330f 100644 --- a/src/verify/mod.rs +++ b/src/bundle/verify/mod.rs @@ -13,12 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Verifier for Sigstore bundles and associated types and policies. +//! Types for verifying Sigstore bundles with policies. + mod models; + pub use models::{VerificationError, VerificationResult}; pub mod policy; pub use policy::{PolicyError, VerificationPolicy}; mod verifier; -pub use verifier::{AsyncVerifier, Verifier}; +pub use verifier::*; diff --git a/src/verify/models.rs b/src/bundle/verify/models.rs similarity index 99% rename from src/verify/models.rs rename to src/bundle/verify/models.rs index 01fdc8b660..198e9c05ee 100644 --- a/src/verify/models.rs +++ b/src/bundle/verify/models.rs @@ -16,7 +16,7 @@ use std::str::FromStr; use crate::{ - bundle::{Bundle, Version as BundleVersion}, + bundle::{models::Version as BundleVersion, Bundle}, crypto::certificate::{is_leaf, is_root_ca, CertificateValidationError}, rekor::models as rekor, }; diff --git a/src/verify/policy.rs b/src/bundle/verify/policy.rs similarity index 99% rename from src/verify/policy.rs rename to src/bundle/verify/policy.rs index c6c1806df8..267badc042 100644 --- a/src/verify/policy.rs +++ b/src/bundle/verify/policy.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Verifiers for certificate metadata. +//! Verification constraints for certificate metadata. //! //! diff --git a/src/verify/verifier.rs b/src/bundle/verify/verifier.rs similarity index 72% rename from src/verify/verifier.rs rename to src/bundle/verify/verifier.rs index ac4d4f62b3..0f4a4a5526 100644 --- a/src/verify/verifier.rs +++ b/src/bundle/verify/verifier.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Verifiers: async and blocking. + use std::io::{self, Read}; use sha2::{Digest, Sha256}; @@ -26,30 +28,30 @@ use crate::{ errors::Result as SigstoreResult, rekor::apis::configuration::Configuration as RekorConfiguration, trust::TrustRoot, - verify::{ - models::{CertificateErrorKind, SignatureErrorKind}, - VerificationError, - }, }; #[cfg(feature = "sigstore-trust-root")] use crate::trust::sigstore::SigstoreTrustRoot; -use super::{models::CheckedBundle, policy::VerificationPolicy, VerificationResult}; +use super::{ + models::{CertificateErrorKind, CheckedBundle, SignatureErrorKind}, + policy::VerificationPolicy, + VerificationError, VerificationResult, +}; /// An asynchronous Sigstore verifier. /// /// For synchronous usage, see [`Verifier`]. -pub struct AsyncVerifier { +pub struct Verifier { #[allow(dead_code)] rekor_config: RekorConfiguration, cert_pool: CertificatePool, } -impl AsyncVerifier { - /// Constructs an [`AsyncVerifier`]. +impl Verifier { + /// Constructs a [`Verifier`]. /// - /// For verifications against the public-good trust root, use [`AsyncVerifier::production`]. + /// For verifications against the public-good trust root, use [`Verifier::production()`]. pub fn new( rekor_config: RekorConfiguration, trust_repo: R, @@ -201,87 +203,89 @@ impl AsyncVerifier { } } -impl AsyncVerifier { - /// Constructs an [`AsyncVerifier`] against the public-good trust root. +impl Verifier { + /// Constructs an [`Verifier`] against the public-good trust root. #[cfg(feature = "sigstore-trust-root")] - pub async fn production() -> SigstoreResult { + pub async fn production() -> SigstoreResult { let updater = SigstoreTrustRoot::new(None).await?; - AsyncVerifier::new(Default::default(), updater) + Verifier::new(Default::default(), updater) } } -/// A synchronous Sigstore verifier. -/// -/// Async callers must use [`AsyncVerifier`]. Async usage of [`Verifier`] will result in a deadlock. -pub struct Verifier { - inner: AsyncVerifier, - rt: tokio::runtime::Runtime, -} +pub mod blocking { + use super::{Verifier as AsyncVerifier, *}; -impl Verifier { - /// Constructs a synchronous Sigstore verifier. - /// - /// For verifications against the public-good trust root, use [`Verifier::production`]. - pub fn new( - rekor_config: RekorConfiguration, - trust_repo: R, - ) -> SigstoreResult { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - let inner = AsyncVerifier::new(rekor_config, trust_repo)?; - - Ok(Self { rt, inner }) + /// A synchronous Sigstore verifier. + pub struct Verifier { + inner: AsyncVerifier, + rt: tokio::runtime::Runtime, } - /// Verifies an input digest against the given Sigstore Bundle, ensuring conformance to the - /// provided [`VerificationPolicy`]. - pub fn verify_digest

( - &self, - input_digest: Sha256, - bundle: Bundle, - policy: &P, - offline: bool, - ) -> VerificationResult - where - P: VerificationPolicy, - { - self.rt.block_on( - self.inner - .verify_digest(input_digest, bundle, policy, offline), - ) - } + impl Verifier { + /// Constructs a synchronous Sigstore verifier. + /// + /// For verifications against the public-good trust root, use [`Verifier::production()`]. + pub fn new( + rekor_config: RekorConfiguration, + trust_repo: R, + ) -> SigstoreResult { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let inner = AsyncVerifier::new(rekor_config, trust_repo)?; + + Ok(Self { rt, inner }) + } - /// Verifies an input against the given Sigstore Bundle, ensuring conformance to the provided - /// [`VerificationPolicy`]. - pub fn verify( - &self, - mut input: R, - bundle: Bundle, - policy: &P, - offline: bool, - ) -> VerificationResult - where - R: Read, - P: VerificationPolicy, - { - let mut hasher = Sha256::new(); - io::copy(&mut input, &mut hasher).map_err(VerificationError::Input)?; + /// Verifies an input digest against the given Sigstore Bundle, ensuring conformance to the + /// provided [`VerificationPolicy`]. + pub fn verify_digest

( + &self, + input_digest: Sha256, + bundle: Bundle, + policy: &P, + offline: bool, + ) -> VerificationResult + where + P: VerificationPolicy, + { + self.rt.block_on( + self.inner + .verify_digest(input_digest, bundle, policy, offline), + ) + } - self.verify_digest(hasher, bundle, policy, offline) + /// Verifies an input against the given Sigstore Bundle, ensuring conformance to the provided + /// [`VerificationPolicy`]. + pub fn verify( + &self, + mut input: R, + bundle: Bundle, + policy: &P, + offline: bool, + ) -> VerificationResult + where + R: Read, + P: VerificationPolicy, + { + let mut hasher = Sha256::new(); + io::copy(&mut input, &mut hasher).map_err(VerificationError::Input)?; + + self.verify_digest(hasher, bundle, policy, offline) + } } -} -impl Verifier { - /// Constructs a synchronous [`Verifier`] against the public-good trust root. - #[cfg(feature = "sigstore-trust-root")] - pub fn production() -> SigstoreResult { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - let inner = rt.block_on(AsyncVerifier::production())?; + impl Verifier { + /// Constructs a synchronous [`Verifier`] against the public-good trust root. + #[cfg(feature = "sigstore-trust-root")] + pub fn production() -> SigstoreResult { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let inner = rt.block_on(AsyncVerifier::production())?; - Ok(Verifier { inner, rt }) + Ok(Verifier { inner, rt }) + } } } diff --git a/src/cosign/client_builder.rs b/src/cosign/client_builder.rs index 0ab7664728..1a688d9139 100644 --- a/src/cosign/client_builder.rs +++ b/src/cosign/client_builder.rs @@ -28,7 +28,7 @@ use crate::trust::TrustRoot; /// ## Rekor integration /// /// Rekor integration can be enabled by specifying Rekor's public key. -/// This can be provided via a [`crate::sigstore::ManualTrustRoot`]. +/// This can be provided via a [`crate::trust::ManualTrustRoot`]. /// /// > Note well: the [`sigstore`](crate::sigstore) module provides helper structs and methods /// > to obtain this data from the official TUF repository of the Sigstore project. diff --git a/src/fulcio/mod.rs b/src/fulcio/mod.rs index 13fd6c945e..9166fa0a1d 100644 --- a/src/fulcio/mod.rs +++ b/src/fulcio/mod.rs @@ -203,7 +203,7 @@ impl FulcioClient { /// /// TODO(tnytown): This (and other API clients) should be autogenerated. See sigstore-rs#209. /// - /// https://github.com/sigstore/fulcio/blob/main/fulcio.proto + /// /// /// Additionally, it might not be reasonable to expect callers to correctly construct and pass /// in an X509 CSR. diff --git a/src/lib.rs b/src/lib.rs index 1fee312ca5..fa336c1029 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -284,9 +284,3 @@ pub mod rekor; #[cfg(any(feature = "sign", feature = "verify"))] pub mod bundle; - -#[cfg(feature = "verify")] -pub mod verify; - -#[cfg(feature = "sign")] -pub mod sign; diff --git a/src/trust/sigstore/mod.rs b/src/trust/sigstore/mod.rs index 81954a9891..0b26ba2af8 100644 --- a/src/trust/sigstore/mod.rs +++ b/src/trust/sigstore/mod.rs @@ -20,22 +20,6 @@ //! //! These can later be given to [`cosign::ClientBuilder`](crate::cosign::ClientBuilder) //! to enable Fulcio and Rekor integrations. -//! -//! # Example -//! -//! The `SigstoreRootTrust` instance can be created via the [`SigstoreTrustRoot::prefetch`] -//! method. -//! -/// ```rust -/// # use sigstore::trust::sigstore::SigstoreTrustRoot; -/// # use sigstore::errors::Result; -/// # #[tokio::main] -/// # async fn main() -> std::result::Result<(), anyhow::Error> { -/// let repo: Result = SigstoreTrustRoot::new(None).await; -/// // Now, get Fulcio and Rekor trust roots with the returned `SigstoreRootTrust` -/// # Ok(()) -/// # } -/// ``` use futures_util::TryStreamExt; use sha2::{Digest, Sha256}; use std::path::PathBuf; @@ -67,7 +51,7 @@ impl SigstoreTrustRoot { checkout_dir: Option, ) -> Result { let trusted_root = { - let data = Self::fetch_target(&repository, &checkout_dir, "trusted_root.json").await?; + let data = Self::fetch_target(repository, &checkout_dir, "trusted_root.json").await?; serde_json::from_slice(&data[..])? }; diff --git a/tests/conformance/conformance.rs b/tests/conformance/conformance.rs index e8ab9d3697..cccc0fcf7c 100644 --- a/tests/conformance/conformance.rs +++ b/tests/conformance/conformance.rs @@ -20,9 +20,9 @@ use std::{fs, process::exit}; use clap::{Parser, Subcommand}; use sigstore::{ + bundle::sign::SigningContext, + bundle::verify::{blocking::Verifier, policy}, oauth::IdentityToken, - sign::SigningContext, - verify::{policy, Verifier}, }; #[derive(Parser, Debug)] @@ -140,7 +140,7 @@ fn sign_bundle(args: SignBundle) -> anyhow::Result<()> { let mut artifact = fs::File::open(artifact)?; let context = SigningContext::production()?; - let signer = context.signer(identity_token); + let signer = context.blocking_signer(identity_token); let signing_artifact = signer?.sign(&mut artifact)?; let bundle_data = signing_artifact.to_bundle(); From c018346ecfec67c393c91a6911f10aa3f54b5a22 Mon Sep 17 00:00:00 2001 From: Andrew Pan Date: Fri, 12 Apr 2024 15:07:22 -0500 Subject: [PATCH 27/27] trust/sigstore: rework tests Signed-off-by: Andrew Pan --- src/trust/sigstore/mod.rs | 96 ++++++++++++++------------------------- 1 file changed, 34 insertions(+), 62 deletions(-) diff --git a/src/trust/sigstore/mod.rs b/src/trust/sigstore/mod.rs index 0b26ba2af8..9ee9f02cce 100644 --- a/src/trust/sigstore/mod.rs +++ b/src/trust/sigstore/mod.rs @@ -22,7 +22,7 @@ //! to enable Fulcio and Rekor integrations. use futures_util::TryStreamExt; use sha2::{Digest, Sha256}; -use std::path::PathBuf; +use std::path::Path; use tokio_util::bytes::BytesMut; use sigstore_protobuf_specs::dev::sigstore::{ @@ -48,10 +48,10 @@ impl SigstoreTrustRoot { /// Constructs a new trust root from a [`tough::Repository`]. async fn from_tough( repository: &tough::Repository, - checkout_dir: Option, + checkout_dir: Option<&Path>, ) -> Result { let trusted_root = { - let data = Self::fetch_target(repository, &checkout_dir, "trusted_root.json").await?; + let data = Self::fetch_target(repository, checkout_dir, "trusted_root.json").await?; serde_json::from_slice(&data[..])? }; @@ -59,7 +59,7 @@ impl SigstoreTrustRoot { } /// Constructs a new trust root backed by the Sigstore Public Good Instance. - pub async fn new(cache_dir: Option) -> Result { + pub async fn new(cache_dir: Option<&Path>) -> Result { // These are statically defined and should always parse correctly. let metadata_base = url::Url::parse(constants::SIGSTORE_METADATA_BASE)?; let target_base = url::Url::parse(constants::SIGSTORE_TARGET_BASE)?; @@ -79,7 +79,7 @@ impl SigstoreTrustRoot { async fn fetch_target( repository: &tough::Repository, - checkout_dir: &Option, + checkout_dir: Option<&Path>, name: N, ) -> Result> where @@ -238,6 +238,7 @@ fn is_timerange_valid(range: Option<&TimeRange>, allow_expired: bool) -> bool { #[cfg(test)] mod tests { use super::*; + use rstest::{fixture, rstest}; use std::fs; use std::path::Path; use std::time::SystemTime; @@ -265,71 +266,42 @@ mod tests { ); } - macro_rules! impl_test { - ($name:ident, cache=$cache:literal $(,setup=$setup:expr)? $(,verify=$verify:expr)?) => { - #[test] - fn $name() -> Result<()> { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; + #[fixture] + fn cache_dir() -> TempDir { + TempDir::new().expect("cannot create temp cache dir") + } - let cache_dir = if $cache { - let tmp = TempDir::new().expect("cannot create temp cache dir"); - Some(tmp.into_path()) - } else { - None - }; - let cache_dir_borrowed = cache_dir.as_ref().map(|p| p.as_path()); + async fn trust_root(cache: Option<&Path>) -> SigstoreTrustRoot { + SigstoreTrustRoot::new(cache) + .await + .expect("failed to construct SigstoreTrustRoot") + } - $( - $setup(cache_dir_borrowed); - )? + #[rstest] + #[tokio::test] + async fn trust_root_fetch(#[values(None, Some(cache_dir()))] cache: Option) { + let cache = cache.as_ref().map(|t| t.path()); + let root = trust_root(cache).await; - let root = rt - .block_on(SigstoreTrustRoot::new(cache_dir.clone())) - .expect("failed to create trust root"); + verify(&root, cache); + } - verify(&root, cache_dir_borrowed); + #[rstest] + #[tokio::test] + async fn trust_root_outdated(cache_dir: TempDir) { + let trusted_root_path = cache_dir.path().join("trusted_root.json"); + let outdated_data = b"fake trusted root"; + fs::write(&trusted_root_path, outdated_data) + .expect("failed to write to trusted root cache"); - $($verify(&root, cache_dir_borrowed);)? + let cache = Some(cache_dir.path()); + let root = trust_root(cache).await; + verify(&root, cache); - Ok(()) - } - } + let data = fs::read(&trusted_root_path).expect("failed to read from trusted root cache"); + assert_ne!(data, outdated_data, "TUF cache was not properly updated"); } - impl_test!(no_local_cache, cache = false); - impl_test!( - current_local_cache, - cache = true, - verify = |_, cache_dir: Option<&Path>| { - assert!( - cache_dir.unwrap().join("trusted_root.json").exists(), - "TUF cache was requested but not used" - ); - } - ); - impl_test!( - outdated_local_cache, - cache = true, - setup = |cache_dir: Option<&Path>| { - fs::write( - cache_dir.unwrap().join("trusted_root.json"), - b"fake trusted root", - ) - .expect("cannot write file to cache dir"); - }, - verify = |_, cache_dir: Option<&Path>| { - let data = fs::read(cache_dir.unwrap().join("trusted_root.json")) - .expect("cannot read trusted_root.json from cache"); - - assert_ne!( - data, b"fake trusted root", - "TUF cache was not properly updated" - ); - } - ); - #[test] fn test_is_timerange_valid() { fn range_from(start: i64, end: i64) -> TimeRange {