diff --git a/Cargo.toml b/Cargo.toml index 2ee5951e55..adb16a4ac5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,6 +120,7 @@ url = "2.2.2" x509-cert = { version = "0.2.2", features = ["pem", "std"] } crypto_secretbox = "0.1.1" zeroize = "1.5.7" +rustls-pki-types = { version = "0.2.1", features = ["std"] } [dev-dependencies] anyhow = { version = "1.0", features = ["backtrace"] } diff --git a/src/errors.rs b/src/errors.rs index f513f17543..3451317445 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -146,6 +146,9 @@ pub enum SigstoreError { #[error("TUF target {0} not found inside of repository")] TufTargetNotFoundError(String), + #[error("{0}")] + TufMetadataError(&'static str), + #[error(transparent)] IOError(#[from] std::io::Error), diff --git a/src/tuf/constants.rs b/src/tuf/constants.rs index 99231be2da..546f118be2 100644 --- a/src/tuf/constants.rs +++ b/src/tuf/constants.rs @@ -13,175 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use lazy_static::lazy_static; -use regex::Regex; - -lazy_static! { - pub(crate) static ref SIGSTORE_FULCIO_CERT_TARGET_REGEX: Regex = - Regex::new(r"fulcio(_v\d+)?\.crt\.pem").expect("cannot compile regexp"); -} - 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"; -pub(crate) const SIGSTORE_REKOR_PUB_KEY_TARGET: &str = "rekor.pub"; - -pub(crate) const SIGSTORE_ROOT: &str = r#"{ - "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", - "keys": { - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04cbc5cab2684160323c25cd06c3307178a6b1d1c9b949328453ae473c5ba7527e35b13f298b41633382241f3fd8526c262d43b45adee5c618fa0642c82b8a9803" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04fa1a3e42f2300cd3c5487a61509348feb1e936920fef2f83b7cd5dbe7ba045f538725ab8f18a666e6233edb7e0db8766c8dc336633449c5e1bbe0c182b02df0b" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62": { - "keyid_hash_algorithms": [ - "sha256", - "sha512" - ], - "keytype": "ecdsa-sha2-nistp256", - "keyval": { - "public": "04a71aacd835dc170ba6db3fa33a1a33dee751d4f8b0217b805b9bd3242921ee93672fdcfd840576c5bb0dc0ed815edf394c1ee48c2b5e02485e59bfc512f3adc7" - }, - "scheme": "ecdsa-sha2-nistp256" - }, - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b": { - "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" - } - }, - "roles": { - "root": { - "keyids": [ - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209" - ], - "threshold": 3 - }, - "snapshot": { - "keyids": [ - "fc61191ba8a516fe386c7d6c97d918e1d241e1589729add09b122725b8c32451" - ], - "threshold": 1 - }, - "targets": { - "keyids": [ - "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", - "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", - "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", - "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", - "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209" - ], - "threshold": 3 - }, - "timestamp": { - "keyids": [ - "b6710623a30c010738e64c5209d367df1c0a18cf90e6ab5292fb01680f83453d" - ], - "threshold": 1 - } - }, - "spec_version": "1.0", - "version": 2 - } -}"#; - -#[cfg(test)] -mod test { - use super::*; - use rstest::*; - - #[rstest] - #[case("fulcio.crt.pem", true)] - #[case("fulcio_v1.crt.pem", true)] - #[case("fulcio-v2.crt.pem", false)] - #[case("foo.crt.pem", false)] - fn check_fulcio_regex(#[case] input: &str, #[case] matches: bool) { - assert_eq!(SIGSTORE_FULCIO_CERT_TARGET_REGEX.is_match(input), matches); - } +macro_rules! tuf_resource { + ($path:literal) => { + include_bytes!(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"); diff --git a/src/tuf/mod.rs b/src/tuf/mod.rs index 982fa99c14..99932e01f8 100644 --- a/src/tuf/mod.rs +++ b/src/tuf/mod.rs @@ -47,118 +47,308 @@ //! special handling when invoked inside of an async context. Please refer to the //! [method docs](SigstoreRepository::fetch) for more details. //! -use std::path::Path; +use std::{ + cell::OnceCell, + fs, + io::Read, + path::{Path, PathBuf}, +}; mod constants; -use constants::*; +mod trustroot; -mod repository_helper; -use repository_helper::RepositoryHelper; +use rustls_pki_types::CertificateDer; +use sha2::{Digest, Sha256}; +use tough::TargetName; + +use self::trustroot::{CertificateAuthority, TimeRange, TransparencyLogInstance, TrustedRoot}; use super::errors::{Result, SigstoreError}; -/// Securely fetches Rekor public key and Fulcio certificates from Sigstore's TUF repository -#[derive(Clone)] +/// 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>; +} + +/// A `FakeRepository` 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 FakeRepository<'a> { + pub fulcio_certs: Option>>, + pub rekor_key: Option>, +} + +impl Repository for FakeRepository<'_> { + fn fulcio_certs(&self) -> Result> { + Ok(match &self.fulcio_certs { + Some(certs) => certs.clone(), + None => Vec::new(), + }) + } + + fn rekor_keys(&self) -> Result> { + Ok(match &self.rekor_key { + Some(key) => vec![&key[..]], + None => Vec::new(), + }) + } +} + +/// Securely fetches Rekor public key and Fulcio certificates from Sigstore's TUF repository. +#[derive(Debug)] pub struct SigstoreRepository { - rekor_pub_key: String, - fulcio_certs: Vec, + repository: tough::Repository, + checkout_dir: Option, + trusted_root: OnceCell, } impl SigstoreRepository { - /// Fetch relevant information from the remote Sigstore TUF repository. - /// - /// ## Parameters - /// - /// * `checkout_dir`: path to a local directory where Rekor's public - /// key and Fulcio's certificates can be found - /// - /// ## Behaviour - /// - /// This method requires network connectivity, because it will always - /// reach out to Sigstore's TUF repository. - /// - /// This crates embeds a trusted copy of the `root.json` file of Sigstore's - /// TUF repository. The `fetch` function will always connect to the online - /// Sigstore's repository to update this embedded file to the latest version. - /// The update process happens using the TUF protocol. - /// - /// When `checkout_dir` is specified, this method will look for the - /// Fulcio and Rekor files inside of this directory. It will then compare the - /// checksums of these local files with the ones reported inside of the - /// TUF repository metadata. + /// Constructs a new trust repository established by a [tough::Repository]. + 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).unwrap(); + let target_base = url::Url::parse(constants::SIGSTORE_TARGET_BASE).unwrap(); + + let repository = + tough::RepositoryLoader::new(constants::SIGSTORE_ROOT, metadata_base, target_base) + .expiration_enforcement(tough::ExpirationEnforcement::Safe) + .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> { + 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())); + + let data = fetch_target_or_reuse_local_cache( + repository, + &trusted_root_target, + local_path.as_ref(), + )?; + + println!("data:\n{}", String::from_utf8_lossy(&data)); + + Ok(serde_json::from_slice(&data[..]).unwrap()) + } + + 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)) + } + + /// Prefetches trust materials. /// - /// If the files are not found, or if their local checksums do not match - /// with the ones reported by TUF's metdata, the files are then downloaded - /// from the TUF repository and then written to the local filesystem. + /// [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. /// - /// When `checkout_dir` is `None`, the `fetch` method will always fetch the - /// Fulcio and Rekor files from the remote TUF repository and keep them - /// in memory. + /// ```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<'a>(tlogs: &'a Vec) -> 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()) + .map(|key_bytes| key_bytes.as_slice()) + } + + #[inline] + fn ca_keys<'a>( + cas: &'a Vec, + 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()) + .map(|cert| cert.raw_bytes.as_slice()) + } +} + +impl Repository for SigstoreRepository { + /// Fetch Fulcio certificates from the given TUF repository or reuse + /// the local cache if its contents are not outdated. /// - /// ## Usage inside of async code + /// 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 fulcio_certs(&self) -> Result> { + let root = self.trusted_root()?; + + // 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(|v| CertificateDer::from(v)).collect(); + + if certs.is_empty() { + Err(SigstoreError::TufMetadataError( + "Fulcio certificates not found", + )) + } else { + Ok(certs) + } + } + + /// Fetch Rekor public keys from the given TUF repository or reuse + /// the local cache if it's not outdated. /// - /// If needed, this can be solved in the following way: - /// - /// ```rust,no_run - /// use tokio::task::spawn_blocking; - /// use sigstore::tuf::SigstoreRepository; - /// - /// async fn my_async_function() { - /// // ... your code - /// - /// let repo: sigstore::errors::Result = spawn_blocking(|| - /// sigstore::tuf::SigstoreRepository::fetch(None) - /// ) - /// .await - /// .expect("Error spawning blocking task"); - /// - /// // handle the case of `repo` being an `Err` - /// // ... your code - /// } - /// ``` + /// The contents of the local cache are updated when they are outdated. /// - /// This of course has a performance hit when used inside of an async function. - pub fn fetch(checkout_dir: Option<&Path>) -> Result { - let metadata_base = url::Url::parse(SIGSTORE_METADATA_BASE).map_err(|_| { - SigstoreError::UnexpectedError(String::from("Cannot convert metadata_base to URL")) - })?; - let target_base = url::Url::parse(SIGSTORE_TARGET_BASE).map_err(|_| { - SigstoreError::UnexpectedError(String::from("Cannot convert target_base to URL")) - })?; - - let repository_helper = RepositoryHelper::new( - SIGSTORE_ROOT.as_bytes(), - metadata_base, - target_base, - checkout_dir, - )?; - - let fulcio_certs = repository_helper.fulcio_certs()?; - - let rekor_pub_key = repository_helper.rekor_pub_key().map(|data| { - String::from_utf8(data).map_err(|e| { - SigstoreError::UnexpectedError(format!( - "Cannot parse Rekor's public key obtained from TUF repository: {e}", - )) - }) - })??; - - Ok(SigstoreRepository { - rekor_pub_key, - fulcio_certs, - }) + /// **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(); + + if keys.len() != 1 { + Err(SigstoreError::TufMetadataError( + "Did not find exactly 1 active Rekor key", + )) + } 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(); + + match range { + // If there was no validity period specified, the key is always valid. + 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. + _ 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, + }, } +} + +/// 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 { + Ok((true, None)) + }?; - /// Rekor public key - pub fn rekor_pub_key(&self) -> &str { - &self.rekor_pub_key + 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) + } } +} - /// Fulcio certificate - pub fn fulcio_certs(&self) -> &[crate::registry::Certificate] { - &self.fulcio_certs +/// 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) +} diff --git a/src/tuf/repository_helper.rs b/src/tuf/repository_helper.rs index a581619638..1baeac693e 100644 --- a/src/tuf/repository_helper.rs +++ b/src/tuf/repository_helper.rs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use rustls_pki_types::CertificateDer; use sha2::{Digest, Sha256}; use std::fs; use std::io::Read; @@ -20,14 +21,13 @@ use std::path::{Path, PathBuf}; use tough::{RepositoryLoader, TargetName}; use url::Url; -use super::{ - super::errors::{Result, SigstoreError}, - constants::{SIGSTORE_FULCIO_CERT_TARGET_REGEX, SIGSTORE_REKOR_PUB_KEY_TARGET}, -}; +use super::super::errors::{Result, SigstoreError}; +use super::trustroot::{CertificateAuthority, TimeRange, TransparencyLogInstance, TrustedRoot}; pub(crate) struct RepositoryHelper { repository: tough::Repository, checkout_dir: Option, + trusted_root: Option, } impl RepositoryHelper { @@ -40,7 +40,7 @@ impl RepositoryHelper { where R: Read, { - let repository = RepositoryLoader::new(root, metadata_base, target_base) + let repository = RepositoryLoader::new(SIGSTORE_ROOT, metadata_base, target_base) .expiration_enforcement(tough::ExpirationEnforcement::Safe) .load() .map_err(Box::new)?; @@ -48,68 +48,130 @@ impl RepositoryHelper { Ok(Self { repository, checkout_dir: checkout_dir.map(|s| s.to_owned()), + trusted_root: None, }) } + pub(crate) fn from_repo(repo: tough::Repository, checkout_dir: Option<&Path>) -> Self { + Self { + repository: repo, + checkout_dir: checkout_dir.map(|s| s.to_owned()), + trusted_root: None, + } + } + + fn trusted_root(&self) -> Result<&TrustedRoot> { + if let Some(result) = self.trusted_root { + return Ok(&result); + } + + let trusted_root_target = TargetName::new("trusted_root.json").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 result = serde_json::from_slice(&data[..])?; + Ok(self.trusted_root.insert(result)) + } + + #[inline] + fn tlog_keys(&self, tlogs: &Vec) -> Vec<&[u8]> { + let mut result = Vec::new(); + + for key in tlogs { + // We won't accept expired keys for transparency logs. + if !is_timerange_valid(key.public_key.valid_for, false) { + continue; + } + + if let Some(raw) = key.public_key.raw_bytes { + result.push(&raw[..]); + } + } + + result + } + + #[inline] + fn ca_keys(&self, cas: &Vec, allow_expired: bool) -> Vec<&[u8]> { + let mut certs = Vec::new(); + + for ca in cas { + if !is_timerange_valid(Some(ca.valid_for), allow_expired) { + continue; + } + + let certs_in_ca = ca.cert_chain.certificates; + certs.extend(certs_in_ca.iter().map(|cert| &cert.raw_bytes[..])); + } + + return certs; + } + /// Fetch Fulcio certificates from the given TUF repository or reuse /// the local cache if its contents are not outdated. /// /// The contents of the local cache are updated when they are outdated. - pub(crate) fn fulcio_certs(&self) -> Result> { - let fulcio_target_names = self.fulcio_cert_target_names(); - let mut certs = vec![]; - - for fulcio_target_name in &fulcio_target_names { - let local_fulcio_path = self - .checkout_dir - .as_ref() - .map(|d| Path::new(d).join(fulcio_target_name.raw())); - - let cert_data = fetch_target_or_reuse_local_cache( - &self.repository, - fulcio_target_name, - local_fulcio_path.as_ref(), - )?; - certs.push(crate::registry::Certificate { - data: cert_data, - encoding: crate::registry::CertificateEncoding::Pem, - }); - } - Ok(certs) - } + pub(crate) fn fulcio_certs(&self) -> Result> { + let root = self.trusted_root()?; + + // 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.iter().map(|v| CertificateDer::from(*v)).collect(); - fn fulcio_cert_target_names(&self) -> Vec { - self.repository - .targets() - .signed - .targets_iter() - .filter_map(|(target_name, _target)| { - if SIGSTORE_FULCIO_CERT_TARGET_REGEX.is_match(target_name.raw()) { - Some(target_name.clone()) - } else { - None - } - }) - .collect() + if certs.is_empty() { + Err(SigstoreError::TufMetadataError( + "Fulcio certificates not found", + )) + } else { + Ok(certs) + } } - /// Fetch Rekor public key from the given TUF repository or reuse + /// Fetch Rekor 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. - pub(crate) fn rekor_pub_key(&self) -> Result> { - let rekor_target_name = TargetName::new(SIGSTORE_REKOR_PUB_KEY_TARGET).map_err(Box::new)?; + pub(crate) fn rekor_keys(&self) -> Result> { + let root = self.trusted_root()?; + let keys = self.tlog_keys(&root.tlogs); - let local_rekor_path = self - .checkout_dir - .as_ref() - .map(|d| Path::new(d).join(SIGSTORE_REKOR_PUB_KEY_TARGET)); + if keys.len() != 1 { + Err(SigstoreError::TufMetadataError( + "Did not find exactly 1 active Rekor key", + )) + } else { + Ok(keys) + } + } +} - fetch_target_or_reuse_local_cache( - &self.repository, - &rekor_target_name, - local_rekor_path.as_ref(), - ) +/// 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, allow_expired: bool) -> bool { + let time = chrono::Utc::now(); + + match range { + // If there was no validity period specified, the key is always valid. + 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. + _ 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, + }, } } @@ -252,63 +314,94 @@ mod tests { )) })?; // It's fine to ignore timestamp.json expiration inside of test env - let repo = - RepositoryLoader::new(SIGSTORE_ROOT.as_bytes(), metadata_base_url, target_base_url) - .expiration_enforcement(tough::ExpirationEnforcement::Unsafe) - .load() - .map_err(Box::new)?; + let repo = RepositoryLoader::new(SIGSTORE_ROOT, metadata_base_url, target_base_url) + .expiration_enforcement(tough::ExpirationEnforcement::Unsafe) + .load() + .map_err(Box::new)?; Ok(repo) } - #[test] - fn get_files_without_using_local_cache() { - let repository = local_tuf_repo().expect("Local TUF repo should not fail"); - let helper = RepositoryHelper { - repository, - checkout_dir: None, - }; + fn find_target(name: &str) -> Result { + let path = test_data().join("repository").join("targets"); + + for entry in fs::read_dir(path)? { + let path = entry?.path(); + if path.is_dir() { + continue; + } + + // Heuristic: Filter for consistent snapshot targets. SHA256 hashes in hexadecimal + // comprise of 64 characters, so our filename must be at least that long. The TUF repo + // shouldn't ever contain paths with invalid Unicode (knock on wood), so we're doing + // the lossy OsStr conversion here. + let filename = path.file_name().unwrap().to_str().unwrap(); + if filename.len() < 64 { + continue; + } + + // Heuristic: see if the filename is in consistent snapshot format (.). + // NB: The consistent snapshot prefix should be ASCII, so indexing the string as + // bytes is safe enough. + if filename.as_bytes()[64] != b'.' { + continue; + } + + // At this point, we're probably dealing with a consistent snapshot. + // Check if the name matches. + if filename.ends_with(name) { + return Ok(path); + } + } + + Err(SigstoreError::UnexpectedError( + "Couldn't find a matching target".to_string(), + )) + } - let mut actual = helper.fulcio_certs().expect("fulcio certs cannot be read"); + fn check_against_disk(helper: &RepositoryHelper) { + let mut actual: Vec<&[u8]> = helper + .fulcio_certs() + .expect("fulcio certs could not be read") + .iter() + .map(|c| c.as_ref()) + .collect(); + let expected = ["fulcio.crt.pem", "fulcio_v1.crt.pem"].iter().map(|t| { + let path = find_target(t)?; + Ok(fs::read(path)?) + }); + let mut expected = expected + .collect::>>>() + .expect("could not find targets"); actual.sort(); - let mut expected: Vec = - ["fulcio.crt.pem", "fulcio_v1.crt.pem"] - .iter() - .map(|filename| { - let data = fs::read( - test_data() - .join("repository") - .join("targets") - .join(filename), - ) - .unwrap_or_else(|_| panic!("cannot read {} from test data", filename)); - crate::registry::Certificate { - data, - encoding: crate::registry::CertificateEncoding::Pem, - } - }) - .collect(); expected.sort(); - assert_eq!( - actual, expected, - "The fulcio cert read from the TUF repository is not what was expected" - ); + assert_eq!(actual, expected, "The fulcio cert is not what was expected"); - let actual = helper.rekor_pub_key().expect("rekor key cannot be read"); - let expected = fs::read( - test_data() - .join("repository") - .join("targets") - .join("rekor.pub"), - ) - .expect("cannot read rekor key from test data"); + let actual = helper.rekor_keys().expect("rekor key cannot be read"); + let expected = fs::read(find_target("rekor.pub").expect("could not find targets")) + .expect("cannot read rekor key from test data"); + let expected = pem::parse(expected).unwrap(); + assert_eq!(expected.tag(), "PUBLIC KEY"); assert_eq!( - actual, expected, - "The rekor key read from the TUF repository is not what was expected" + actual, + &[expected.contents()], + "The rekor key is not what was expected" ); } + #[test] + fn get_files_without_using_local_cache() { + let repository = local_tuf_repo().expect("Local TUF repo should not fail"); + let helper = RepositoryHelper { + repository, + checkout_dir: None, + trusted_root: None, + }; + + check_against_disk(&helper); + } + #[test] fn download_files_to_local_cache() { let cache_dir = TempDir::new().expect("Cannot create temp cache dir"); @@ -317,42 +410,10 @@ mod tests { let helper = RepositoryHelper { repository, checkout_dir: Some(cache_dir.path().to_path_buf()), + trusted_root: None, }; - let mut actual = helper.fulcio_certs().expect("fulcio certs cannot be read"); - actual.sort(); - let mut expected: Vec = - ["fulcio.crt.pem", "fulcio_v1.crt.pem"] - .iter() - .map(|filename| { - let data = fs::read( - test_data() - .join("repository") - .join("targets") - .join(filename), - ) - .unwrap_or_else(|_| panic!("cannot read {} from test data", filename)); - crate::registry::Certificate { - data, - encoding: crate::registry::CertificateEncoding::Pem, - } - }) - .collect(); - expected.sort(); - - assert_eq!( - actual, expected, - "The fulcio cert read from the cache dir is not what was expected" - ); - - let expected = helper.rekor_pub_key().expect("rekor key cannot be read"); - let actual = fs::read(cache_dir.path().join("rekor.pub")) - .expect("cannot read rekor key from cache dir"); - - assert_eq!( - actual, expected, - "The rekor key read from the cache dir is not what was expected" - ); + check_against_disk(&helper); } #[test] @@ -365,8 +426,8 @@ mod tests { .expect("Cannot write file to cache dir"); } fs::write( - cache_dir.path().join(SIGSTORE_REKOR_PUB_KEY_TARGET), - b"fake rekor", + cache_dir.path().join("trusted_root.json"), + b"fake trusted root", ) .expect("Cannot write file to cache dir"); @@ -374,41 +435,22 @@ mod tests { let helper = RepositoryHelper { repository, checkout_dir: Some(cache_dir.path().to_path_buf()), + trusted_root: None, }; - let mut actual = helper.fulcio_certs().expect("fulcio certs cannot be read"); - actual.sort(); - let mut expected: Vec = - ["fulcio.crt.pem", "fulcio_v1.crt.pem"] - .iter() - .map(|filename| { - let data = fs::read( - test_data() - .join("repository") - .join("targets") - .join(filename), - ) - .unwrap_or_else(|_| panic!("cannot read {} from test data", filename)); - crate::registry::Certificate { - data, - encoding: crate::registry::CertificateEncoding::Pem, - } - }) - .collect(); - expected.sort(); + check_against_disk(&helper); + } - assert_eq!( - actual, expected, - "The fulcio cert read from the TUF repository is not what was expected" - ); + #[test] + fn deser_trusted_root() { + let metadata_base_path = test_data().join("repository"); + let targets_base_path = metadata_base_path.join("targets"); - let expected = helper.rekor_pub_key().expect("rekor key cannot be read"); - let actual = fs::read(cache_dir.path().join("rekor.pub")) - .expect("cannot read rekor key from cache dir"); + let repository = local_tuf_repo().expect("Local TUF repo should not fail"); + let helper = RepositoryHelper::from_repo(repository, None); - assert_eq!( - actual, expected, - "The rekor key read from the cache dir is not what was expected" - ); + helper + .trusted_root() + .expect("Trusted Root should deserialize"); } } diff --git a/src/tuf/trustroot.rs b/src/tuf/trustroot.rs new file mode 100644 index 0000000000..cffd1f15d1 --- /dev/null +++ b/src/tuf/trustroot.rs @@ -0,0 +1,122 @@ +#![allow(dead_code)] + +use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Deserializer}; + +fn deserialize_option_base64<'de, D>(de: D) -> std::result::Result>, D::Error> +where + D: Deserializer<'de>, +{ + let buf: Option<&str> = Deserialize::deserialize(de)?; + + match buf { + Some(s) => Ok(Some(base64.decode(s).map_err(serde::de::Error::custom)?)), + _ => Ok(None), + } +} + +fn deserialize_base64<'de, D>(de: D) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + let buf: &str = Deserialize::deserialize(de)?; + + base64.decode(buf).map_err(serde::de::Error::custom) +} + +#[derive(Deserialize, PartialEq, Debug)] +#[allow(non_camel_case_types)] +pub(crate) enum HashAlgorithm { + HASH_ALGORITHM_UNSPECIFIED = 0, + SHA2_256 = 1, +} + +#[derive(Deserialize, PartialEq, Debug)] +#[allow(non_camel_case_types)] +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 +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct LogId { + #[serde(deserialize_with = "deserialize_base64")] + pub key_id: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct TimeRange { + pub start: DateTime, + pub end: Option>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PublicKey { + #[serde(deserialize_with = "deserialize_option_base64")] + pub raw_bytes: Option>, + pub key_details: PublicKeyDetails, + pub valid_for: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct DistinguishedName { + pub organization: String, + pub common_name: String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct X509Certificate { + #[serde(deserialize_with = "deserialize_base64")] + pub raw_bytes: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct X509CertificateChain { + pub certificates: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct TransparencyLogInstance { + pub base_url: String, + pub hash_algorithm: HashAlgorithm, + pub public_key: PublicKey, + pub log_id: LogId, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CertificateAuthority { + pub subject: DistinguishedName, + pub uri: Option, + pub cert_chain: X509CertificateChain, + pub valid_for: TimeRange, +} + +// HACK: We should probably use definitions from sigstore-protobuf-specs, but +// the autogenerated definitions are unergonomic. Declare it locally here. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct TrustedRoot { + pub media_type: String, + pub tlogs: Vec, + pub certificate_authorities: Vec, + pub ctlogs: Vec, + pub timestamp_authorities: Vec, +} diff --git a/trust_root/prod/root.json b/trust_root/prod/root.json new file mode 100644 index 0000000000..38f80f9404 --- /dev/null +++ b/trust_root/prod/root.json @@ -0,0 +1,156 @@ +{ + "signed": { + "_type": "root", + "spec_version": "1.0", + "version": 5, + "expires": "2023-04-18T18:13:43Z", + "keys": { + "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXsz3SZXFb8jMV42j6pJlyjbjR8K\nN3Bwocexq6LMIb5qsWKOQvLN16NUefLc4HswOoumRsVVaajSpQS6fobkRw==\n-----END PUBLIC KEY-----\n" + } + }, + "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0ghrh92Lw1Yr3idGV5WqCtMDB8Cx\n+D8hdC4w2ZLNIplVRoVGLskYa3gheMyOjiJ8kPi15aQ2//7P+oj7UvJPGw==\n-----END PUBLIC KEY-----\n" + } + }, + "45b283825eb184cabd582eb17b74fc8ed404f68cf452acabdad2ed6f90ce216b": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELrWvNt94v4R085ELeeCMxHp7PldF\n0/T1GxukUh2ODuggLGJE0pc1e8CSBf6CS91Fwo9FUOuRsjBUld+VqSyCdQ==\n-----END PUBLIC KEY-----\n" + } + }, + "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEinikSsAQmYkNeH5eYq/CnIzLaacO\nxlSaawQDOwqKy/tCqxq5xxPSJc21K4WIhs9GyOkKfzueY3GILzcMJZ4cWw==\n-----END PUBLIC KEY-----\n" + } + }, + "e1863ba02070322ebc626dcecf9d881a3a38c35c3b41a83765b6ad6c37eaec2a": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWRiGr5+j+3J5SsH+Ztr5nE2H2wO7\nBV+nO3s93gLca18qTOzHY1oWyAGDykMSsGTUBSt9D+An0KfKsD2mfSM42Q==\n-----END PUBLIC KEY-----\n" + } + }, + "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBzVOmHCPojMVLSI364WiiV8NPrD\n6IgRxVliskz/v+y3JER5mcVGcONliDcWMC5J2lfHmjPNPhb4H7xm8LzfSA==\n-----END PUBLIC KEY-----\n" + } + }, + "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEy8XKsmhBYDI8Jc0GwzBxeKax0cm5\nSTKEU65HPFunUn41sT8pi0FjM4IkHz/YUmwmLUO0Wt7lxhj6BkLIK4qYAw==\n-----END PUBLIC KEY-----\n" + } + } + }, + "roles": { + "root": { + "keyids": [ + "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c", + "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99", + "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f", + "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b", + "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de" + ], + "threshold": 3 + }, + "snapshot": { + "keyids": [ + "45b283825eb184cabd582eb17b74fc8ed404f68cf452acabdad2ed6f90ce216b" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c", + "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99", + "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f", + "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b", + "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de" + ], + "threshold": 3 + }, + "timestamp": { + "keyids": [ + "e1863ba02070322ebc626dcecf9d881a3a38c35c3b41a83765b6ad6c37eaec2a" + ], + "threshold": 1 + } + }, + "consistent_snapshot": true + }, + "signatures": [ + { + "keyid": "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c", + "sig": "3045022100fc1c2be509ce50ea917bbad1d9efe9d96c8c2ebea04af2717aa3d9c6fe617a75022012eef282a19f2d8bd4818aa333ef48a06489f49d4d34a20b8fe8fc867bb25a7a" + }, + { + "keyid": "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99", + "sig": "30450221008a4392ae5057fc00778b651e61fea244766a4ae58db84d9f1d3810720ab0f3b702207c49e59e8031318caf02252ecea1281cecc1e5986c309a9cef61f455ecf7165d" + }, + { + "keyid": "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b", + "sig": "3046022100da1b8dc5d53aaffbbfac98de3e23ee2d2ad3446a7bed09fac0f88bae19be2587022100b681c046afc3919097dfe794e0d819be891e2e850aade315bec06b0c4dea221b" + }, + { + "keyid": "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de", + "sig": "3046022100b534e0030e1b271133ecfbdf3ba9fbf3becb3689abea079a2150afbb63cdb7c70221008c39a718fd9495f249b4ab8788d5b9dc269f0868dbe38b272f48207359d3ded9" + }, + { + "keyid": "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", + "sig": "3045022100fc1c2be509ce50ea917bbad1d9efe9d96c8c2ebea04af2717aa3d9c6fe617a75022012eef282a19f2d8bd4818aa333ef48a06489f49d4d34a20b8fe8fc867bb25a7a" + }, + { + "keyid": "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", + "sig": "30450221008a4392ae5057fc00778b651e61fea244766a4ae58db84d9f1d3810720ab0f3b702207c49e59e8031318caf02252ecea1281cecc1e5986c309a9cef61f455ecf7165d" + }, + { + "keyid": "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209", + "sig": "3046022100da1b8dc5d53aaffbbfac98de3e23ee2d2ad3446a7bed09fac0f88bae19be2587022100b681c046afc3919097dfe794e0d819be891e2e850aade315bec06b0c4dea221b" + }, + { + "keyid": "75e867ab10e121fdef32094af634707f43ddd79c6bab8ad6c5ab9f03f4ea8c90", + "sig": "3046022100b534e0030e1b271133ecfbdf3ba9fbf3becb3689abea079a2150afbb63cdb7c70221008c39a718fd9495f249b4ab8788d5b9dc269f0868dbe38b272f48207359d3ded9" + } + ] +} \ No newline at end of file diff --git a/trust_root/prod/trusted_root.json b/trust_root/prod/trusted_root.json new file mode 100644 index 0000000000..bb4e6fcd88 --- /dev/null +++ b/trust_root/prod/trusted_root.json @@ -0,0 +1,91 @@ +{ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "https://rekor.sigstore.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27.000Z" + } + }, + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" + } + ] + }, + "validFor": { + "start": "2021-03-07T03:20:29.000Z", + "end": "2022-12-31T23:59:59.999Z" + } + }, + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" + }, + { + "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" + } + ] + }, + "validFor": { + "start": "2022-04-13T20:06:15.000Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.sigstore.dev/test", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-03-14T00:00:00.000Z", + "end": "2022-10-31T23:59:59.999Z" + } + }, + "logId": { + "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" + } + }, + { + "baseUrl": "https://ctfe.sigstore.dev/2022", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-10-20T00:00:00.000Z" + } + }, + "logId": { + "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" + } + } + ], + "timestampAuthorities": [] +}