Skip to content

Commit

Permalink
Introduce CertificatePool
Browse files Browse the repository at this point in the history
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: sigstore#32 (comment)

Signed-off-by: Flavio Castelli <fcastelli@suse.com>
  • Loading branch information
flavio committed Mar 7, 2022
1 parent 9b561af commit 3d12f0b
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 1 deletion.
5 changes: 5 additions & 0 deletions Cargo.toml
Expand Up @@ -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"] }
Expand All @@ -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"] }
86 changes: 86 additions & 0 deletions 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<picky::x509::Cert>,
}

impl CertificatePool {
/// Build a `CertificatePool` instance using the provided list of [`Certificate`]
pub(crate) fn from_certificates(certs: &[Certificate]) -> Result<Self> {
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(),
))
}
}
}
1 change: 1 addition & 0 deletions src/crypto/mod.rs
Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion src/errors.rs
Expand Up @@ -32,10 +32,12 @@ pub enum SigstoreError {

#[error(transparent)]
X509ParseError(#[from] x509_parser::nom::Err<x509_parser::error::X509Error>),

#[error(transparent)]
X509Error(#[from] x509_parser::error::X509Error),

#[error(transparent)]
CertError(#[from] picky::x509::certificate::CertError),

#[error(transparent)]
Base64DecodeError(#[from] base64::DecodeError),

Expand Down Expand Up @@ -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 },

Expand Down

0 comments on commit 3d12f0b

Please sign in to comment.