diff --git a/Cargo.lock b/Cargo.lock index 53bb9a6..39bfccc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,9 +120,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.0" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" dependencies = [ "cc", "cmake", @@ -144,9 +144,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -181,9 +181,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -300,16 +300,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -1094,9 +1084,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libredox" @@ -1170,17 +1160,17 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "9d5d26952a508f321b4d3d2e80e78fc2603eaefcdf0c30783867f19586518bdc" dependencies = [ "libc", "log", "openssl", - "openssl-probe 0.1.6", + "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework", "security-framework-sys", "tempfile", ] @@ -1248,12 +1238,6 @@ dependencies = [ "syn", ] -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -1751,10 +1735,10 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.1", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] @@ -1773,7 +1757,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "jni", "log", @@ -1782,7 +1766,7 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework 3.5.1", + "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -1859,25 +1843,12 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" dependencies = [ "bitflags", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -1885,9 +1856,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" dependencies = [ "core-foundation-sys", "libc", @@ -2195,6 +2166,7 @@ dependencies = [ "serde_json", "sigstore-crypto", "sigstore-types", + "tempfile", "thiserror 2.0.18", "tokio", "tough", @@ -2348,9 +2320,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -2379,12 +2351,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2673,9 +2645,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -3440,6 +3412,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/README.md b/README.md index cf3fc1e..2921150 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ sigstore-sign = "0.1" use sigstore_verify::{Verifier, VerificationPolicy}; use sigstore_trust_root::TrustedRoot; -// Load the trusted root (contains Fulcio CA, Rekor keys, etc.) -let root = TrustedRoot::production()?; +// Load the trusted root via TUF (recommended - ensures up-to-date trust material) +let root = TrustedRoot::production().await?; let verifier = Verifier::new(&root); // Parse the bundle (contains signature, certificate, transparency log entry) diff --git a/crates/sigstore-conformance/src/main.rs b/crates/sigstore-conformance/src/main.rs index 98379f4..4c7dcf1 100644 --- a/crates/sigstore-conformance/src/main.rs +++ b/crates/sigstore-conformance/src/main.rs @@ -7,7 +7,7 @@ use sigstore_oidc::IdentityToken; use sigstore_sign::{SigningConfig as SignerSigningConfig, SigningContext}; -use sigstore_trust_root::{SigningConfig as TufSigningConfig, TrustedRoot}; +use sigstore_trust_root::{SigningConfig as TufSigningConfig, TrustedRoot, SIGSTORE_PRODUCTION_TRUSTED_ROOT}; use sigstore_types::{Bundle, Sha256Hash, SignatureContent}; use sigstore_verify::{verify, VerificationPolicy}; @@ -224,8 +224,9 @@ fn verify_bundle(args: &[String]) -> Result<(), Box> { let trusted_root = if let Some(root_path) = trusted_root_path { TrustedRoot::from_file(&root_path)? } else { - // Default to production trusted root when not specified - TrustedRoot::production()? + // Default to embedded production trusted root when not specified + // For better freshness, use TrustedRoot::production().await in async contexts + TrustedRoot::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT)? }; // Load bundle diff --git a/crates/sigstore-sign/src/sign.rs b/crates/sigstore-sign/src/sign.rs index 9807081..db736e6 100644 --- a/crates/sigstore-sign/src/sign.rs +++ b/crates/sigstore-sign/src/sign.rs @@ -10,7 +10,10 @@ use sigstore_oidc::IdentityToken; use sigstore_rekor::{ DsseEntry, DsseEntryV2, HashedRekord, HashedRekordV2, RekorApiVersion, RekorClient, }; -use sigstore_trust_root::SigningConfig as TufSigningConfig; +use sigstore_trust_root::{ + SigningConfig as TufSigningConfig, SIGSTORE_PRODUCTION_SIGNING_CONFIG, + SIGSTORE_STAGING_SIGNING_CONFIG, +}; use sigstore_tsa::TimestampClient; use sigstore_types::{ Artifact, Bundle, DerCertificate, DsseEnvelope, DsseSignature, KeyId, PayloadBytes, Sha256Hash, @@ -49,15 +52,23 @@ impl SigningConfig { /// Create configuration for Sigstore public-good instance /// /// This uses the embedded signing config to get the best available endpoints. + /// For the most up-to-date endpoints, use `from_tuf_config()` with a TUF-fetched config. pub fn production() -> Self { - Self::from_tuf_config(&TufSigningConfig::production().expect("embedded config is valid")) + Self::from_tuf_config( + &TufSigningConfig::from_json(SIGSTORE_PRODUCTION_SIGNING_CONFIG) + .expect("embedded config is valid"), + ) } /// Create configuration for Sigstore staging instance /// /// This uses the embedded signing config to get the best available endpoints. + /// For the most up-to-date endpoints, use `from_tuf_config()` with a TUF-fetched config. pub fn staging() -> Self { - Self::from_tuf_config(&TufSigningConfig::staging().expect("embedded config is valid")) + Self::from_tuf_config( + &TufSigningConfig::from_json(SIGSTORE_STAGING_SIGNING_CONFIG) + .expect("embedded config is valid"), + ) } /// Create configuration from a TUF signing config diff --git a/crates/sigstore-trust-root/Cargo.toml b/crates/sigstore-trust-root/Cargo.toml index 64910b6..212f6c3 100644 --- a/crates/sigstore-trust-root/Cargo.toml +++ b/crates/sigstore-trust-root/Cargo.toml @@ -8,7 +8,7 @@ repository.workspace = true rust-version.workspace = true [features] -default = [] +default = ["tuf"] tuf = ["tough", "tokio", "futures", "directories", "url", "tracing"] [dependencies] @@ -43,5 +43,6 @@ url = { workspace = true, optional = true } tracing = { workspace = true, optional = true } [dev-dependencies] +tempfile = "3.25.0" # For testing tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/sigstore-trust-root/README.md b/crates/sigstore-trust-root/README.md index 9912f34..1202446 100644 --- a/crates/sigstore-trust-root/README.md +++ b/crates/sigstore-trust-root/README.md @@ -9,10 +9,11 @@ This crate handles parsing and management of Sigstore trusted root bundles. The ## Features - **Trusted root parsing**: Load and parse `trusted_root.json` files -- **Embedded roots**: Built-in production and staging trust anchors -- **TUF support**: Optional secure fetching via The Update Framework (requires `tuf` feature) +- **TUF support**: Secure fetching via The Update Framework (enabled by default) +- **Embedded roots**: Built-in production and staging trust anchors for offline use - **Key extraction**: Extract public keys and certificates for verification - **Validity periods**: Time-based key and certificate validity checking +- **Custom TUF repos**: Support for custom TUF repository URLs ## Trust Anchors @@ -26,22 +27,41 @@ This crate handles parsing and management of Sigstore trusted root bundles. The ## Usage ```rust -use sigstore_trust_root::TrustedRoot; +use sigstore_trust_root::{TrustedRoot, SIGSTORE_PRODUCTION_TRUSTED_ROOT}; -// Use embedded production root -let root = TrustedRoot::production()?; +// Fetch via TUF (recommended - ensures up-to-date trust material) +let root = TrustedRoot::production().await?; + +// Use embedded data (for offline use) +let root = TrustedRoot::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT)?; // Load from file let root = TrustedRoot::from_file("trusted_root.json")?; +``` -// With TUF feature: fetch securely -#[cfg(feature = "tuf")] -let root = TrustedRoot::from_tuf().await?; +### Custom TUF Repository + +```rust +use sigstore_trust_root::{TrustedRoot, TufConfig}; + +// Fetch from a custom TUF repository (e.g., for testing) +let config = TufConfig::custom( + "https://sigstore.github.io/root-signing/", + include_bytes!("path/to/root.json"), +); +let root = TrustedRoot::from_tuf(config).await?; ``` ## Cargo Features -- `tuf` - Enable TUF-based secure fetching of trusted roots +- `tuf` (default) - Enable TUF-based secure fetching of trusted roots + +To opt out of TUF support: + +```toml +[dependencies] +sigstore-trust-root = { version = "0.1", default-features = false } +``` ## Related Crates diff --git a/crates/sigstore-trust-root/src/lib.rs b/crates/sigstore-trust-root/src/lib.rs index 0683186..d883f38 100644 --- a/crates/sigstore-trust-root/src/lib.rs +++ b/crates/sigstore-trust-root/src/lib.rs @@ -21,35 +21,60 @@ //! //! # Features //! -//! - `tuf` - Enable TUF (The Update Framework) support for securely fetching -//! trusted roots from Sigstore's TUF repository. This adds async methods -//! like [`TrustedRoot::from_tuf()`] and [`TrustedRoot::from_tuf_staging()`]. +//! - `tuf` (default) - Enable TUF (The Update Framework) support for securely fetching +//! trusted roots from Sigstore's TUF repository. This is the recommended way to use +//! this crate for production as it ensures you always have up-to-date trust material. //! -//! # Example +//! # Example (Recommended) +//! +//! Fetch the latest trusted root and signing config via TUF protocol: //! //! ```no_run //! use sigstore_trust_root::{TrustedRoot, SigningConfig}; //! -//! // Load embedded production trusted root -//! let root = TrustedRoot::production().unwrap(); -//! -//! // Load embedded production signing config -//! let config = SigningConfig::production().unwrap(); +//! # async fn example() -> Result<(), sigstore_trust_root::Error> { +//! // Fetch via TUF protocol (secure, up-to-date) - RECOMMENDED +//! let root = TrustedRoot::production().await?; +//! let config = SigningConfig::production().await?; //! //! // Get the best Rekor endpoint (highest available version) //! if let Some(rekor) = config.get_rekor_url(None) { //! println!("Rekor URL: {} (v{})", rekor.url, rekor.major_api_version); //! } +//! # Ok(()) +//! # } +//! ``` +//! +//! # Example (Offline/Embedded) +//! +//! Use embedded data when offline or TUF is not available: +//! +//! ``` +//! use sigstore_trust_root::{ +//! TrustedRoot, SigningConfig, +//! SIGSTORE_PRODUCTION_TRUSTED_ROOT, SIGSTORE_PRODUCTION_SIGNING_CONFIG, +//! }; +//! +//! // Use embedded data (may be stale, but works offline) +//! let root = TrustedRoot::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT).unwrap(); +//! let config = SigningConfig::from_json(SIGSTORE_PRODUCTION_SIGNING_CONFIG).unwrap(); //! ``` //! -//! With the `tuf` feature enabled: +//! # Example (Custom TUF Repository) +//! +//! Fetch from a custom TUF repository (e.g., for testing): //! //! ```ignore -//! use sigstore_trust_root::{TrustedRoot, SigningConfig}; +//! use sigstore_trust_root::{TrustedRoot, TufConfig}; //! -//! // Fetch via TUF protocol (secure, up-to-date) -//! let root = TrustedRoot::from_tuf().await?; -//! let config = SigningConfig::from_tuf().await?; +//! # async fn example() -> Result<(), sigstore_trust_root::Error> { +//! let config = TufConfig::custom( +//! "https://sigstore.github.io/root-signing/", +//! include_bytes!("path/to/root.json"), +//! ); +//! let root = TrustedRoot::from_tuf(config).await?; +//! # Ok(()) +//! # } //! ``` pub mod error; diff --git a/crates/sigstore-trust-root/src/signing_config.rs b/crates/sigstore-trust-root/src/signing_config.rs index 9830f55..4cb6377 100644 --- a/crates/sigstore-trust-root/src/signing_config.rs +++ b/crates/sigstore-trust-root/src/signing_config.rs @@ -12,13 +12,25 @@ //! ```no_run //! use sigstore_trust_root::SigningConfig; //! -//! // Load embedded production signing config -//! let config = SigningConfig::production().unwrap(); +//! # async fn example() -> Result<(), sigstore_trust_root::Error> { +//! // Fetch production signing config via TUF (recommended) +//! let config = SigningConfig::production().await?; //! //! // Get the best Rekor endpoint (highest available version) //! if let Some(rekor) = config.get_rekor_url(None) { //! println!("Rekor URL: {} (v{})", rekor.url, rekor.major_api_version); //! } +//! # Ok(()) +//! # } +//! ``` +//! +//! For offline use: +//! +//! ``` +//! use sigstore_trust_root::{SigningConfig, SIGSTORE_PRODUCTION_SIGNING_CONFIG}; +//! +//! // Load embedded config (may be stale) +//! let config = SigningConfig::from_json(SIGSTORE_PRODUCTION_SIGNING_CONFIG).unwrap(); //! ``` use chrono::{DateTime, Utc}; @@ -144,17 +156,29 @@ pub struct SigningConfig { } impl SigningConfig { - /// Load the embedded production signing config - pub fn production() -> Result { - Self::from_json(SIGSTORE_PRODUCTION_SIGNING_CONFIG) - } - - /// Load the embedded staging signing config - pub fn staging() -> Result { - Self::from_json(SIGSTORE_STAGING_SIGNING_CONFIG) - } - /// Parse signing config from JSON + /// + /// This parses a signing configuration from a JSON string. For offline use, + /// you can pass the embedded constants directly: + /// + /// # Example + /// + /// ``` + /// use sigstore_trust_root::{SigningConfig, SIGSTORE_PRODUCTION_SIGNING_CONFIG}; + /// + /// let config = SigningConfig::from_json(SIGSTORE_PRODUCTION_SIGNING_CONFIG).unwrap(); + /// if let Some(rekor) = config.get_rekor_url(None) { + /// println!("Rekor URL: {}", rekor.url); + /// } + /// ``` + /// + /// For staging: + /// + /// ``` + /// use sigstore_trust_root::{SigningConfig, SIGSTORE_STAGING_SIGNING_CONFIG}; + /// + /// let config = SigningConfig::from_json(SIGSTORE_STAGING_SIGNING_CONFIG).unwrap(); + /// ``` pub fn from_json(json: &str) -> Result { let config: SigningConfig = serde_json::from_str(json)?; @@ -254,16 +278,18 @@ mod tests { use super::*; #[test] - fn test_parse_production_signing_config() { - let config = SigningConfig::production().expect("Failed to parse production config"); + fn test_from_json_production() { + let config = SigningConfig::from_json(SIGSTORE_PRODUCTION_SIGNING_CONFIG) + .expect("Failed to parse production config"); assert_eq!(config.media_type, SIGNING_CONFIG_MEDIA_TYPE); assert!(!config.ca_urls.is_empty()); assert!(!config.rekor_tlog_urls.is_empty()); } #[test] - fn test_parse_staging_signing_config() { - let config = SigningConfig::staging().expect("Failed to parse staging config"); + fn test_from_json_staging() { + let config = SigningConfig::from_json(SIGSTORE_STAGING_SIGNING_CONFIG) + .expect("Failed to parse staging config"); assert_eq!(config.media_type, SIGNING_CONFIG_MEDIA_TYPE); assert!(!config.ca_urls.is_empty()); assert!(!config.rekor_tlog_urls.is_empty()); @@ -271,7 +297,8 @@ mod tests { #[test] fn test_get_rekor_url_highest_version() { - let config = SigningConfig::staging().expect("Failed to parse staging config"); + let config = SigningConfig::from_json(SIGSTORE_STAGING_SIGNING_CONFIG) + .expect("Failed to parse staging config"); if let Some(rekor) = config.get_rekor_url(None) { // Staging should have V2 available println!("Best Rekor: {} v{}", rekor.url, rekor.major_api_version); @@ -280,7 +307,8 @@ mod tests { #[test] fn test_get_rekor_url_force_version() { - let config = SigningConfig::staging().expect("Failed to parse staging config"); + let config = SigningConfig::from_json(SIGSTORE_STAGING_SIGNING_CONFIG) + .expect("Failed to parse staging config"); // Force V1 if let Some(rekor) = config.get_rekor_url(Some(1)) { diff --git a/crates/sigstore-trust-root/src/trusted_root.rs b/crates/sigstore-trust-root/src/trusted_root.rs index e05d1ea..b7bc5a5 100644 --- a/crates/sigstore-trust-root/src/trusted_root.rs +++ b/crates/sigstore-trust-root/src/trusted_root.rs @@ -432,21 +432,6 @@ pub const SIGSTORE_PRODUCTION_TRUSTED_ROOT: &str = include_str!("trusted_root.js /// This is the trusted root for Sigstore's staging/testing instance. pub const SIGSTORE_STAGING_TRUSTED_ROOT: &str = include_str!("trusted_root_staging.json"); -impl TrustedRoot { - /// Load the default Sigstore production trusted root - pub fn production() -> Result { - Self::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT) - } - - /// Load the Sigstore staging trusted root - /// - /// This is useful for testing against the Sigstore staging environment - /// at . - pub fn staging() -> Result { - Self::from_json(SIGSTORE_STAGING_TRUSTED_ROOT) - } -} - #[cfg(test)] mod tests { use super::*; @@ -495,16 +480,16 @@ mod tests { } #[test] - fn test_production_trusted_root() { - let root = TrustedRoot::production().unwrap(); + fn test_from_json_production() { + let root = TrustedRoot::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT).unwrap(); assert!(!root.tlogs.is_empty()); assert!(!root.certificate_authorities.is_empty()); assert!(!root.ctlogs.is_empty()); } #[test] - fn test_staging_trusted_root() { - let root = TrustedRoot::staging().unwrap(); + fn test_from_json_staging() { + let root = TrustedRoot::from_json(SIGSTORE_STAGING_TRUSTED_ROOT).unwrap(); assert!(!root.tlogs.is_empty()); assert!(!root.certificate_authorities.is_empty()); assert!(!root.ctlogs.is_empty()); diff --git a/crates/sigstore-trust-root/src/tuf.rs b/crates/sigstore-trust-root/src/tuf.rs index 785bfdc..72538ed 100644 --- a/crates/sigstore-trust-root/src/tuf.rs +++ b/crates/sigstore-trust-root/src/tuf.rs @@ -9,21 +9,38 @@ //! use sigstore_trust_root::{TrustedRoot, SigningConfig}; //! //! # async fn example() -> Result<(), sigstore_trust_root::Error> { -//! // Fetch trusted root via TUF from production Sigstore -//! let root = TrustedRoot::from_tuf().await?; +//! // Fetch trusted root via TUF from production Sigstore (recommended) +//! let root = TrustedRoot::production().await?; //! //! // Fetch signing config via TUF -//! let config = SigningConfig::from_tuf().await?; +//! let config = SigningConfig::production().await?; //! //! // Or from staging -//! let staging_root = TrustedRoot::from_tuf_staging().await?; -//! let staging_config = SigningConfig::from_tuf_staging().await?; +//! let staging_root = TrustedRoot::staging().await?; +//! let staging_config = SigningConfig::staging().await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! For custom TUF repositories: +//! +//! ```ignore +//! use sigstore_trust_root::{TrustedRoot, TufConfig}; +//! +//! # async fn example() -> Result<(), sigstore_trust_root::Error> { +//! let config = TufConfig::custom( +//! "https://sigstore.github.io/root-signing/", +//! include_bytes!("path/to/root.json"), +//! ); +//! let root = TrustedRoot::from_tuf(config).await?; //! # Ok(()) //! # } //! ``` -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use chrono::{DateTime, Utc}; +use serde::Deserialize; use tough::{HttpTransport, IntoVec, RepositoryLoader, TargetName}; use url::Url; @@ -47,17 +64,38 @@ pub const TRUSTED_ROOT_TARGET: &str = "trusted_root.json"; /// TUF target name for signing configuration pub const SIGNING_CONFIG_TARGET: &str = "signing_config.v0.2.json"; +/// Convert a URL to a safe directory name for caching +/// +/// This encodes special characters to create a filesystem-safe name while +/// remaining human-readable. +fn url_to_dirname(url: &str) -> String { + let mut result = String::with_capacity(url.len() * 3); + for c in url.chars() { + match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => result.push(c), + _ => { + for byte in c.to_string().as_bytes() { + result.push_str(&format!("%{:02X}", byte)); + } + } + } + } + result +} + /// Configuration for TUF client #[derive(Debug, Clone)] pub struct TufConfig { /// Base URL for the TUF repository pub url: String, - /// Path to local cache directory (optional) + /// Path to local cache directory (optional, derived from URL if not set) pub cache_dir: Option, /// Whether to disable local caching pub disable_cache: bool, /// Whether to use offline mode (no network, use cached/embedded data) pub offline: bool, + /// Custom TUF root.json for bootstrapping trust (None = use embedded for known URLs) + root_json: Option>, } impl Default for TufConfig { @@ -67,6 +105,7 @@ impl Default for TufConfig { cache_dir: None, disable_cache: false, offline: false, + root_json: None, } } } @@ -85,6 +124,67 @@ impl TufConfig { } } + /// Create configuration for a custom TUF repository + /// + /// # Arguments + /// + /// * `url` - Base URL of the TUF repository + /// * `root_json` - Contents of root.json for bootstrapping trust + /// + /// # Example + /// + /// ```ignore + /// use sigstore_trust_root::{TrustedRoot, TufConfig}; + /// + /// # async fn example() -> Result<(), sigstore_trust_root::Error> { + /// // For the root-signing test repository + /// let config = TufConfig::custom( + /// "https://sigstore.github.io/root-signing/", + /// include_bytes!("path/to/root.json"), + /// ); + /// let root = TrustedRoot::from_tuf(config).await?; + /// # Ok(()) + /// # } + /// ``` + pub fn custom(url: impl Into, root_json: impl AsRef<[u8]>) -> Self { + Self { + url: url.into(), + cache_dir: None, + disable_cache: false, + offline: false, + root_json: Some(root_json.as_ref().to_vec()), + } + } + + /// Create configuration for a custom TUF repository, loading root.json from a file + /// + /// # Arguments + /// + /// * `url` - Base URL of the TUF repository + /// * `root_path` - Path to the root.json file + /// + /// # Example + /// + /// ```no_run + /// use sigstore_trust_root::{TrustedRoot, TufConfig}; + /// + /// # async fn example() -> Result<(), Box> { + /// let config = TufConfig::custom_from_file( + /// "https://sigstore.github.io/root-signing/", + /// "path/to/root.json", + /// )?; + /// let root = TrustedRoot::from_tuf(config).await?; + /// # Ok(()) + /// # } + /// ``` + pub fn custom_from_file( + url: impl Into, + root_path: impl AsRef, + ) -> std::io::Result { + let root_json = std::fs::read(root_path)?; + Ok(Self::custom(url, root_json)) + } + /// Set the cache directory pub fn with_cache_dir(mut self, path: PathBuf) -> Self { self.cache_dir = Some(path); @@ -110,6 +210,32 @@ impl TufConfig { self.offline = true; self } + + /// Get the TUF root.json bytes for this configuration + /// + /// Returns the custom root if set, otherwise returns the embedded root + /// for known URLs (production/staging). + /// + /// # Panics + /// + /// Panics if no root.json is available for the configured URL. + fn get_root_json(&self) -> &[u8] { + if let Some(ref root) = self.root_json { + return root.as_slice(); + } + + // Fall back to embedded roots for known URLs + if self.url == DEFAULT_TUF_URL || self.url.starts_with(DEFAULT_TUF_URL) { + PRODUCTION_TUF_ROOT + } else if self.url == STAGING_TUF_URL || self.url.starts_with(STAGING_TUF_URL) { + STAGING_TUF_ROOT + } else { + panic!( + "No root.json provided for custom URL: {}. Use TufConfig::custom() to provide one.", + self.url + ) + } + } } /// Embedded production trusted root (same as SIGSTORE_PRODUCTION_TRUSTED_ROOT but as bytes) @@ -126,45 +252,53 @@ const EMBEDDED_STAGING_TRUSTED_ROOT: &[u8] = include_bytes!("trusted_root_stagin const EMBEDDED_STAGING_SIGNING_CONFIG: &[u8] = include_bytes!("../repository/signing_config_staging.json"); +/// Minimal TUF metadata structure for parsing expiration dates. +/// +/// TUF metadata files (timestamp.json, snapshot.json, etc.) contain a "signed" +/// object with an "expires" field. We only need to parse this to check freshness. +#[derive(Deserialize)] +struct TufMetadata { + signed: TufSigned, +} + +#[derive(Deserialize)] +struct TufSigned { + expires: DateTime, +} + /// Internal TUF client for fetching targets struct TufClient { config: TufConfig, - root_json: &'static [u8], /// Embedded targets for offline fallback (target_name -> bytes) embedded_targets: &'static [(&'static str, &'static [u8])], } impl TufClient { - /// Create a new client for production - fn production() -> Self { - Self { - config: TufConfig::production(), - root_json: PRODUCTION_TUF_ROOT, - embedded_targets: &[ - (TRUSTED_ROOT_TARGET, EMBEDDED_PRODUCTION_TRUSTED_ROOT), - (SIGNING_CONFIG_TARGET, EMBEDDED_PRODUCTION_SIGNING_CONFIG), - ], - } - } - - /// Create a new client for staging - fn staging() -> Self { - Self { - config: TufConfig::staging(), - root_json: STAGING_TUF_ROOT, - embedded_targets: &[ - (TRUSTED_ROOT_TARGET, EMBEDDED_STAGING_TRUSTED_ROOT), - (SIGNING_CONFIG_TARGET, EMBEDDED_STAGING_SIGNING_CONFIG), - ], - } - } + /// Create a new TUF client with the given configuration + /// + /// Embedded fallback targets are automatically configured for known URLs + /// (production and staging). + fn new(config: TufConfig) -> Self { + // Determine embedded targets based on URL for offline fallback + let embedded_targets: &'static [(&'static str, &'static [u8])] = + if config.url == DEFAULT_TUF_URL || config.url.starts_with(DEFAULT_TUF_URL) { + &[ + (TRUSTED_ROOT_TARGET, EMBEDDED_PRODUCTION_TRUSTED_ROOT), + (SIGNING_CONFIG_TARGET, EMBEDDED_PRODUCTION_SIGNING_CONFIG), + ] + } else if config.url == STAGING_TUF_URL || config.url.starts_with(STAGING_TUF_URL) { + &[ + (TRUSTED_ROOT_TARGET, EMBEDDED_STAGING_TRUSTED_ROOT), + (SIGNING_CONFIG_TARGET, EMBEDDED_STAGING_SIGNING_CONFIG), + ] + } else { + // Custom URLs have no embedded fallback + &[] + }; - /// Create a new client with custom configuration (no embedded fallback) - fn new(config: TufConfig, root_json: &'static [u8]) -> Self { Self { config, - root_json, - embedded_targets: &[], + embedded_targets, } } @@ -185,8 +319,8 @@ impl TufClient { .join("targets/") .map_err(|e| Error::Tuf(e.to_string()))?; - // Create repository loader with embedded root - let root_bytes = self.root_json.to_vec(); + // Create repository loader with root.json + let root_bytes = self.config.get_root_json().to_vec(); let mut loader = RepositoryLoader::new(&root_bytes, metadata_url, targets_url); // Use HTTP transport @@ -222,21 +356,48 @@ impl TufClient { .await .map_err(|e| Error::Tuf(format!("Failed to read target contents: {}", e)))?; + // Cache the target bytes for offline use + if !self.config.disable_cache { + if let Ok(cache_dir) = self.get_cache_dir() { + let targets_dir = cache_dir.join("targets"); + if tokio::fs::create_dir_all(&targets_dir).await.is_ok() { + // Best-effort: don't fail the fetch if caching fails + let _ = tokio::fs::write(targets_dir.join(target_name), &bytes).await; + } + } + } + Ok(bytes) } /// Fetch target in offline mode (no network) /// /// Priority: - /// 1. Check local TUF cache for previously downloaded target - /// 2. Fall back to embedded data + /// 1. Check local TUF cache — only if TUF metadata has not expired + /// 2. Fall back to embedded data (compile-time snapshot, no expiration check) + /// + /// The TUF metadata expiration check prevents serving stale cached data after + /// a key rotation or revocation. This mirrors TUF's built-in freshness guarantees + /// that are normally enforced during online updates. async fn fetch_target_offline(&self, target_name: &str) -> Result> { - // Try to read from cache first + // Try to read from cache first, with expiration check if !self.config.disable_cache { if let Ok(cache_dir) = self.get_cache_dir() { let cached_path = cache_dir.join("targets").join(target_name); if let Ok(bytes) = tokio::fs::read(&cached_path).await { - return Ok(bytes); + // Check TUF metadata expiration before serving cached data. + // The timestamp.json file has the shortest expiration in TUF + // (typically 1 day) and is the primary freshness indicator. + match self.check_cache_expiration(&cache_dir).await { + Ok(()) => return Ok(bytes), + Err(e) => { + tracing::warn!( + "Cached TUF metadata has expired ({}), falling back to embedded data", + e + ); + // Fall through to embedded data + } + } } } } @@ -254,24 +415,57 @@ impl TufClient { ))) } + /// Check whether cached TUF metadata is still fresh (not expired). + /// + /// Reads the `timestamp.json` from the datastore and checks its `expires` field. + /// TUF's timestamp metadata has the shortest expiration (typically 1 day) and + /// serves as the primary freshness indicator. If the timestamp has expired, + /// the cached data should not be trusted because a key rotation or revocation + /// may have occurred since the last online update. + async fn check_cache_expiration(&self, cache_dir: &Path) -> std::result::Result<(), String> { + let timestamp_path = cache_dir.join("timestamp.json"); + let timestamp_bytes = tokio::fs::read(×tamp_path) + .await + .map_err(|e| format!("cannot read timestamp.json: {}", e))?; + + let metadata: TufMetadata = serde_json::from_slice(×tamp_bytes) + .map_err(|e| format!("cannot parse timestamp.json: {}", e))?; + + let now = Utc::now(); + if now > metadata.signed.expires { + return Err(format!( + "timestamp.json expired at {} (now: {})", + metadata.signed.expires, now + )); + } + + Ok(()) + } + /// Get the cache directory path + /// + /// Returns URL-namespaced cache directory to prevent collisions between + /// different TUF repositories. fn get_cache_dir(&self) -> Result { if let Some(ref dir) = self.config.cache_dir { return Ok(dir.clone()); } - // Use platform-specific cache directory + // Use platform-specific cache directory with URL namespace let project_dirs = directories::ProjectDirs::from("dev", "sigstore", "sigstore-rust") .ok_or_else(|| Error::Tuf("Could not determine cache directory".into()))?; - Ok(project_dirs.cache_dir().join("tuf")) + // Create URL-namespaced subdirectory + let namespace = url_to_dirname(&self.config.url); + Ok(project_dirs.cache_dir().join("tuf").join(namespace)) } } impl TrustedRoot { /// Fetch the trusted root from Sigstore's production TUF repository /// - /// This securely fetches the `trusted_root.json` using the TUF protocol, + /// This is the **recommended** way to get the trusted root for production use. + /// It securely fetches the latest `trusted_root.json` using the TUF protocol, /// verifying all metadata signatures against the embedded root of trust. /// /// # Example @@ -280,38 +474,68 @@ impl TrustedRoot { /// use sigstore_trust_root::TrustedRoot; /// /// # async fn example() -> Result<(), sigstore_trust_root::Error> { - /// let root = TrustedRoot::from_tuf().await?; + /// let root = TrustedRoot::production().await?; /// println!("Loaded {} Rekor logs", root.tlogs.len()); /// # Ok(()) /// # } /// ``` - pub async fn from_tuf() -> Result { - let client = TufClient::production(); - let bytes = client.fetch_target(TRUSTED_ROOT_TARGET).await?; - let json = String::from_utf8(bytes) - .map_err(|e| Error::Tuf(format!("Invalid UTF-8 in {}: {}", TRUSTED_ROOT_TARGET, e)))?; - Self::from_json(&json) + pub async fn production() -> Result { + Self::from_tuf(TufConfig::production()).await } /// Fetch the trusted root from Sigstore's staging TUF repository /// /// This is useful for testing against the staging Sigstore infrastructure. - pub async fn from_tuf_staging() -> Result { - let client = TufClient::staging(); - let bytes = client.fetch_target(TRUSTED_ROOT_TARGET).await?; - let json = String::from_utf8(bytes) - .map_err(|e| Error::Tuf(format!("Invalid UTF-8 in {}: {}", TRUSTED_ROOT_TARGET, e)))?; - Self::from_json(&json) + /// + /// # Example + /// + /// ```no_run + /// use sigstore_trust_root::TrustedRoot; + /// + /// # async fn example() -> Result<(), sigstore_trust_root::Error> { + /// let root = TrustedRoot::staging().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn staging() -> Result { + Self::from_tuf(TufConfig::staging()).await } - /// Fetch the trusted root from a custom TUF repository + /// Fetch the trusted root from a TUF repository with custom configuration /// - /// # Arguments + /// This method allows fetching from custom TUF repositories or configuring + /// advanced options like cache directory, offline mode, etc. + /// + /// # Example: Custom TUF Repository + /// + /// ```ignore + /// use sigstore_trust_root::{TrustedRoot, TufConfig}; + /// + /// # async fn example() -> Result<(), sigstore_trust_root::Error> { + /// // For the root-signing test repository + /// let config = TufConfig::custom( + /// "https://sigstore.github.io/root-signing/", + /// include_bytes!("path/to/root.json"), + /// ); + /// let root = TrustedRoot::from_tuf(config).await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Example: Offline Mode /// - /// * `config` - TUF client configuration - /// * `tuf_root` - The TUF root.json to use for bootstrapping trust - pub async fn from_tuf_with_config(config: TufConfig, tuf_root: &'static [u8]) -> Result { - let client = TufClient::new(config, tuf_root); + /// ```no_run + /// use sigstore_trust_root::{TrustedRoot, TufConfig}; + /// + /// # async fn example() -> Result<(), sigstore_trust_root::Error> { + /// // Use cached/embedded data only (no network) + /// let config = TufConfig::production().offline(); + /// let root = TrustedRoot::from_tuf(config).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn from_tuf(config: TufConfig) -> Result { + let client = TufClient::new(config); let bytes = client.fetch_target(TRUSTED_ROOT_TARGET).await?; let json = String::from_utf8(bytes) .map_err(|e| Error::Tuf(format!("Invalid UTF-8 in {}: {}", TRUSTED_ROOT_TARGET, e)))?; @@ -322,7 +546,8 @@ impl TrustedRoot { impl SigningConfig { /// Fetch the signing configuration from Sigstore's production TUF repository /// - /// This securely fetches the `signing_config.v0.2.json` using the TUF protocol, + /// This is the **recommended** way to get the signing config for production use. + /// It securely fetches the latest `signing_config.v0.2.json` using the TUF protocol, /// verifying all metadata signatures against the embedded root of trust. /// /// The signing config contains service endpoints for signing operations: @@ -337,43 +562,55 @@ impl SigningConfig { /// use sigstore_trust_root::SigningConfig; /// /// # async fn example() -> Result<(), sigstore_trust_root::Error> { - /// let config = SigningConfig::from_tuf().await?; + /// let config = SigningConfig::production().await?; /// if let Some(rekor) = config.get_rekor_url(None) { /// println!("Rekor URL: {} (v{})", rekor.url, rekor.major_api_version); /// } /// # Ok(()) /// # } /// ``` - pub async fn from_tuf() -> Result { - let client = TufClient::production(); - let bytes = client.fetch_target(SIGNING_CONFIG_TARGET).await?; - let json = String::from_utf8(bytes).map_err(|e| { - Error::Tuf(format!("Invalid UTF-8 in {}: {}", SIGNING_CONFIG_TARGET, e)) - })?; - Self::from_json(&json) + pub async fn production() -> Result { + Self::from_tuf(TufConfig::production()).await } /// Fetch the signing configuration from Sigstore's staging TUF repository /// /// This is useful for testing against the staging Sigstore infrastructure, /// which may have newer API versions (e.g., Rekor V2) available. - pub async fn from_tuf_staging() -> Result { - let client = TufClient::staging(); - let bytes = client.fetch_target(SIGNING_CONFIG_TARGET).await?; - let json = String::from_utf8(bytes).map_err(|e| { - Error::Tuf(format!("Invalid UTF-8 in {}: {}", SIGNING_CONFIG_TARGET, e)) - })?; - Self::from_json(&json) + /// + /// # Example + /// + /// ```no_run + /// use sigstore_trust_root::SigningConfig; + /// + /// # async fn example() -> Result<(), sigstore_trust_root::Error> { + /// let config = SigningConfig::staging().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn staging() -> Result { + Self::from_tuf(TufConfig::staging()).await } - /// Fetch the signing configuration from a custom TUF repository + /// Fetch the signing configuration from a TUF repository with custom configuration /// - /// # Arguments + /// This method allows fetching from custom TUF repositories or configuring + /// advanced options like cache directory, offline mode, etc. + /// + /// # Example + /// + /// ```no_run + /// use sigstore_trust_root::{SigningConfig, TufConfig}; /// - /// * `config` - TUF client configuration - /// * `tuf_root` - The TUF root.json to use for bootstrapping trust - pub async fn from_tuf_with_config(config: TufConfig, tuf_root: &'static [u8]) -> Result { - let client = TufClient::new(config, tuf_root); + /// # async fn example() -> Result<(), sigstore_trust_root::Error> { + /// // Use offline mode with cached data + /// let config = TufConfig::production().offline(); + /// let signing_config = SigningConfig::from_tuf(config).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn from_tuf(config: TufConfig) -> Result { + let client = TufClient::new(config); let bytes = client.fetch_target(SIGNING_CONFIG_TARGET).await?; let json = String::from_utf8(bytes).map_err(|e| { Error::Tuf(format!("Invalid UTF-8 in {}: {}", SIGNING_CONFIG_TARGET, e)) @@ -386,6 +623,20 @@ impl SigningConfig { mod tests { use super::*; + #[test] + fn test_url_to_dirname() { + assert_eq!( + url_to_dirname("https://tuf-repo-cdn.sigstore.dev"), + "https%3A%2F%2Ftuf-repo-cdn.sigstore.dev" + ); + assert_eq!( + url_to_dirname("https://sigstore.github.io/root-signing/"), + "https%3A%2F%2Fsigstore.github.io%2Froot-signing%2F" + ); + // Alphanumeric and safe chars should pass through + assert_eq!(url_to_dirname("abc-123_test.json"), "abc-123_test.json"); + } + #[test] fn test_tuf_config_default() { let config = TufConfig::default(); @@ -393,6 +644,7 @@ mod tests { assert!(config.cache_dir.is_none()); assert!(!config.disable_cache); assert!(!config.offline); + assert!(config.root_json.is_none()); } #[test] @@ -401,6 +653,14 @@ mod tests { assert_eq!(config.url, STAGING_TUF_URL); } + #[test] + fn test_tuf_config_custom() { + let root_json = b"test root json"; + let config = TufConfig::custom("https://custom.tuf/", root_json); + assert_eq!(config.url, "https://custom.tuf/"); + assert_eq!(config.root_json, Some(root_json.to_vec())); + } + #[test] fn test_tuf_config_builder() { let config = TufConfig::production() @@ -412,6 +672,38 @@ mod tests { assert_eq!(config.cache_dir, Some(PathBuf::from("/tmp/test"))); } + #[test] + fn test_tuf_config_get_root_json_production() { + let config = TufConfig::production(); + assert_eq!(config.get_root_json(), PRODUCTION_TUF_ROOT); + } + + #[test] + fn test_tuf_config_get_root_json_staging() { + let config = TufConfig::staging(); + assert_eq!(config.get_root_json(), STAGING_TUF_ROOT); + } + + #[test] + fn test_tuf_config_get_root_json_custom() { + let root_json = b"custom root"; + let config = TufConfig::custom("https://custom.tuf/", root_json); + assert_eq!(config.get_root_json(), root_json); + } + + #[test] + #[should_panic(expected = "No root.json provided for custom URL")] + fn test_tuf_config_get_root_json_unknown_url_panics() { + let config = TufConfig { + url: "https://unknown.tuf/".to_string(), + cache_dir: None, + disable_cache: false, + offline: false, + root_json: None, + }; + let _ = config.get_root_json(); + } + #[test] fn test_embedded_tuf_roots_are_valid_json() { // Verify the embedded TUF roots are valid JSON @@ -439,16 +731,9 @@ mod tests { #[tokio::test] async fn test_offline_mode_uses_embedded_data() { - // Create a client in offline mode with cache disabled - // This should fall back to embedded data - let client = TufClient { - config: TufConfig::production().offline().without_cache(), - root_json: PRODUCTION_TUF_ROOT, - embedded_targets: &[ - (TRUSTED_ROOT_TARGET, EMBEDDED_PRODUCTION_TRUSTED_ROOT), - (SIGNING_CONFIG_TARGET, EMBEDDED_PRODUCTION_SIGNING_CONFIG), - ], - }; + // Use offline mode with cache disabled - should fall back to embedded data + let config = TufConfig::production().offline().without_cache(); + let client = TufClient::new(config); // Should successfully return embedded trusted root let bytes = client.fetch_target(TRUSTED_ROOT_TARGET).await.unwrap(); @@ -463,14 +748,99 @@ mod tests { #[tokio::test] async fn test_offline_mode_fails_for_unknown_target() { - let client = TufClient { - config: TufConfig::production().offline().without_cache(), - root_json: PRODUCTION_TUF_ROOT, - embedded_targets: &[], // No embedded data - }; + let config = TufConfig::production().offline().without_cache(); + let client = TufClient::new(config); // Should fail for unknown target let result = client.fetch_target("unknown.json").await; assert!(result.is_err()); } + + #[tokio::test] + async fn test_custom_url_offline_fails_without_cache() { + // Custom URLs have no embedded fallback + let config = TufConfig::custom("https://custom.tuf/", b"root") + .offline() + .without_cache(); + let client = TufClient::new(config); + + // Should fail since there's no embedded data for custom URLs + let result = client.fetch_target(TRUSTED_ROOT_TARGET).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_offline_mode_expired_cache_falls_back_to_embedded() { + // Create a temp dir with expired TUF metadata and a cached target + let tmp = tempfile::tempdir().unwrap(); + let cache_dir = tmp.path().to_path_buf(); + + // Write an expired timestamp.json + let expired_timestamp = r#"{ + "signed": { + "_type": "timestamp", + "expires": "2020-01-01T00:00:00Z", + "version": 1, + "spec_version": "1.0" + }, + "signatures": [] + }"#; + tokio::fs::write(cache_dir.join("timestamp.json"), expired_timestamp) + .await + .unwrap(); + + // Write a cached target (this should NOT be returned due to expiration) + let targets_dir = cache_dir.join("targets"); + tokio::fs::create_dir_all(&targets_dir).await.unwrap(); + tokio::fs::write(targets_dir.join(TRUSTED_ROOT_TARGET), b"CACHED_BUT_EXPIRED") + .await + .unwrap(); + + // Use offline mode pointing to our temp cache + let config = TufConfig::production().offline().with_cache_dir(cache_dir); + let client = TufClient::new(config); + + // Should fall back to embedded data since cache is expired + let bytes = client.fetch_target(TRUSTED_ROOT_TARGET).await.unwrap(); + assert_ne!(bytes, b"CACHED_BUT_EXPIRED"); + // Should be valid embedded trusted root + let _root: crate::TrustedRoot = serde_json::from_slice(&bytes).unwrap(); + } + + #[tokio::test] + async fn test_offline_mode_fresh_cache_is_used() { + // Create a temp dir with fresh TUF metadata and a cached target + let tmp = tempfile::tempdir().unwrap(); + let cache_dir = tmp.path().to_path_buf(); + + // Write a timestamp.json that expires far in the future + let fresh_timestamp = r#"{ + "signed": { + "_type": "timestamp", + "expires": "2099-01-01T00:00:00Z", + "version": 1, + "spec_version": "1.0" + }, + "signatures": [] + }"#; + tokio::fs::write(cache_dir.join("timestamp.json"), fresh_timestamp) + .await + .unwrap(); + + // Write a cached target + let targets_dir = cache_dir.join("targets"); + tokio::fs::create_dir_all(&targets_dir).await.unwrap(); + let cached_content = EMBEDDED_PRODUCTION_TRUSTED_ROOT; // valid content + tokio::fs::write(targets_dir.join(TRUSTED_ROOT_TARGET), cached_content) + .await + .unwrap(); + + // Use offline mode pointing to our temp cache + let config = TufConfig::production().offline().with_cache_dir(cache_dir); + let client = TufClient::new(config); + + // Should use the cached data since it's fresh + let bytes = client.fetch_target(TRUSTED_ROOT_TARGET).await.unwrap(); + assert_eq!(bytes, cached_content); + } } diff --git a/crates/sigstore-verify/README.md b/crates/sigstore-verify/README.md index 7c4bfdd..2b2c12e 100644 --- a/crates/sigstore-verify/README.md +++ b/crates/sigstore-verify/README.md @@ -30,7 +30,8 @@ use sigstore_verify::{verify, Verifier, VerificationPolicy}; use sigstore_trust_root::TrustedRoot; use sigstore_types::{Artifact, Bundle, Sha256Hash}; -let root = TrustedRoot::production()?; +// Fetch trusted root via TUF (recommended - ensures up-to-date trust material) +let root = TrustedRoot::production().await?; let bundle: Bundle = serde_json::from_str(bundle_json)?; let policy = VerificationPolicy::default(); diff --git a/crates/sigstore-verify/examples/verify_bundle.rs b/crates/sigstore-verify/examples/verify_bundle.rs index bf74c89..84c1ad7 100644 --- a/crates/sigstore-verify/examples/verify_bundle.rs +++ b/crates/sigstore-verify/examples/verify_bundle.rs @@ -45,7 +45,7 @@ //! ``` use regex::Regex; -use sigstore_trust_root::TrustedRoot; +use sigstore_trust_root::{TrustedRoot, SIGSTORE_PRODUCTION_TRUSTED_ROOT}; use sigstore_types::{Artifact, Bundle, Sha256Hash}; use sigstore_verify::{verify, VerificationPolicy}; @@ -136,7 +136,9 @@ fn main() { }; // Load trusted root (production Sigstore infrastructure) - let trusted_root = match TrustedRoot::production() { + // Using embedded data for this example - in production, prefer TrustedRoot::production().await + // to fetch the latest trust material via TUF protocol + let trusted_root = match TrustedRoot::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT) { Ok(root) => root, Err(e) => { eprintln!("Error loading trusted root: {}", e); diff --git a/crates/sigstore-verify/examples/verify_conda_attestation.rs b/crates/sigstore-verify/examples/verify_conda_attestation.rs index f47a2ea..0e2cc3c 100644 --- a/crates/sigstore-verify/examples/verify_conda_attestation.rs +++ b/crates/sigstore-verify/examples/verify_conda_attestation.rs @@ -24,7 +24,7 @@ //! crates/sigstore-verify/test_data/bundles/conda-attestation.sigstore.json //! ``` -use sigstore_trust_root::TrustedRoot; +use sigstore_trust_root::{TrustedRoot, SIGSTORE_PRODUCTION_TRUSTED_ROOT}; use sigstore_types::{bundle::SignatureContent, Bundle}; use sigstore_verify::{verify, VerificationPolicy}; @@ -78,7 +78,9 @@ fn main() { }; // Load trusted root (production Sigstore infrastructure) - let trusted_root = match TrustedRoot::production() { + // Using embedded data for this example - in production, prefer TrustedRoot::production().await + // to fetch the latest trust material via TUF protocol + let trusted_root = match TrustedRoot::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT) { Ok(root) => root, Err(e) => { eprintln!("Error loading trusted root: {}", e); diff --git a/crates/sigstore-verify/src/lib.rs b/crates/sigstore-verify/src/lib.rs index 4406292..54c806b 100644 --- a/crates/sigstore-verify/src/lib.rs +++ b/crates/sigstore-verify/src/lib.rs @@ -9,8 +9,9 @@ //! use sigstore_trust_root::TrustedRoot; //! use sigstore_types::Bundle; //! -//! # fn example() -> Result<(), Box> { -//! let trusted_root = TrustedRoot::production()?; +//! # async fn example() -> Result<(), Box> { +//! // Fetch trusted root via TUF (recommended) +//! let trusted_root = TrustedRoot::production().await?; //! let bundle_json = std::fs::read_to_string("artifact.sigstore.json")?; //! let bundle = Bundle::from_json(&bundle_json)?; //! let artifact = std::fs::read("artifact.txt")?; diff --git a/crates/sigstore-verify/src/verify.rs b/crates/sigstore-verify/src/verify.rs index 2f248c7..5fae3dd 100644 --- a/crates/sigstore-verify/src/verify.rs +++ b/crates/sigstore-verify/src/verify.rs @@ -175,8 +175,9 @@ impl Verifier { /// use sigstore_trust_root::TrustedRoot; /// use sigstore_types::{Artifact, Bundle, Sha256Hash}; /// - /// # fn example() -> Result<(), Box> { - /// let trusted_root = TrustedRoot::production()?; + /// # async fn example() -> Result<(), Box> { + /// // Fetch trusted root via TUF (recommended) + /// let trusted_root = TrustedRoot::production().await?; /// let verifier = Verifier::new(&trusted_root); /// let bundle: Bundle = todo!(); /// let policy = VerificationPolicy::default(); diff --git a/crates/sigstore-verify/tests/verification_tests.rs b/crates/sigstore-verify/tests/verification_tests.rs index 3a85f8c..eb3d468 100644 --- a/crates/sigstore-verify/tests/verification_tests.rs +++ b/crates/sigstore-verify/tests/verification_tests.rs @@ -2,7 +2,7 @@ //! //! These tests validate the complete verification flow using real bundles. -use sigstore_trust_root::TrustedRoot; +use sigstore_trust_root::{TrustedRoot, SIGSTORE_PRODUCTION_TRUSTED_ROOT}; use sigstore_types::{LogIndex, Sha256Hash}; use sigstore_verify::bundle::{validate_bundle, validate_bundle_with_options, ValidationOptions}; use sigstore_verify::types::Bundle; @@ -34,9 +34,10 @@ fn extract_artifact_digest(bundle: &Bundle) -> Option { } } -/// Get the production trusted root for tests +/// Get the production trusted root for tests (using embedded data) fn production_root() -> TrustedRoot { - TrustedRoot::production().expect("Failed to load production trusted root") + TrustedRoot::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT) + .expect("Failed to load production trusted root") } /// Real v0.3 bundle from sigstore-python tests