From 3d12f0b3cc1a560d431de4d1722b3a6f6a960ad8 Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Mon, 7 Mar 2022 09:29:50 +0100 Subject: [PATCH] Introduce `CertificatePool` Create a struct that helps manage a pool of trusted root certificates. The certificate verification is then done using the `picky` crate. For more information about why the `picky` crate has been chosen, please refer to this conversation: https://github.com/sigstore/sigstore-rs/issues/32#issuecomment-1047613720 Signed-off-by: Flavio Castelli --- Cargo.toml | 5 ++ src/crypto/certificate_pool.rs | 86 ++++++++++++++++++++++++++++++++++ src/crypto/mod.rs | 1 + src/errors.rs | 7 ++- 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/crypto/certificate_pool.rs diff --git a/Cargo.toml b/Cargo.toml index 9b04140793..bfc90f5365 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,15 @@ rustls-tls = ["oci-distribution/rustls-tls"] [dependencies] async-trait = "0.1.52" base64 = "0.13.0" +lazy_static = "1.4.0" # TODO: go back to the officially release oci-distribution once these patches are released oci-distribution = { git = "https://github.com/krustlet/oci-distribution", rev = "0f717968093a5415f428503d741dedf24ea97948", default-features = false } #oci-distribution = { version = "0.8.1", default-features = false } olpc-cjson = "0.1.1" pem = "1.0.2" +# TODO: get rid of the git reference on the crate is published on crates.io +picky = { git = "https://github.com/Devolutions/picky-rs.git", tag = "picky-7.0.0-rc.1", default-features = false, features = [ "x509" ] } +regex = "1.5.4" ring = "0.16.20" serde_json = "1.0.79" serde = { version = "1.0.136", features = ["derive"] } @@ -37,5 +41,6 @@ anyhow = "1.0.54" chrono = "0.4.19" clap = { version = "3.1.0", features = ["derive"] } openssl = "0.10.38" +rstest = "0.12.0" tempfile = "3.3.0" tracing-subscriber = { version = "0.3.9", features = ["env-filter"] } diff --git a/src/crypto/certificate_pool.rs b/src/crypto/certificate_pool.rs new file mode 100644 index 0000000000..a21724edcd --- /dev/null +++ b/src/crypto/certificate_pool.rs @@ -0,0 +1,86 @@ +// +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + errors::{Result, SigstoreError}, + registry::Certificate, +}; + +/// A collection of trusted root certificates +#[derive(Default, Debug)] +pub(crate) struct CertificatePool { + trusted_roots: 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![]; + + 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::UnexpectedError("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), + }?; + + if !matches!(pc.ty(), picky::x509::certificate::CertType::Root) { + return Err(SigstoreError::CertificatePoolError( + "Cannot add non-root certificate".to_string(), + )); + } + + trusted_roots.push(pc); + } + + Ok(CertificatePool { trusted_roots }) + } + + /// 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 + /// 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(&self, cert_pem: &[u8]) -> Result<()> { + let cert_pem_str = String::from_utf8(cert_pem.to_vec()).map_err(|_| { + SigstoreError::UnexpectedError("Cannot convert cert back to string".to_string()) + })?; + let cert = picky::x509::Cert::from_pem_str(&cert_pem_str)?; + + let verified = self.trusted_roots.iter().any(|trusted_root| { + let chain = [trusted_root.clone()]; + cert.verifier() + .chain(chain.iter()) + .exact_date(&cert.valid_not_before()) + .verify() + .is_ok() + }); + + if verified { + Ok(()) + } else { + Err(SigstoreError::CertificateValidityError( + "Not issued by a trusted root".to_string(), + )) + } + } +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 78eb4856ad..6374b12f13 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -64,6 +64,7 @@ pub enum Signature<'a> { } pub(crate) mod certificate; +pub(crate) mod certificate_pool; pub mod verification_key; pub use verification_key::CosignVerificationKey; diff --git a/src/errors.rs b/src/errors.rs index 1132863350..188198281d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -32,10 +32,12 @@ pub enum SigstoreError { #[error(transparent)] X509ParseError(#[from] x509_parser::nom::Err), - #[error(transparent)] X509Error(#[from] x509_parser::error::X509Error), + #[error(transparent)] + CertError(#[from] picky::x509::certificate::CertError), + #[error(transparent)] Base64DecodeError(#[from] base64::DecodeError), @@ -75,6 +77,9 @@ pub enum SigstoreError { #[error("Certificate with incomplete Subject Alternative Name")] CertificateWithIncompleteSubjectAlternativeName, + #[error("Certificate pool error: {0}")] + CertificatePoolError(String), + #[error("Cannot fetch manifest of {image}: {error}")] RegistryFetchManifestError { image: String, error: String },