From 8a269a304c3b3ed1c3a6c89e4a6a88f9af22598a Mon Sep 17 00:00:00 2001 From: Jack Leightcap Date: Fri, 20 Oct 2023 13:32:03 -0400 Subject: [PATCH 1/3] cosign/tuf: init Signed-off-by: Jack Leightcap Co-authored-by: Andrew Pan --- Cargo.toml | 6 +- examples/cosign/verify/main.rs | 42 +- src/cosign/client.rs | 10 +- src/cosign/client_builder.rs | 72 +-- src/cosign/mod.rs | 24 +- src/cosign/signature_layers.rs | 25 +- .../certificate_verifier.rs | 9 +- src/crypto/certificate_pool.rs | 172 +++---- src/errors.rs | 11 +- src/lib.rs | 16 +- src/registry/config.rs | 18 + src/tuf/constants.rs | 174 +------ src/tuf/mod.rs | 393 +++++++++++----- src/tuf/repository_helper.rs | 428 ++++++++---------- src/tuf/trustroot.rs | 148 ++++++ trust_root/prod/root.json | 156 +++++++ trust_root/prod/trusted_root.json | 91 ++++ 17 files changed, 1048 insertions(+), 747 deletions(-) create mode 100644 src/tuf/trustroot.rs create mode 100644 trust_root/prod/root.json create mode 100644 trust_root/prod/trusted_root.json diff --git a/Cargo.toml b/Cargo.toml index 2ee5951e55..b91f598203 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,6 +110,7 @@ rsa = "0.9.2" scrypt = "0.11.0" serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.79" +serde_with = { version = "3.4.0", features = ["base64"] } sha2 = { version = "0.10.6", features = ["oid"] } signature = { version = "2.0" } thiserror = "1.0.30" @@ -117,9 +118,12 @@ tokio = { version = "1.17.0", features = ["rt"] } tough = { version = "0.14", features = ["http"], optional = true } tracing = "0.1.31" url = "2.2.2" -x509-cert = { version = "0.2.2", features = ["pem", "std"] } +x509-cert = { version = "0.2.2", features = ["builder", "pem", "std"] } crypto_secretbox = "0.1.1" zeroize = "1.5.7" +rustls-webpki = { version = "0.102.0-alpha.4", features = ["alloc"] } +rustls-pki-types = { version = "0.2.1", features = ["std"] } +serde_repr = "0.1.16" [dev-dependencies] anyhow = { version = "1.0", features = ["backtrace"] } diff --git a/examples/cosign/verify/main.rs b/examples/cosign/verify/main.rs index b1d3cdcc67..60369cf094 100644 --- a/examples/cosign/verify/main.rs +++ b/examples/cosign/verify/main.rs @@ -110,7 +110,7 @@ struct Cli { async fn run_app( cli: &Cli, - frd: &FulcioAndRekorData, + frd: &dyn sigstore::tuf::Repository, ) -> anyhow::Result<(Vec, VerificationConstraintVec)> { // Note well: this a limitation deliberately introduced by this example. if cli.cert_email.is_some() && cli.cert_url.is_some() { @@ -133,20 +133,13 @@ async fn run_app( let mut client_builder = sigstore::cosign::ClientBuilder::default().with_oci_client_config(oci_client_config); - - if let Some(key) = frd.rekor_pub_key.as_ref() { - client_builder = client_builder.with_rekor_pub_key(key); - } + client_builder = client_builder.with_trust_repository(frd)?; let cert_chain: Option> = match cli.cert_chain.as_ref() { None => None, Some(cert_chain_path) => Some(parse_cert_bundle(cert_chain_path)?), }; - if !frd.fulcio_certs.is_empty() { - client_builder = client_builder.with_fulcio_certs(&frd.fulcio_certs); - } - if cli.enable_registry_caching { client_builder = client_builder.enable_registry_caching(); } @@ -194,7 +187,7 @@ async fn run_app( } if let Some(path_to_cert) = cli.cert.as_ref() { let cert = fs::read(path_to_cert).map_err(|e| anyhow!("Cannot read cert: {:?}", e))?; - let require_rekor_bundle = if frd.rekor_pub_key.is_some() { + let require_rekor_bundle = if !frd.rekor_keys()?.is_empty() { true } else { warn!("certificate based verification is weaker when Rekor integration is disabled"); @@ -235,31 +228,22 @@ async fn run_app( Ok((trusted_layers, verification_constraints)) } -#[derive(Default)] -struct FulcioAndRekorData { - pub rekor_pub_key: Option, - pub fulcio_certs: Vec, -} - -async fn fulcio_and_rekor_data(cli: &Cli) -> anyhow::Result { - let mut data = FulcioAndRekorData::default(); - +async fn fulcio_and_rekor_data(cli: &Cli) -> anyhow::Result> { if cli.use_sigstore_tuf_data { let repo: sigstore::errors::Result = spawn_blocking(|| { info!("Downloading data from Sigstore TUF repository"); - sigstore::tuf::SigstoreRepository::fetch(None) + SigstoreRepository::new(None)?.prefetch() }) .await .map_err(|e| anyhow!("Error spawning blocking task inside of tokio: {}", e))?; - let repo: SigstoreRepository = repo?; - data.fulcio_certs = repo.fulcio_certs().into(); - data.rekor_pub_key = Some(repo.rekor_pub_key().to_string()); + return Ok(Box::new(repo?)); }; + let mut data = sigstore::tuf::FakeRepository::default(); if let Some(path) = cli.rekor_pub_key.as_ref() { - data.rekor_pub_key = Some( - fs::read_to_string(path) + data.rekor_key = Some( + fs::read(path) .map_err(|e| anyhow!("Error reading rekor public key from disk: {}", e))?, ); } @@ -272,10 +256,12 @@ async fn fulcio_and_rekor_data(cli: &Cli) -> anyhow::Result encoding: sigstore::registry::CertificateEncoding::Pem, data: cert_data, }; - data.fulcio_certs.push(certificate); + data.fulcio_certs + .get_or_insert(Vec::new()) + .push(certificate.try_into()?); } - Ok(data) + Ok(Box::new(data)) } #[tokio::main] @@ -304,7 +290,7 @@ pub async fn main() { println!("Loop {}/{}", n + 1, cli.loops); } - match run_app(&cli, &frd).await { + match run_app(&cli, frd.as_ref()).await { Ok((trusted_layers, verification_constraints)) => { let filter_result = sigstore::cosign::verify_constraints( &trusted_layers, diff --git a/src/cosign/client.rs b/src/cosign/client.rs index 1c97e113af..af70bf457b 100644 --- a/src/cosign/client.rs +++ b/src/cosign/client.rs @@ -37,15 +37,15 @@ pub const CONFIG_DATA: &str = "{}"; /// Cosign Client /// /// Instances of `Client` can be built via [`sigstore::cosign::ClientBuilder`](crate::cosign::ClientBuilder). -pub struct Client { +pub struct Client<'a> { pub(crate) registry_client: Box, pub(crate) rekor_pub_key: Option, - pub(crate) fulcio_cert_pool: Option, + pub(crate) fulcio_cert_pool: Option>, } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl CosignCapabilities for Client { +impl CosignCapabilities for Client<'_> { async fn triangulate( &mut self, image: &OciReference, @@ -140,7 +140,7 @@ impl CosignCapabilities for Client { } } -impl Client { +impl Client<'_> { /// Internal helper method used to fetch data from an OCI registry async fn fetch_manifest_and_layers( &mut self, @@ -177,7 +177,7 @@ mod tests { use crate::crypto::SigningScheme; use crate::mock_client::test::MockOciClient; - fn build_test_client(mock_client: MockOciClient) -> Client { + fn build_test_client(mock_client: MockOciClient) -> Client<'static> { let rekor_pub_key = CosignVerificationKey::from_pem(REKOR_PUB_KEY.as_bytes(), &SigningScheme::default()) .expect("Cannot create CosignVerificationKey"); diff --git a/src/cosign/client_builder.rs b/src/cosign/client_builder.rs index 83b58a9ad1..f076e83528 100644 --- a/src/cosign/client_builder.rs +++ b/src/cosign/client_builder.rs @@ -13,13 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +use rustls_pki_types::CertificateDer; use tracing::info; use super::client::Client; use crate::crypto::SigningScheme; use crate::crypto::{certificate_pool::CertificatePool, CosignVerificationKey}; use crate::errors::Result; -use crate::registry::{Certificate, ClientConfig}; +use crate::registry::ClientConfig; +use crate::tuf::Repository; /// A builder that generates Client objects. /// @@ -34,7 +36,7 @@ use crate::registry::{Certificate, ClientConfig}; /// ## Fulcio integration /// /// Fulcio integration can be enabled by specifying Fulcio's certificate. -/// This can be provided via the [`ClientBuilder::with_fulcio_cert`] method. +/// This can be provided via the [`ClientBuilder::with_fulcio_certs`] method. /// /// > Note well: the [`tuf`](crate::tuf) module provides helper structs and methods /// > to obtain this data from the official TUF repository of the Sigstore project. @@ -50,15 +52,16 @@ use crate::registry::{Certificate, ClientConfig}; /// /// Each cached entry will automatically expire after 60 seconds. #[derive(Default)] -pub struct ClientBuilder { +pub struct ClientBuilder<'a> { oci_client_config: ClientConfig, - rekor_pub_key: Option, - fulcio_certs: Vec, + rekor_pub_key: Option<&'a [u8]>, + fulcio_certs: Vec>, + // repo: Repository #[cfg(feature = "cached-client")] enable_registry_caching: bool, } -impl ClientBuilder { +impl<'a> ClientBuilder<'a> { /// Enable caching of data returned from remote OCI registries #[cfg(feature = "cached-client")] pub fn enable_registry_caching(mut self) -> Self { @@ -66,47 +69,18 @@ impl ClientBuilder { self } - /// Specify the public key used by Rekor. + /// Optional - Configures the roots of trust. /// - /// The public key can be obtained by using the helper methods under the - /// [`tuf`](crate::tuf) module. - /// - /// `key` is a PEM encoded public key - /// - /// When provided, this enables Rekor's integration. - pub fn with_rekor_pub_key(mut self, key: &str) -> Self { - self.rekor_pub_key = Some(key.to_string()); - self - } - - /// Specify the certificate used by Fulcio. This method can be invoked - /// multiple times to add all the certificates that Fulcio used over the - /// time. - /// - /// `cert` is a PEM encoded certificate - /// - /// The certificates can be obtained by using the helper methods under the - /// [`tuf`](crate::tuf) module. - /// - /// When provided, this enables Fulcio's integration. - pub fn with_fulcio_cert(mut self, cert: &[u8]) -> Self { - let certificate = Certificate { - encoding: crate::registry::CertificateEncoding::Pem, - data: cert.to_owned(), - }; - self.fulcio_certs.push(certificate); - self - } + /// Enables Fulcio and Rekor integration with the given trust repository. + /// See [crate::tuf::Repository] for more details on trust repositories. + pub fn with_trust_repository(mut self, repo: &'a R) -> Result { + let rekor_keys = repo.rekor_keys()?; + if !rekor_keys.is_empty() { + self.rekor_pub_key = Some(rekor_keys[0]); + } + self.fulcio_certs = repo.fulcio_certs()?; - /// Specify the certificates used by Fulcio. - /// - /// The certificates can be obtained by using the helper methods under the - /// [`tuf`](crate::tuf) module. - /// - /// When provided, this enables Fulcio's integration. - pub fn with_fulcio_certs(mut self, certs: &[crate::registry::Certificate]) -> Self { - self.fulcio_certs = certs.to_vec(); - self + Ok(self) } /// Optional - the configuration to be used by the OCI client. @@ -118,14 +92,14 @@ impl ClientBuilder { self } - pub fn build(self) -> Result { + pub fn build(self) -> Result> { let rekor_pub_key = match self.rekor_pub_key { None => { info!("Rekor public key not provided. Rekor integration disabled"); None } - Some(data) => Some(CosignVerificationKey::from_pem( - data.as_bytes(), + Some(data) => Some(CosignVerificationKey::from_der( + data, &SigningScheme::default(), )?), }; @@ -134,7 +108,7 @@ impl ClientBuilder { info!("No Fulcio cert has been provided. Fulcio integration disabled"); None } else { - let cert_pool = CertificatePool::from_certificates(&self.fulcio_certs)?; + let cert_pool = CertificatePool::from_certificates(self.fulcio_certs, [])?; Some(cert_pool) }; diff --git a/src/cosign/mod.rs b/src/cosign/mod.rs index 187826bb66..4e6be029f2 100644 --- a/src/cosign/mod.rs +++ b/src/cosign/mod.rs @@ -282,6 +282,7 @@ where #[cfg(test)] mod tests { + use rustls_pki_types::CertificateDer; use serde_json::json; use std::collections::HashMap; @@ -335,18 +336,15 @@ TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ #[cfg(feature = "test-registry")] const SIGNED_IMAGE: &str = "busybox:1.34"; - pub(crate) fn get_fulcio_cert_pool() -> CertificatePool { - let certificates = vec![ - crate::registry::Certificate { - encoding: crate::registry::CertificateEncoding::Pem, - data: FULCIO_CRT_1_PEM.as_bytes().to_vec(), - }, - crate::registry::Certificate { - encoding: crate::registry::CertificateEncoding::Pem, - data: FULCIO_CRT_2_PEM.as_bytes().to_vec(), - }, - ]; - CertificatePool::from_certificates(&certificates).unwrap() + pub(crate) fn get_fulcio_cert_pool() -> CertificatePool<'static> { + fn pem_to_der<'a>(input: &'a str) -> CertificateDer<'a> { + let pem_cert = pem::parse(input).unwrap(); + assert_eq!(pem_cert.tag(), "CERTIFICATE"); + CertificateDer::from(pem_cert.into_contents()) + } + let certificates = vec![pem_to_der(FULCIO_CRT_1_PEM), pem_to_der(FULCIO_CRT_2_PEM)]; + + CertificatePool::from_certificates(certificates, []).unwrap() } pub(crate) fn get_rekor_public_key() -> CosignVerificationKey { @@ -645,7 +643,7 @@ TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ } #[cfg(feature = "test-registry")] - async fn prepare_image_to_be_signed(client: &mut Client, image_ref: &OciReference) { + async fn prepare_image_to_be_signed(client: &mut Client<'_>, image_ref: &OciReference) { let data = client .registry_client .pull( diff --git a/src/cosign/signature_layers.rs b/src/cosign/signature_layers.rs index de7a7cf8f4..b26fcbc5f0 100644 --- a/src/cosign/signature_layers.rs +++ b/src/cosign/signature_layers.rs @@ -438,7 +438,12 @@ impl CertificateSignature { let integrated_time = trusted_bundle.payload.integrated_time; // ensure the certificate has been issued by Fulcio - fulcio_cert_pool.verify_pem_cert(cert_pem)?; + fulcio_cert_pool.verify_pem_cert( + cert_pem, + Some(rustls_pki_types::UnixTime::since_unix_epoch( + cert.tbs_certificate.validity.not_before.to_unix_duration(), + )), + )?; crypto::certificate::is_trusted(&cert, integrated_time)?; @@ -899,8 +904,10 @@ JsB89BPhZYch0U0hKANx5TY+ncrm0s8bfJxxHoenAEFhwhuXeb4PqIrtoQ== let issued_cert_pem = issued_cert.cert.to_pem()?; - let certs = vec![crate::registry::Certificate::try_from(ca_data.cert).unwrap()]; - let cert_pool = CertificatePool::from_certificates(&certs).unwrap(); + let certs = vec![crate::registry::Certificate::try_from(ca_data.cert) + .unwrap() + .try_into()?]; + let cert_pool = CertificatePool::from_certificates(certs, []).unwrap(); let integrated_time = Utc::now().checked_sub_signed(Duration::minutes(1)).unwrap(); let bundle = Bundle { @@ -946,8 +953,10 @@ JsB89BPhZYch0U0hKANx5TY+ncrm0s8bfJxxHoenAEFhwhuXeb4PqIrtoQ== let issued_cert_pem = issued_cert.cert.to_pem()?; - let certs = vec![crate::registry::Certificate::try_from(ca_data.cert).unwrap()]; - let cert_pool = CertificatePool::from_certificates(&certs).unwrap(); + let certs = vec![crate::registry::Certificate::try_from(ca_data.cert) + .unwrap() + .try_into()?]; + let cert_pool = CertificatePool::from_certificates(certs, []).unwrap(); let integrated_time = Utc::now().checked_sub_signed(Duration::minutes(1)).unwrap(); let bundle = Bundle { @@ -992,8 +1001,10 @@ JsB89BPhZYch0U0hKANx5TY+ncrm0s8bfJxxHoenAEFhwhuXeb4PqIrtoQ== let issued_cert_pem = issued_cert.cert.to_pem()?; - let certs = vec![crate::registry::Certificate::try_from(ca_data.cert).unwrap()]; - let cert_pool = CertificatePool::from_certificates(&certs).unwrap(); + let certs = vec![crate::registry::Certificate::try_from(ca_data.cert) + .unwrap() + .try_into()?]; + let cert_pool = CertificatePool::from_certificates(certs, []).unwrap(); let integrated_time = Utc::now().checked_sub_signed(Duration::minutes(1)).unwrap(); let bundle = Bundle { diff --git a/src/cosign/verification_constraint/certificate_verifier.rs b/src/cosign/verification_constraint/certificate_verifier.rs index bdfabffe8f..b002aa3bb3 100644 --- a/src/cosign/verification_constraint/certificate_verifier.rs +++ b/src/cosign/verification_constraint/certificate_verifier.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use pkcs8::der::Decode; +use rustls_pki_types::CertificateDer; use std::convert::TryFrom; use tracing::warn; use x509_cert::Certificate; @@ -61,8 +62,12 @@ impl CertificateVerifier { crate::crypto::certificate::verify_validity(&cert)?; if let Some(certs) = cert_chain { - let cert_pool = CertificatePool::from_certificates(certs)?; - cert_pool.verify_der_cert(cert_bytes)?; + let certs = certs + .iter() + .map(|c| CertificateDer::try_from(c.clone())) + .collect::>>()?; + let cert_pool = CertificatePool::from_certificates(certs, [])?; + cert_pool.verify_der_cert(cert_bytes, None)?; } let subject_public_key_info = &cert.tbs_certificate.subject_public_key_info; diff --git a/src/crypto/certificate_pool.rs b/src/crypto/certificate_pool.rs index 992e61b993..f7f702f804 100644 --- a/src/crypto/certificate_pool.rs +++ b/src/crypto/certificate_pool.rs @@ -13,141 +13,101 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{ - errors::{Result, SigstoreError}, - registry::Certificate, -}; +use const_oid::db::rfc5280::ID_KP_CODE_SIGNING; +use rustls_pki_types::{CertificateDer, TrustAnchor, UnixTime}; +use webpki::{EndEntityCert, KeyUsage}; -// The untrusted intermediate CA certificate, used for chain building -// TODO: Remove once this is bundled in TUF metadata. -const FULCIO_INTERMEDIATE_V1: &str = "-----BEGIN CERTIFICATE----- -MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw -KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y -MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl -LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C -AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7 -7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS -0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB -BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp -KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI -zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR -nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP -mygUY7Ii2zbdCdliiow= ------END CERTIFICATE-----"; +use crate::errors::{Result, SigstoreError}; -/// A collection of trusted root certificates +/// A collection of trusted root certificates. #[derive(Default, Debug)] -pub(crate) struct CertificatePool { - trusted_roots: Vec, - intermediates: Vec, +pub(crate) struct CertificatePool<'a> { + trusted_roots: Vec>, + intermediates: Vec>, } -impl CertificatePool { - /// Build a `CertificatePool` instance using the provided list of [`Certificate`] - pub(crate) fn from_certificates(certs: &[Certificate]) -> Result { - let mut trusted_roots = vec![]; - let mut intermediates = vec![]; - - for c in certs { - let pc = match c.encoding { - crate::registry::CertificateEncoding::Pem => { - let pem_str = String::from_utf8(c.data.clone()).map_err(|_| { - SigstoreError::X509Error("certificate is not PEM encoded".to_string()) - })?; - picky::x509::Cert::from_pem_str(&pem_str) - } - crate::registry::CertificateEncoding::Der => picky::x509::Cert::from_der(&c.data), - }?; - - match pc.ty() { - picky::x509::certificate::CertType::Root => { - trusted_roots.push(pc); - } - picky::x509::certificate::CertType::Intermediate => { - intermediates.push(pc); - } - _ => { - return Err(SigstoreError::CertificatePoolError( - "Cannot add a certificate that is no root or intermediate".to_string(), - )); - } - } - } - - // TODO: Remove once FULCIO_INTERMEDIATE_V1 is bundled in TUF metadata. - if intermediates.is_empty() { - intermediates.push(picky::x509::Cert::from_pem_str(FULCIO_INTERMEDIATE_V1)?); - } - +impl<'a> CertificatePool<'a> { + /// Builds a `CertificatePool` instance using the provided list of [`Certificate`]. + pub(crate) fn from_certificates( + trusted_roots: R, + untrusted_intermediates: I, + ) -> Result> + where + R: IntoIterator>, + I: IntoIterator>, + { Ok(CertificatePool { - trusted_roots, - intermediates, + trusted_roots: trusted_roots + .into_iter() + .map(|x| Ok(webpki::extract_trust_anchor(&x)?.to_owned())) + .collect::, webpki::Error>>()?, + intermediates: untrusted_intermediates.into_iter().collect(), }) } /// Ensures the given certificate has been issued by one of the trusted root certificates /// An `Err` is returned when the verification fails. /// - /// **Note well:** certificates issued by Fulciuo are, by design, valid only + /// **Note well:** certificates issued by Fulcio are, by design, valid only /// for a really limited amount of time. /// Because of that the validity checks performed by this method are more /// relaxed. The validity checks are done inside of /// [`crate::crypto::verify_validity`] and [`crate::crypto::verify_expiration`]. - pub(crate) fn verify_pem_cert(&self, cert_pem: &[u8]) -> Result<()> { - let cert_pem_str = std::str::from_utf8(cert_pem).map_err(|_| { - SigstoreError::UnexpectedError("Cannot convert cert back to string".to_string()) - })?; - let cert = picky::x509::Cert::from_pem_str(cert_pem_str)?; - self.verify(&cert) + pub(crate) fn verify_pem_cert( + &self, + cert_pem: &[u8], + verification_time: Option, + ) -> Result<()> { + let cert_pem = pem::parse(cert_pem)?; + if cert_pem.tag() != "CERTIFICATE" { + return Err(SigstoreError::CertificatePoolError( + "PEM file is not a certificate", + )); + } + + self.verify_der_cert(cert_pem.contents(), verification_time) } /// Ensures the given certificate has been issued by one of the trusted root certificates /// An `Err` is returned when the verification fails. /// - /// **Note well:** certificates issued by Fulciuo are, by design, valid only + /// **Note well:** certificates issued by Fulcio are, by design, valid only /// for a really limited amount of time. /// Because of that the validity checks performed by this method are more /// relaxed. The validity checks are done inside of /// [`crate::crypto::verify_validity`] and [`crate::crypto::verify_expiration`]. - pub(crate) fn verify_der_cert(&self, bytes: &[u8]) -> Result<()> { - let cert = picky::x509::Cert::from_der(bytes)?; - self.verify(&cert) + pub(crate) fn verify_der_cert( + &self, + der: &[u8], + verification_time: Option, + ) -> Result<()> { + self.verify_cert_with_time(der, verification_time.unwrap_or(UnixTime::now())) } - fn verify(&self, cert: &picky::x509::Cert) -> Result<()> { - let verified = self - .create_chains_for_all_certificates() - .iter() - .any(|chain| { - cert.verifier() - .chain(chain.iter().copied()) - .exact_date(&cert.valid_not_before()) - .verify() - .is_ok() - }); + /// TODO(tnytown): nudge webpki into behaving as the cosign code expects + pub(crate) fn verify_cert_with_time( + &self, + cert: &[u8], + verification_time: UnixTime, + ) -> Result<()> { + let der = CertificateDer::from(cert); + let cert = EndEntityCert::try_from(&der)?; - if verified { - Ok(()) - } else { - Err(SigstoreError::CertificateValidityError( - "Not issued by a trusted root".to_string(), - )) - } - } + // TODO(tnytown): Determine which of these algs are used in the Sigstore ecosystem. + let signing_algs = webpki::ALL_VERIFICATION_ALGS; + + let eku_code_signing = ID_KP_CODE_SIGNING.as_bytes(); - fn create_chains_for_all_certificates(&self) -> Vec> { - let mut chains: Vec> = vec![]; - self.trusted_roots.iter().for_each(|trusted_root| { - chains.push([trusted_root].to_vec()); - }); - self.intermediates.iter().for_each(|intermediate| { - for root in self.trusted_roots.iter() { - if root.is_parent_of(intermediate).is_ok() { - chains.push([intermediate, root].to_vec()); - } - } - }); + cert.verify_for_usage( + signing_algs, + &self.trusted_roots, + self.intermediates.as_slice(), + verification_time, + KeyUsage::required(eku_code_signing), + None, + None, + )?; - chains + Ok(()) } } diff --git a/src/errors.rs b/src/errors.rs index f513f17543..430066cc65 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -61,9 +61,6 @@ pub enum SigstoreError { #[error(transparent)] FromPEMError(#[from] pem::PemError), - #[error(transparent)] - CertError(#[from] picky::x509::certificate::CertError), - #[error(transparent)] Base64DecodeError(#[from] base64::DecodeError), @@ -104,7 +101,7 @@ pub enum SigstoreError { CertificateWithIncompleteSubjectAlternativeName, #[error("Certificate pool error: {0}")] - CertificatePoolError(String), + CertificatePoolError(&'static str), #[error("Cannot fetch manifest of {image}: {error}")] RegistryFetchManifestError { image: String, error: String }, @@ -146,6 +143,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), @@ -200,6 +200,9 @@ pub enum SigstoreError { #[error(transparent)] Utf8Error(#[from] std::str::Utf8Error), + #[error(transparent)] + WebPKIError(#[from] webpki::Error), + #[error("Failed to parse the key: {0}")] KeyParseError(String), diff --git a/src/lib.rs b/src/lib.rs index a6eb641818..f439bfc663 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,15 +82,23 @@ //! //! // Provide both rekor and fulcio data -> this enables keyless verification //! // Read rekor's key from the location generated by `cosign initialize` -//! let rekor_pub_key = fs::read_to_string("~/.sigstore/root/targets/rekor.pub") +//! let rekor_pub_key = fs::read("~/.sigstore/root/targets/rekor.pub") //! .expect("Cannot read rekor public key"); //! // Read fulcio's certificate from the location generated by `cosign initialize` -//! let fulcio_cert = fs::read_to_string("~/.sigstore/root/targets/fulcio.crt.pem") +//! let fulcio_cert_data = fs::read("~/.sigstore/root/targets/fulcio.crt.pem") //! .expect("Cannot read fulcio certificate"); +//! let fulcio_cert = sigstore::registry::Certificate { +//! encoding: sigstore::registry::CertificateEncoding::Pem, +//! data: fulcio_cert_data +//! }; +//! +//! let mut repo = sigstore::tuf::FakeRepository::default(); +//! repo.fulcio_certs.get_or_insert(Vec::new()).push(fulcio_cert.try_into().unwrap()); +//! repo.rekor_key = Some(rekor_pub_key); //! //! let mut client = sigstore::cosign::ClientBuilder::default() -//! .with_rekor_pub_key(&rekor_pub_key) -//! .with_fulcio_cert(fulcio_cert.as_bytes()) +//! .with_trust_repository(&repo) +//! .expect("Cannot construct cosign client from given materials") //! .build() //! .expect("Unexpected failure while building Client"); //! diff --git a/src/registry/config.rs b/src/registry/config.rs index bb6be2c04e..c8cdfa24ab 100644 --- a/src/registry/config.rs +++ b/src/registry/config.rs @@ -15,10 +15,13 @@ //! Set of structs and enums used to define how to interact with OCI registries +use rustls_pki_types::CertificateDer; use serde::Serialize; use std::cmp::Ordering; use std::convert::From; +use crate::errors; + /// A method for authenticating to a registry #[derive(Serialize, Debug)] pub enum Auth { @@ -123,6 +126,21 @@ impl From<&Certificate> for oci_distribution::client::Certificate { } } +impl<'a> TryFrom for CertificateDer<'a> { + type Error = errors::SigstoreError; + fn try_from(value: Certificate) -> errors::Result> { + #[inline] + fn to_der(pem: &[u8]) -> errors::Result> { + Ok(pem::parse(pem)?.into_contents()) + } + + match &value.encoding { + CertificateEncoding::Der => Ok(CertificateDer::from(value.data)), + CertificateEncoding::Pem => Ok(CertificateDer::from(to_der(&value.data)?)), + } + } +} + /// A client configuration #[derive(Debug, Clone)] pub struct ClientConfig { diff --git a/src/tuf/constants.rs b/src/tuf/constants.rs index 99231be2da..325989706c 100644 --- a/src/tuf/constants.rs +++ b/src/tuf/constants.rs @@ -13,175 +13,13 @@ // 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"); diff --git a/src/tuf/mod.rs b/src/tuf/mod.rs index 982fa99c14..a0f7a84c80 100644 --- a/src/tuf/mod.rs +++ b/src/tuf/mod.rs @@ -23,142 +23,315 @@ //! //! # Example //! -//! The `SigstoreRepository` instance can be created via the [`SigstoreRepository::fetch`] +//! The `SigstoreRepository` instance can be created via the [`SigstoreRepository::prefetch`] //! method. //! //! ```rust,no_run //! use sigstore::tuf::SigstoreRepository; -//! use sigstore::cosign; -//! -//! let repo = SigstoreRepository::fetch(None) -//! .expect("Error while building SigstoreRepository"); -//! let client = cosign::ClientBuilder::default() -//! .with_rekor_pub_key(repo.rekor_pub_key()) -//! .with_fulcio_certs(repo.fulcio_certs()) -//! .build() -//! .expect("Error while building cosign client"); +//! let repo = SigstoreRepository::new(None).unwrap().prefetch().unwrap(); //! ``` -//! -//! The `SigstoreRepository::fetch` method can attempt to leverage local copies -//! of the Rekor and Fulcio files. Please refer to the -//! [method docs](SigstoreRepository::fetch) for more details. -//! -//! **Warning:** the `SigstoreRepository::fetch` method currently needs -//! 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; + +use rustls_pki_types::CertificateDer; +use sha2::{Digest, Sha256}; +use tough::TargetName; -mod repository_helper; -use repository_helper::RepositoryHelper; +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)?; + let target_base = url::Url::parse(constants::SIGSTORE_TARGET_BASE)?; + + let repository = + tough::RepositoryLoader::new(constants::SIGSTORE_ROOT, metadata_base, target_base) + .expiration_enforcement(tough::ExpirationEnforcement::Safe) + .load() + .map_err(Box::new)?; + + 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[..])?) + } + + 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(tlogs: &[TransparencyLogInstance]) -> impl Iterator { + tlogs + .iter() + .filter(|key| is_timerange_valid(key.public_key.valid_for.as_ref(), false)) + .filter_map(|key| key.public_key.raw_bytes.as_ref()) + .map(|key_bytes| key_bytes.as_slice()) + } + + #[inline] + fn ca_keys( + cas: &[CertificateAuthority], + 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(CertificateDer::from).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(); - /// Rekor public key - pub fn rekor_pub_key(&self) -> &str { - &self.rekor_pub_key + 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)) + }?; - /// Fulcio certificate - pub fn fulcio_certs(&self) -> &[crate::registry::Certificate] { - &self.fulcio_certs + let data = if local_file_outdated { + let data = fetch_target(repository, target_name)?; + if let Some(path) = local_file { + // update the local file to have latest data from the TUF repo + fs::write(path, data.clone())?; + } + data + } else { + local_file_contents + .expect("local file contents to not be 'None'") + .as_bytes() + .to_owned() + }; + + Ok(data) +} + +/// Download a file from a TUF repository +fn fetch_target(repository: &tough::Repository, target_name: &TargetName) -> Result> { + let data: Vec; + match repository.read_target(target_name).map_err(Box::new)? { + None => Err(SigstoreError::TufTargetNotFoundError( + target_name.raw().to_string(), + )), + Some(reader) => { + data = read_to_end(reader)?; + Ok(data) + } + } +} + +/// Compares the checksum of a local file, with the digest reported inside of +/// TUF repository metadata +fn is_local_file_outdated( + repository: &tough::Repository, + target_name: &TargetName, + local_file: &Path, +) -> Result<(bool, Option)> { + let target = repository + .targets() + .signed + .targets + .get(target_name) + .ok_or_else(|| SigstoreError::TufTargetNotFoundError(target_name.raw().to_string()))?; + + if local_file.exists() { + let data = fs::read_to_string(local_file)?; + let local_checksum = Sha256::digest(data.clone()); + let expected_digest: Vec = target.hashes.sha256.to_vec(); + + if local_checksum.as_slice() == expected_digest.as_slice() { + // local data is not outdated + Ok((false, Some(data))) + } else { + Ok((true, None)) + } + } else { + Ok((true, None)) } } + +/// Gets the goods from a read and makes a Vec +fn read_to_end(mut reader: R) -> Result> { + let mut v = Vec::new(); + reader.read_to_end(&mut v)?; + Ok(v) +} diff --git a/src/tuf/repository_helper.rs b/src/tuf/repository_helper.rs index a581619638..74747d363e 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,162 +48,110 @@ impl RepositoryHelper { Ok(Self { repository, checkout_dir: checkout_dir.map(|s| s.to_owned()), + trusted_root: None, }) } - /// 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, - }); + 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, } - Ok(certs) } - 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() - } - - /// Fetch Rekor public key 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)?; + fn trusted_root(&self) -> Result<&TrustedRoot> { + if let Some(result) = self.trusted_root { + return Ok(&result); + } - let local_rekor_path = self + let trusted_root_target = TargetName::new("trusted_root.json").map_err(Box::new)?; + let local_path = self .checkout_dir .as_ref() - .map(|d| Path::new(d).join(SIGSTORE_REKOR_PUB_KEY_TARGET)); + .map(|d| d.join(trusted_root_target.raw())); - fetch_target_or_reuse_local_cache( + let data = fetch_target_or_reuse_local_cache( &self.repository, - &rekor_target_name, - local_rekor_path.as_ref(), - ) + &trusted_root_target, + local_path.as_ref(), + )?; + + let result = serde_json::from_slice(&data[..])?; + Ok(self.trusted_root.insert(result)) } -} -/// 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)) - }?; - - 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())?; + #[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[..]); + } } - 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) + 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; } -} -/// 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))) + /// 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 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(); + + if certs.is_empty() { + Err(SigstoreError::TufMetadataError( + "Fulcio certificates not found", + )) } else { - Ok((true, None)) + Ok(certs) } - } 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) + /// 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_keys(&self) -> Result> { + let root = self.trusted_root()?; + let keys = self.tlog_keys(&root.tlogs); + + if keys.len() != 1 { + Err(SigstoreError::TufMetadataError( + "Did not find exactly 1 active Rekor key", + )) + } else { + Ok(keys) + } + } } #[cfg(test)] @@ -252,63 +200,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 +296,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 +312,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 +321,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..61586a8ffa --- /dev/null +++ b/src/tuf/trustroot.rs @@ -0,0 +1,148 @@ +#![allow(dead_code)] + +// HACK(jl): protobuf-specs schemas are currently compiled for direct dependencies of the Bundle schema. +// See note https://github.com/sigstore/protobuf-specs/blob/main/gen/pb-rust/src/lib.rs#L1-L23 +// HACK(ap): We should probably use definitions from sigstore-protobuf-specs, but +// the autogenerated definitions are unergonomic. Declare it locally here. + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::base64::Base64; + +use serde_with::serde_as; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[allow(non_camel_case_types)] +pub(crate) enum HashAlgorithm { + HASH_ALGORITHM_UNSPECIFIED = 0, + SHA2_256 = 1, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[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 +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct LogId { + #[serde_as(as = "Base64")] + pub key_id: Vec, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct TimeRange { + pub start: DateTime, + pub end: Option>, +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PublicKey { + #[serde_as(as = "Option")] + pub raw_bytes: Option>, + pub key_details: PublicKeyDetails, + pub valid_for: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct DistinguishedName { + pub organization: String, + pub common_name: String, +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct X509Certificate { + #[serde_as(as = "Base64")] + pub raw_bytes: Vec, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct X509CertificateChain { + pub certificates: Vec, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[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(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct CertificateAuthority { + pub subject: DistinguishedName, + pub uri: Option, + pub cert_chain: X509CertificateChain, + pub valid_for: TimeRange, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct TrustedRoot { + pub media_type: String, + pub tlogs: Vec, + pub certificate_authorities: Vec, + pub ctlogs: Vec, + pub timestamp_authorities: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tuf_serde_as_base64() { + let data = X509Certificate { + raw_bytes: b"Hello World".to_vec(), // NOTE(jl): value not representative + }; + let json = serde_json::json!({"rawBytes": "SGVsbG8gV29ybGQ=",}); + + assert_eq!(json, serde_json::to_value(&data).unwrap()); + assert_eq!(data, serde_json::from_value(json).unwrap()); + } + + #[test] + fn tuf_serde_as_nested_structure_base64() { + let data = PublicKey { + raw_bytes: Some(b"Hello World".to_vec()), + key_details: PublicKeyDetails::PKIX_ED25519, + valid_for: Some(TimeRange { + start: DateTime::from_timestamp(1_500_000_000, 0).unwrap(), + end: None, + }), + }; + let json = serde_json::json!({ + "rawBytes": "SGVsbG8gV29ybGQ=", + "keyDetails": "PKIX_ED25519", + "validFor": { + "start": "2017-07-14T02:40:00Z", + "end": None::> + } + }); + + assert_eq!(json, serde_json::to_value(&data).unwrap()); + assert_eq!(data, serde_json::from_value(json).unwrap()); + } +} diff --git a/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": [] +} From a4467ee84fb64d279b4eb9ec99f5173eba2c05ad Mon Sep 17 00:00:00 2001 From: Jack Leightcap Date: Thu, 9 Nov 2023 14:32:57 -0500 Subject: [PATCH 2/3] cosign/tuf: docs Signed-off-by: Jack Leightcap --- src/cosign/client_builder.rs | 4 +-- src/cosign/mod.rs | 13 +++++----- src/crypto/verification_key.rs | 2 +- src/lib.rs | 12 +++++---- src/tuf/trustroot.rs | 46 ++++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/cosign/client_builder.rs b/src/cosign/client_builder.rs index f076e83528..4bfae89e2d 100644 --- a/src/cosign/client_builder.rs +++ b/src/cosign/client_builder.rs @@ -28,7 +28,7 @@ use crate::tuf::Repository; /// ## Rekor integration /// /// Rekor integration can be enabled by specifying Rekor's public key. -/// This can be provided via the [`ClientBuilder::with_rekor_pub_key`] method. +/// This can be provided via a [`crate::tuf::FakeRepository`]. /// /// > Note well: the [`tuf`](crate::tuf) module provides helper structs and methods /// > to obtain this data from the official TUF repository of the Sigstore project. @@ -36,7 +36,7 @@ use crate::tuf::Repository; /// ## Fulcio integration /// /// Fulcio integration can be enabled by specifying Fulcio's certificate. -/// This can be provided via the [`ClientBuilder::with_fulcio_certs`] method. +/// This can be provided via a [`crate::tuf::FakeRepository`]. /// /// > Note well: the [`tuf`](crate::tuf) module provides helper structs and methods /// > to obtain this data from the official TUF repository of the Sigstore project. diff --git a/src/cosign/mod.rs b/src/cosign/mod.rs index 4e6be029f2..9ddcdcc813 100644 --- a/src/cosign/mod.rs +++ b/src/cosign/mod.rs @@ -102,13 +102,14 @@ pub trait CosignCapabilities { /// must be satisfied: /// /// * The [`sigstore::cosign::Client`](crate::cosign::client::Client) must - /// have been created with Rekor integration enabled (see - /// [`sigstore::cosign::ClientBuilder::with_rekor_pub_key`](crate::cosign::ClientBuilder::with_rekor_pub_key)) + /// have been created with Rekor integration enabled (see [`crate::tuf::FakeRepository`]) /// * The [`sigstore::cosign::Client`](crate::cosign::client::Client) must - /// have been created with Fulcio integration enabled (see - /// [`sigstore::cosign::ClientBuilder::with_fulcio_certs`](crate::cosign::ClientBuilder::with_fulcio_certs)) + /// have been created with Fulcio integration enabled (see [`crate::tuf::FakeRepository]) /// * The layer must include a bundle produced by Rekor /// + /// > Note well: the [`tuf`](crate::tuf) module provides helper structs and methods + /// > to obtain this data from the official TUF repository of the Sigstore project. + /// /// When the embedded certificate cannot be verified, [`SignatureLayer::certificate_signature`] /// is going to be `None`. /// @@ -199,7 +200,7 @@ pub trait CosignCapabilities { /// verification. /// /// Returns a `Result` with either `Ok()` for passed verification or -/// [`SigstoreVerifyConstraintsError`](crate::errors::SigstoreVerifyConstraintsError), +/// [`SigstoreVerifyConstraintsError`] /// which contains a vector of references to unsatisfied constraints. /// /// See the documentation of the [`cosign::verification_constraint`](crate::cosign::verification_constraint) module for more @@ -249,7 +250,7 @@ where /// passes applying constraints process. /// /// Returns a `Result` with either `Ok()` for success or -/// [`SigstoreApplicationConstraintsError`](crate::errors::SigstoreApplicationConstraintsError), +/// [`SigstoreApplicationConstraintsError`] /// which contains a vector of references to unapplied constraints. /// /// See the documentation of the [`cosign::sign_constraint`](crate::cosign::sign_constraint) module for more diff --git a/src/crypto/verification_key.rs b/src/crypto/verification_key.rs index 9ba0feaaab..1cf40d7eed 100644 --- a/src/crypto/verification_key.rs +++ b/src/crypto/verification_key.rs @@ -59,7 +59,7 @@ pub enum CosignVerificationKey { ED25519(ed25519_dalek::VerifyingKey), } -/// Attempts to convert a [x509 Subject Public Key Info](SubjectPublicKeyInfo) object into +/// Attempts to convert a [x509 Subject Public Key Info](x509_cert::spki::SubjectPublicKeyInfo) object into /// a `CosignVerificationKey` one. /// /// Currently can convert only the following types of keys: diff --git a/src/lib.rs b/src/lib.rs index f439bfc663..30475140a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ //! //! # Verification //! -//! Sigstore verification is done using the [`cosign::Client`](crate::cosign::Client) +//! Sigstore verification is done using the [`cosign::Client`] //! struct. //! //! ## Triangulation of Sigstore signature @@ -92,9 +92,11 @@ //! data: fulcio_cert_data //! }; //! -//! let mut repo = sigstore::tuf::FakeRepository::default(); -//! repo.fulcio_certs.get_or_insert(Vec::new()).push(fulcio_cert.try_into().unwrap()); -//! repo.rekor_key = Some(rekor_pub_key); +//! let mut repo = sigstore::tuf::FakeRepository { +//! fulcio_certs: Some(vec![fulcio_cert.try_into().unwrap()]), +//! rekor_key: Some(rekor_pub_key), +//! ..Default::default() +//! }; //! //! let mut client = sigstore::cosign::ClientBuilder::default() //! .with_trust_repository(&repo) @@ -222,7 +224,7 @@ //! //! ## Fulcio and Rekor integration //! -//! [`cosign::Client`](crate::cosign::Client) integration with Fulcio and Rekor +//! [`cosign::Client`] integration with Fulcio and Rekor //! requires the following data to work: Fulcio's certificate and Rekor's public key. //! //! These files are safely distributed by the Sigstore project via a TUF repository. diff --git a/src/tuf/trustroot.rs b/src/tuf/trustroot.rs index 61586a8ffa..aeb321fd92 100644 --- a/src/tuf/trustroot.rs +++ b/src/tuf/trustroot.rs @@ -13,6 +13,13 @@ use serde_with::serde_as; #[derive(Serialize, Deserialize, Debug, PartialEq)] #[allow(non_camel_case_types)] +/// Only a subset of the secure hash standard algorithms are supported. +/// See for more +/// details. +/// UNSPECIFIED SHOULD not be used, primary reason for inclusion is to force +/// any proto JSON serialization to emit the used hash algorithm, as default +/// option is to *omit* the default value of an enum (which is the first +/// value, represented by '0'. pub(crate) enum HashAlgorithm { HASH_ALGORITHM_UNSPECIFIED = 0, SHA2_256 = 1, @@ -20,6 +27,12 @@ pub(crate) enum HashAlgorithm { #[derive(Serialize, Deserialize, Debug, PartialEq)] #[allow(non_camel_case_types)] +/// Details of a specific public key, capturing the the key encoding method, +/// and signature algorithm. +/// To avoid the possibility of contradicting formats such as PKCS1 with +/// ED25519 the valid permutations are listed as a linear set instead of a +/// cartesian set (i.e one combined variable instead of two, one for encoding +/// and one for the signature algorithm). pub(crate) enum PublicKeyDetails { PUBLIC_KEY_DETAILS_UNSPECIFIED = 0, // RSA @@ -37,6 +50,7 @@ pub(crate) enum PublicKeyDetails { #[serde_as] #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "camelCase")] +/// LogId captures the identity of a transparency log. pub(crate) struct LogId { #[serde_as(as = "Base64")] pub key_id: Vec, @@ -44,6 +58,10 @@ pub(crate) struct LogId { #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "camelCase")] +/// The time range is closed and includes both the start and end times, +/// (i.e., [start, end]). +/// End is optional to be able to capture a period that has started but +/// has no known end. pub(crate) struct TimeRange { pub start: DateTime, pub end: Option>, @@ -76,12 +94,19 @@ pub(crate) struct X509Certificate { #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "camelCase")] +/// A chain of X.509 certificates. pub(crate) struct X509CertificateChain { pub certificates: Vec, } #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "camelCase")] +/// TransparencyLogInstance describes the immutable parameters from a +/// transparency log. +/// See https://www.rfc-editor.org/rfc/rfc9162.html#name-log-parameters +/// for more details. +/// The included parameters are the minimal set required to identify a log, +/// and verify an inclusion proof/promise. pub(crate) struct TransparencyLogInstance { pub base_url: String, pub hash_algorithm: HashAlgorithm, @@ -91,6 +116,8 @@ pub(crate) struct TransparencyLogInstance { #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "camelCase")] +/// CertificateAuthority enlists the information required to identify which +/// CA to use and perform signature verification. pub(crate) struct CertificateAuthority { pub subject: DistinguishedName, pub uri: Option, @@ -100,6 +127,25 @@ pub(crate) struct CertificateAuthority { #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "camelCase")] +/// TrustedRoot describes the client's complete set of trusted entities. +/// How the TrustedRoot is populated is not specified, but can be a +/// combination of many sources such as TUF repositories, files on disk etc. +/// +/// The TrustedRoot is not meant to be used for any artifact verification, only +/// to capture the complete/global set of trusted verification materials. +/// When verifying an artifact, based on the artifact and policies, a selection +/// of keys/authorities are expected to be extracted and provided to the +/// verification function. This way the set of keys/authorities can be kept to +/// a minimal set by the policy to gain better control over what signatures +/// that are allowed. +/// +/// The embedded transparency logs, CT logs, CAs and TSAs MUST include any +/// previously used instance -- otherwise signatures made in the past cannot +/// be verified. +/// The currently used instances MUST NOT have their 'end' timestamp set in +/// their 'valid_for' attribute for easy identification. +/// All the listed instances SHOULD be sorted by the 'valid_for' in ascending +/// order, that is, the oldest instance first and the current instance last. pub(crate) struct TrustedRoot { pub media_type: String, pub tlogs: Vec, From 2f8150d0f7a1c308564ad28d38e0891200f49412 Mon Sep 17 00:00:00 2001 From: Jack Leightcap Date: Mon, 13 Nov 2023 12:34:29 -0500 Subject: [PATCH 3/3] cosign/tuf: PR feedback Signed-off-by: Jack Leightcap --- Cargo.toml | 2 +- examples/cosign/verify/main.rs | 2 +- src/cosign/client_builder.rs | 5 ++--- src/cosign/mod.rs | 4 ++-- src/lib.rs | 2 +- src/tuf/mod.rs | 9 +++++---- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b91f598203..704975e0b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,7 +118,7 @@ tokio = { version = "1.17.0", features = ["rt"] } tough = { version = "0.14", features = ["http"], optional = true } tracing = "0.1.31" url = "2.2.2" -x509-cert = { version = "0.2.2", features = ["builder", "pem", "std"] } +x509-cert = { version = "0.2.2", features = ["pem", "std"] } crypto_secretbox = "0.1.1" zeroize = "1.5.7" rustls-webpki = { version = "0.102.0-alpha.4", features = ["alloc"] } diff --git a/examples/cosign/verify/main.rs b/examples/cosign/verify/main.rs index 60369cf094..fc687ba627 100644 --- a/examples/cosign/verify/main.rs +++ b/examples/cosign/verify/main.rs @@ -240,7 +240,7 @@ async fn fulcio_and_rekor_data(cli: &Cli) -> anyhow::Result Note well: the [`tuf`](crate::tuf) module provides helper structs and methods /// > to obtain this data from the official TUF repository of the Sigstore project. @@ -36,7 +36,7 @@ use crate::tuf::Repository; /// ## Fulcio integration /// /// Fulcio integration can be enabled by specifying Fulcio's certificate. -/// This can be provided via a [`crate::tuf::FakeRepository`]. +/// This can be provided via a [`crate::tuf::ManualRepository`]. /// /// > Note well: the [`tuf`](crate::tuf) module provides helper structs and methods /// > to obtain this data from the official TUF repository of the Sigstore project. @@ -56,7 +56,6 @@ pub struct ClientBuilder<'a> { oci_client_config: ClientConfig, rekor_pub_key: Option<&'a [u8]>, fulcio_certs: Vec>, - // repo: Repository #[cfg(feature = "cached-client")] enable_registry_caching: bool, } diff --git a/src/cosign/mod.rs b/src/cosign/mod.rs index 9ddcdcc813..50c9ce8c29 100644 --- a/src/cosign/mod.rs +++ b/src/cosign/mod.rs @@ -102,9 +102,9 @@ pub trait CosignCapabilities { /// must be satisfied: /// /// * The [`sigstore::cosign::Client`](crate::cosign::client::Client) must - /// have been created with Rekor integration enabled (see [`crate::tuf::FakeRepository`]) + /// have been created with Rekor integration enabled (see [`crate::tuf::ManualRepository`]) /// * The [`sigstore::cosign::Client`](crate::cosign::client::Client) must - /// have been created with Fulcio integration enabled (see [`crate::tuf::FakeRepository]) + /// have been created with Fulcio integration enabled (see [`crate::tuf::ManualRepository]) /// * The layer must include a bundle produced by Rekor /// /// > Note well: the [`tuf`](crate::tuf) module provides helper structs and methods diff --git a/src/lib.rs b/src/lib.rs index 30475140a1..afea5441a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,7 +92,7 @@ //! data: fulcio_cert_data //! }; //! -//! let mut repo = sigstore::tuf::FakeRepository { +//! let mut repo = sigstore::tuf::ManualRepository { //! fulcio_certs: Some(vec![fulcio_cert.try_into().unwrap()]), //! rekor_key: Some(rekor_pub_key), //! ..Default::default() diff --git a/src/tuf/mod.rs b/src/tuf/mod.rs index a0f7a84c80..4df3c757c2 100644 --- a/src/tuf/mod.rs +++ b/src/tuf/mod.rs @@ -43,6 +43,7 @@ mod trustroot; use rustls_pki_types::CertificateDer; use sha2::{Digest, Sha256}; use tough::TargetName; +use tracing::debug; use self::trustroot::{CertificateAuthority, TimeRange, TransparencyLogInstance, TrustedRoot}; @@ -54,15 +55,15 @@ pub trait Repository { fn rekor_keys(&self) -> Result>; } -/// A `FakeRepository` is a [Repository] with out-of-band trust materials. +/// A `ManualRepository` is a [Repository] with out-of-band trust materials. /// As it does not establish a trust root with TUF, users must initialize its materials themselves. #[derive(Debug, Default)] -pub struct FakeRepository<'a> { +pub struct ManualRepository<'a> { pub fulcio_certs: Option>>, pub rekor_key: Option>, } -impl Repository for FakeRepository<'_> { +impl Repository for ManualRepository<'_> { fn fulcio_certs(&self) -> Result> { Ok(match &self.fulcio_certs { Some(certs) => certs.clone(), @@ -120,7 +121,7 @@ impl SigstoreRepository { local_path.as_ref(), )?; - println!("data:\n{}", String::from_utf8_lossy(&data)); + debug!("data:\n{}", String::from_utf8_lossy(&data)); Ok(serde_json::from_slice(&data[..])?) }