From 63590e1e913c13a9e71c1747ea9f7995aeea3d3f Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Thu, 10 Nov 2022 17:20:14 +0100 Subject: [PATCH 1/7] refactor: make some functions pub(crate) Make some certificate related functions public inside of the crate. This allows their reuse. Signed-off-by: Flavio Castelli --- src/crypto/certificate.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/crypto/certificate.rs b/src/crypto/certificate.rs index aaff8f53ea..a9a9728630 100644 --- a/src/crypto/certificate.rs +++ b/src/crypto/certificate.rs @@ -32,7 +32,7 @@ pub(crate) fn is_trusted(certificate: &X509Certificate, integrated_time: i64) -> Ok(()) } -fn verify_key_usages(certificate: &X509Certificate) -> Result<()> { +pub(crate) fn verify_key_usages(certificate: &X509Certificate) -> Result<()> { let key_usage = certificate .tbs_certificate .key_usage()? @@ -52,7 +52,7 @@ fn verify_key_usages(certificate: &X509Certificate) -> Result<()> { Ok(()) } -fn verify_has_san(certificate: &X509Certificate) -> Result<()> { +pub(crate) fn verify_has_san(certificate: &X509Certificate) -> Result<()> { let _subject_alternative_name = certificate .tbs_certificate .subject_alternative_name()? @@ -60,7 +60,7 @@ fn verify_has_san(certificate: &X509Certificate) -> Result<()> { Ok(()) } -fn verify_validity(certificate: &X509Certificate) -> Result<()> { +pub(crate) fn verify_validity(certificate: &X509Certificate) -> Result<()> { // Comment taken from cosign verification code: // THIS IS IMPORTANT: WE DO NOT CHECK TIMES HERE // THE CERTIFICATE IS TREATED AS TRUSTED FOREVER From e2a4959e03479fdc0717390051de99be31cb3dc8 Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Thu, 10 Nov 2022 17:23:37 +0100 Subject: [PATCH 2/7] refactor: rename some internal pub(crate) functions CertificatePool: define two different verification functions, depending on how the certificate to be verified is encoded (DER/PEM). Signed-off-by: Flavio Castelli --- src/cosign/signature_layers.rs | 2 +- src/crypto/certificate_pool.rs | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/cosign/signature_layers.rs b/src/cosign/signature_layers.rs index 27ccde6474..bc29edf7e7 100644 --- a/src/cosign/signature_layers.rs +++ b/src/cosign/signature_layers.rs @@ -387,7 +387,7 @@ impl CertificateSignature { let integrated_time = trusted_bundle.payload.integrated_time; // ensure the certificate has been issued by Fulcio - fulcio_cert_pool.verify(cert_raw)?; + fulcio_cert_pool.verify_pem_cert(cert_raw)?; crypto::certificate::is_trusted(&cert, integrated_time)?; diff --git a/src/crypto/certificate_pool.rs b/src/crypto/certificate_pool.rs index 4a8b3f43be..e0a4d3d153 100644 --- a/src/crypto/certificate_pool.rs +++ b/src/crypto/certificate_pool.rs @@ -93,12 +93,28 @@ impl CertificatePool { /// 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(|_| { + 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)?; + let cert = picky::x509::Cert::from_pem_str(cert_pem_str)?; + self.verify(&cert) + } + + /// 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_der_cert(&self, bytes: &[u8]) -> Result<()> { + let cert = picky::x509::Cert::from_der(bytes)?; + self.verify(&cert) + } + fn verify(&self, cert: &picky::x509::Cert) -> Result<()> { let verified = self .create_chains_for_all_certificates() .iter() From a46f5c1822be45021009f33ddd1dc714d886ae87 Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Mon, 14 Nov 2022 09:58:40 +0100 Subject: [PATCH 3/7] feat: x509 SubjectPublicKeyInfo to CosignVerificationKey Implement the `TryFrom` trait that allows to convert a x509 SubjectPublicKeyInfo into a CosignVerificationKey. Signed-off-by: Flavio Castelli --- src/crypto/mod.rs | 97 +++++++++++++++++---- src/crypto/verification_key.rs | 150 +++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 18 deletions(-) diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 10cbe61fc6..d27b83e082 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -192,7 +192,7 @@ OSWS1X9vPavpiQOoTTGC0xX57OojUadxF1cdQmrsiReWg2Wn4FneJfa8xw== pub(crate) struct CertData { pub cert: X509, - pub private_key: EcKey, + pub private_key: pkey::PKey, } pub(crate) struct CertGenerationOptions { @@ -206,6 +206,8 @@ OSWS1X9vPavpiQOoTTGC0xX57OojUadxF1cdQmrsiReWg2Wn4FneJfa8xw== pub subject_issuer: Option, pub not_before: DateTime, pub not_after: DateTime, + pub private_key: pkey::PKey, + pub public_key: pkey::PKey, } impl Default for CertGenerationOptions { @@ -213,6 +215,12 @@ OSWS1X9vPavpiQOoTTGC0xX57OojUadxF1cdQmrsiReWg2Wn4FneJfa8xw== let not_before = Utc::now().checked_sub_signed(Duration::days(1)).unwrap(); let not_after = Utc::now().checked_add_signed(Duration::days(1)).unwrap(); + // Sigstore relies on NIST P-256 + // NIST P-256 is a Weierstrass curve specified in FIPS 186-4: Digital Signature Standard (DSS): + // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf + // Also known as prime256v1 (ANSI X9.62) and secp256r1 (SECG) + let (private_key, public_key) = generate_ecdsa_p256_keypair(); + CertGenerationOptions { digital_signature_key_usage: true, code_signing_extended_key_usage: true, @@ -221,26 +229,80 @@ OSWS1X9vPavpiQOoTTGC0xX57OojUadxF1cdQmrsiReWg2Wn4FneJfa8xw== subject_url: None, not_before, not_after, + private_key, + public_key, } } } - pub(crate) fn generate_certificate( - issuer: Option<&CertData>, - settings: CertGenerationOptions, - ) -> anyhow::Result { - // Sigstore relies on NIST P-256 - // NIST P-256 is a Weierstrass curve specified in FIPS 186-4: Digital Signature Standard (DSS): - // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf - // Also known as prime256v1 (ANSI X9.62) and secp256r1 (SECG) + pub(crate) fn generate_ecdsa_p256_keypair( + ) -> (pkey::PKey, pkey::PKey) { let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).expect("Cannot create EcGroup"); - let private_key = EcKey::generate(&group).expect("Cannot create private key"); - let public_key = private_key.public_key(); + let ec_private_key = EcKey::generate(&group).expect("Cannot create private key"); + let ec_public_key = ec_private_key.public_key(); + let ec_pub_key = + EcKey::from_public_key(&group, ec_public_key).expect("Cannot create ec pub key"); + + let public_key = pkey::PKey::from_ec_key(ec_pub_key).expect("Cannot create pkey"); + let private_key = pkey::PKey::from_ec_key(ec_private_key).expect("Cannot create pkey"); + (private_key, public_key) + } + + pub(crate) fn generate_ecdsa_p384_keypair( + ) -> (pkey::PKey, pkey::PKey) { + let group = EcGroup::from_curve_name(Nid::SECP384R1).expect("Cannot create EcGroup"); + let ec_private_key = EcKey::generate(&group).expect("Cannot create private key"); + let ec_public_key = ec_private_key.public_key(); let ec_pub_key = - EcKey::from_public_key(&group, public_key).expect("Cannot create ec pub key"); - let pkey = pkey::PKey::from_ec_key(ec_pub_key).expect("Cannot create pkey"); + EcKey::from_public_key(&group, ec_public_key).expect("Cannot create ec pub key"); + + let public_key = pkey::PKey::from_ec_key(ec_pub_key).expect("Cannot create pkey"); + let private_key = pkey::PKey::from_ec_key(ec_private_key).expect("Cannot create pkey"); + + (private_key, public_key) + } + + pub(crate) fn generate_rsa_keypair( + bits: u32, + ) -> (pkey::PKey, pkey::PKey) { + use openssl::rsa; + let rsa_private_key = rsa::Rsa::generate(bits).expect("Cannot generate RSA key"); + let rsa_public_key_pem = rsa_private_key + .public_key_to_pem() + .expect("Cannot obtain public key"); + let rsa_public_key = rsa::Rsa::public_key_from_pem(&rsa_public_key_pem) + .expect("Cannot create rsa_public_key"); + + let private_key = pkey::PKey::from_rsa(rsa_private_key).expect("cannot create private_key"); + let public_key = pkey::PKey::from_rsa(rsa_public_key).expect("cannot create public_key"); + + (private_key, public_key) + } + + pub(crate) fn generate_dsa_keypair( + bits: u32, + ) -> (pkey::PKey, pkey::PKey) { + use openssl::dsa; + + let dsa_private_key = dsa::Dsa::generate(bits).expect("Cannot generate DSA key"); + let dsa_public_key_pem = dsa_private_key + .public_key_to_pem() + .expect("Cannot obtain public key"); + let dsa_public_key = dsa::Dsa::public_key_from_pem(&dsa_public_key_pem) + .expect("Cannot create rsa_public_key"); + + let private_key = pkey::PKey::from_dsa(dsa_private_key).expect("cannot create private_key"); + let public_key = pkey::PKey::from_dsa(dsa_public_key).expect("cannot create public_key"); + + (private_key, public_key) + } + + pub(crate) fn generate_certificate( + issuer: Option<&CertData>, + settings: CertGenerationOptions, + ) -> anyhow::Result { let mut x509_name_builder = X509NameBuilder::new()?; x509_name_builder.append_entry_by_text("O", "tests")?; x509_name_builder.append_entry_by_text("CN", "sigstore.test")?; @@ -249,7 +311,7 @@ OSWS1X9vPavpiQOoTTGC0xX57OojUadxF1cdQmrsiReWg2Wn4FneJfa8xw== let mut x509_builder = openssl::x509::X509::builder()?; x509_builder.set_subject_name(&x509_name)?; x509_builder - .set_pubkey(&pkey) + .set_pubkey(&settings.public_key) .expect("Cannot set public key"); // set serial number @@ -376,11 +438,10 @@ OSWS1X9vPavpiQOoTTGC0xX57OojUadxF1cdQmrsiReWg2Wn4FneJfa8xw== } // sign the cert - let issuer_key = match issuer { + let issuer_pkey = match issuer { Some(issuer_data) => issuer_data.private_key.clone(), - None => private_key.clone(), + None => settings.private_key.clone(), }; - let issuer_pkey = pkey::PKey::from_ec_key(issuer_key).expect("Cannot create signer pkey"); x509_builder .sign(&issuer_pkey, MessageDigest::sha256()) .expect("Cannot sign certificate"); @@ -389,7 +450,7 @@ OSWS1X9vPavpiQOoTTGC0xX57OojUadxF1cdQmrsiReWg2Wn4FneJfa8xw== Ok(CertData { cert: x509, - private_key, + private_key: settings.private_key, }) } } diff --git a/src/crypto/verification_key.rs b/src/crypto/verification_key.rs index be81107107..2d37f90825 100644 --- a/src/crypto/verification_key.rs +++ b/src/crypto/verification_key.rs @@ -17,6 +17,7 @@ use pkcs8::DecodePublicKey; use rsa::{pkcs1v15, pss}; use sha2::{Digest, Sha256, Sha384}; use signature::{DigestVerifier, Signature as _, Verifier}; +use std::convert::TryFrom; use x509_parser::{prelude::FromDer, x509::SubjectPublicKeyInfo}; use super::{ @@ -53,6 +54,47 @@ pub enum CosignVerificationKey { ED25519(ed25519_dalek_fiat::PublicKey), } +/// Attempts to convert a [x509 Subject Public Key Info](SubjectPublicKeyInfo) object into +/// a `CosignVerificationKey` one. +/// +/// Currently can convert only the following types of keys: +/// * ECDSA P-256: assumes the SHA-256 digest algorithm is used +/// * ECDSA P-384: assumes the SHA-384 digest algorithm is used +/// * RSA: assumes PKCS1 padding is used +impl<'a> TryFrom<&SubjectPublicKeyInfo<'a>> for CosignVerificationKey { + type Error = SigstoreError; + + fn try_from(subject_pub_key_info: &SubjectPublicKeyInfo<'a>) -> Result { + use x509_parser::public_key::PublicKey; + + let pubkey = subject_pub_key_info.parsed()?; + match pubkey { + PublicKey::EC(_) => match pubkey.key_size() { + 256 => CosignVerificationKey::from_der( + subject_pub_key_info.raw, + &SigningScheme::ECDSA_P256_SHA256_ASN1, + ), + 384 => CosignVerificationKey::from_der( + subject_pub_key_info.raw, + &SigningScheme::ECDSA_P384_SHA384_ASN1, + ), + _ => Err(SigstoreError::PublicKeyUnsupportedAlgorithmError(format!( + "EC with size {} is not supported", + pubkey.key_size() + ))), + }, + PublicKey::RSA(_) => CosignVerificationKey::from_der( + subject_pub_key_info.raw, + &SigningScheme::RSA_PKCS1_SHA256(pubkey.key_size()), + ), + _ => Err(SigstoreError::PublicKeyUnsupportedAlgorithmError(format!( + "Key with algorithm OID {} is not supported", + subject_pub_key_info.algorithm.oid() + ))), + } + } +} + impl CosignVerificationKey { /// Builds a [`CosignVerificationKey`] from DER-encoded data. The methods takes care /// of extracting the SubjectPublicKeyInfo from the DER-encoded data. @@ -377,4 +419,112 @@ DwIDAQAB .verify_signature(signature, msg.as_bytes()) .is_ok()); } + + #[test] + fn convert_ecdsa_p256_subject_public_key_to_cosign_verification_key() -> anyhow::Result<()> { + let (private_key, public_key) = generate_ecdsa_p256_keypair(); + let issued_cert_generation_options = CertGenerationOptions { + private_key, + public_key, + ..Default::default() + }; + + let ca_data = generate_certificate(None, CertGenerationOptions::default())?; + + let issued_cert = generate_certificate(Some(&ca_data), issued_cert_generation_options)?; + let issued_cert_pem = issued_cert.cert.to_pem()?; + let (_, pem) = x509_parser::pem::parse_x509_pem(&issued_cert_pem)?; + let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents)?; + let subject_public_key = cert.public_key(); + + let cosign_verification_key = + CosignVerificationKey::try_from(subject_public_key).expect("conversion failed"); + + assert!(matches!( + cosign_verification_key, + CosignVerificationKey::ECDSA_P256_SHA256_ASN1(_) + )); + Ok(()) + } + + #[test] + fn convert_ecdsa_p384_subject_public_key_to_cosign_verification_key() -> anyhow::Result<()> { + let (private_key, public_key) = generate_ecdsa_p384_keypair(); + let issued_cert_generation_options = CertGenerationOptions { + private_key, + public_key, + ..Default::default() + }; + + let ca_data = generate_certificate(None, CertGenerationOptions::default())?; + + let issued_cert = generate_certificate(Some(&ca_data), issued_cert_generation_options)?; + let issued_cert_pem = issued_cert.cert.to_pem()?; + let (_, pem) = x509_parser::pem::parse_x509_pem(&issued_cert_pem)?; + let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents)?; + let subject_public_key = cert.public_key(); + + let cosign_verification_key = + CosignVerificationKey::try_from(subject_public_key).expect("conversion failed"); + + assert!(matches!( + cosign_verification_key, + CosignVerificationKey::ECDSA_P384_SHA384_ASN1(_) + )); + Ok(()) + } + + #[test] + fn convert_rsa_subject_public_key_to_cosign_verification_key() -> anyhow::Result<()> { + let (private_key, public_key) = generate_rsa_keypair(2048); + let issued_cert_generation_options = CertGenerationOptions { + private_key, + public_key, + ..Default::default() + }; + + let ca_data = generate_certificate(None, CertGenerationOptions::default())?; + + let issued_cert = generate_certificate(Some(&ca_data), issued_cert_generation_options)?; + let issued_cert_pem = issued_cert.cert.to_pem()?; + let (_, pem) = x509_parser::pem::parse_x509_pem(&issued_cert_pem)?; + let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents)?; + let subject_public_key = cert.public_key(); + + let cosign_verification_key = + CosignVerificationKey::try_from(subject_public_key).expect("conversion failed"); + + assert!(matches!( + cosign_verification_key, + CosignVerificationKey::RSA_PKCS1_SHA256(_) + )); + Ok(()) + } + + #[test] + fn convert_unsupported_curve_subject_public_key_to_cosign_verification_key( + ) -> anyhow::Result<()> { + let (private_key, public_key) = generate_dsa_keypair(2048); + let issued_cert_generation_options = CertGenerationOptions { + private_key, + public_key, + ..Default::default() + }; + + let ca_data = generate_certificate(None, CertGenerationOptions::default())?; + + let issued_cert = generate_certificate(Some(&ca_data), issued_cert_generation_options)?; + let issued_cert_pem = issued_cert.cert.to_pem()?; + let (_, pem) = x509_parser::pem::parse_x509_pem(&issued_cert_pem)?; + let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents)?; + let subject_public_key = cert.public_key(); + + let err = CosignVerificationKey::try_from(subject_public_key); + assert!(matches!( + err, + Err(SigstoreError::PublicKeyUnsupportedAlgorithmError(_)) + )); + + Ok(()) + } } From a48f2b496680a023cffca46bb4a6cb53039f5edd Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Mon, 14 Nov 2022 14:10:50 +0100 Subject: [PATCH 4/7] refactor: split verification_constraint file Move each verifier into its own `.rs` file. The original `verification_constraint.rs` file was too big. Note: the public API is not affected by this refactor. Everything keeps working as expected for the consumers of our library. Signed-off-by: Flavio Castelli --- src/cosign/verification_constraint.rs | 461 ------------------ .../annotation_verifier.rs | 29 ++ .../cert_subject_email_verifier.rs | 211 ++++++++ .../cert_subject_url_verifier.rs | 128 +++++ src/cosign/verification_constraint/mod.rs | 79 +++ .../public_key_verifier.rs | 59 +++ 6 files changed, 506 insertions(+), 461 deletions(-) delete mode 100644 src/cosign/verification_constraint.rs create mode 100644 src/cosign/verification_constraint/annotation_verifier.rs create mode 100644 src/cosign/verification_constraint/cert_subject_email_verifier.rs create mode 100644 src/cosign/verification_constraint/cert_subject_url_verifier.rs create mode 100644 src/cosign/verification_constraint/mod.rs create mode 100644 src/cosign/verification_constraint/public_key_verifier.rs diff --git a/src/cosign/verification_constraint.rs b/src/cosign/verification_constraint.rs deleted file mode 100644 index 9536621726..0000000000 --- a/src/cosign/verification_constraint.rs +++ /dev/null @@ -1,461 +0,0 @@ -// -// Copyright 2021 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. - -//! Structs that can be used to verify [`crate::cosign::SignatureLayer`] -//! with special business logic. -//! -//! This module provides already the most common kind of verification constraints: -//! * [`PublicKeyVerifier`]: ensure a signature has been produced by a specific -//! cosign key -//! * [`CertSubjectEmailVerifier`]: ensure a signature has been produced in keyless mode, -//! plus the email address associated with the signer matches a specific one -//! * [`CertSubjectUrlVerifier`]: ensure a signature has been produced in keyless mode, -//! plus the certificate SAN has a specific URI inside of it. This can be used to verify -//! signatures produced by GitHub Actions. -//! -//! Developers can define ad-hoc validation logic by creating a Struct that implements -//! the [`VerificationConstraintVec`] trait. - -use std::collections::HashMap; - -use super::signature_layers::{CertificateSubject, SignatureLayer}; -use crate::crypto::{CosignVerificationKey, SigningScheme}; -use crate::errors::Result; - -/// A list of objects implementing the [`VerificationConstraint`] trait -pub type VerificationConstraintVec = Vec>; - -/// A list of references to objects implementing the [`VerificationConstraint`] trait -pub type VerificationConstraintRefVec<'a> = Vec<&'a Box>; - -/// A trait that can be used to define verification constraints objects -/// that use a custom verification logic. -pub trait VerificationConstraint: std::fmt::Debug { - /// Given the `signature_layer` object, return `true` if the verification - /// check is satisfied. - /// - /// Developer can use the - /// [`errors::SigstoreError::VerificationConstraintError`](crate::errors::SigstoreError::VerificationConstraintError) - /// error when something goes wrong inside of the verification logic. - /// - /// ``` - /// use sigstore::{ - /// cosign::verification_constraint::VerificationConstraint, - /// cosign::signature_layers::SignatureLayer, - /// errors::{SigstoreError, Result}, - /// }; - /// - /// #[derive(Debug)] - /// struct MyVerifier{} - /// - /// impl VerificationConstraint for MyVerifier { - /// fn verify(&self, _sl: &SignatureLayer) -> Result { - /// Err(SigstoreError::VerificationConstraintError( - /// "something went wrong!".to_string())) - /// } - /// } - fn verify(&self, signature_layer: &SignatureLayer) -> Result; -} - -/// Verification Constraint for signatures produced with public/private keys -#[derive(Debug)] -pub struct PublicKeyVerifier { - key: CosignVerificationKey, -} - -impl PublicKeyVerifier { - /// Create a new instance of `PublicKeyVerifier`. - /// The `key_raw` variable holds a PEM encoded representation of the - /// public key to be used at verification time. - pub fn new(key_raw: &[u8], signing_scheme: &SigningScheme) -> Result { - let key = CosignVerificationKey::from_pem(key_raw, signing_scheme)?; - Ok(PublicKeyVerifier { key }) - } - - /// Create a new instance of `PublicKeyVerifier`. - /// The `key_raw` variable holds a PEM encoded representation of the - /// public key to be used at verification time. The verification - /// algorithm will be derived from the public key type: - /// * `RSA public key`: `RSA_PSS_SHA256` - /// * `EC public key with P-256 curve`: `ECDSA_P256_SHA256_ASN1` - /// * `EC public key with P-384 curve`: `ECDSA_P384_SHA384_ASN1` - /// * `Ed25519 public key`: `Ed25519` - pub fn try_from(key_raw: &[u8]) -> Result { - let key = CosignVerificationKey::try_from_pem(key_raw)?; - Ok(PublicKeyVerifier { key }) - } -} - -impl VerificationConstraint for PublicKeyVerifier { - fn verify(&self, signature_layer: &SignatureLayer) -> Result { - Ok(signature_layer.is_signed_by_key(&self.key)) - } -} - -/// Verification Constraint for signatures produced in keyless mode. -/// -/// Keyless signatures have a x509 certificate associated to them. This -/// verifier ensures the SAN portion of the certificate has an email -/// attribute that matches the one provided by the user. -/// -/// It's also possible to specify the `Issuer`, this is the name of the -/// identity provider that was used by the user to authenticate. -/// -/// For example, `cosign` produces the following signature when the user -/// relies on GitHub to authenticate himself: -/// -/// ```hcl -/// { -/// "critical": { -/// // not relevant -/// }, -/// "optional": { -/// "Bundle": { -/// // not relevant -/// }, -/// "Issuer": "https://github.com/login/oauth", -/// "Subject": "alice@example.com" -/// } -/// } -/// ``` -/// -/// The following constraints would be able to enforce this signature to be -/// found: -/// -/// ```rust -/// use sigstore::cosign::verification_constraint::CertSubjectEmailVerifier; -/// -/// // This looks only for the email address of the trusted user -/// let vc_email = CertSubjectEmailVerifier{ -/// email: String::from("alice@example.com"), -/// ..Default::default() -/// }; -/// -/// // This ensures the user authenticated via GitHub (see the issuer value), -/// // plus the email associated to his GitHub account must be the one specified. -/// let vc_email_and_issuer = CertSubjectEmailVerifier{ -/// email: String::from("alice@example.com"), -/// issuer: Some(String::from("https://github.com/login/oauth")), -/// }; -/// ``` -/// -/// When `issuer` is `None`, the value found inside of the signature's certificate -/// is not checked. -/// -/// For example, given the following constraint: -/// ```rust -/// use sigstore::cosign::verification_constraint::CertSubjectEmailVerifier; -/// -/// let constraint = CertSubjectEmailVerifier{ -/// email: String::from("alice@example.com"), -/// ..Default::default() -/// }; -/// ``` -/// -/// Both these signatures would be trusted: -/// ```hcl -/// [ -/// { -/// "critical": { -/// // not relevant -/// }, -/// "optional": { -/// "Bundle": { -/// // not relevant -/// }, -/// "Issuer": "https://github.com/login/oauth", -/// "Subject": "alice@example.com" -/// } -/// }, -/// { -/// "critical": { -/// // not relevant -/// }, -/// "optional": { -/// "Bundle": { -/// // not relevant -/// }, -/// "Issuer": "https://example.com/login/oauth", -/// "Subject": "alice@example.com" -/// } -/// } -/// ] -/// ``` -#[derive(Default, Debug)] -pub struct CertSubjectEmailVerifier { - pub email: String, - pub issuer: Option, -} - -impl VerificationConstraint for CertSubjectEmailVerifier { - fn verify(&self, signature_layer: &SignatureLayer) -> Result { - let verified = match &signature_layer.certificate_signature { - Some(signature) => { - let email_matches = match &signature.subject { - CertificateSubject::Email(e) => e == &self.email, - _ => false, - }; - - let issuer_matches = match self.issuer { - Some(_) => self.issuer == signature.issuer, - None => true, - }; - - email_matches && issuer_matches - } - _ => false, - }; - Ok(verified) - } -} - -/// Verification Constraint for signatures produced in keyless mode. -/// -/// Keyless signatures have a x509 certificate associated to them. This -/// verifier ensures the SAN portion of the certificate has a URI -/// attribute that matches the one provided by the user. -/// -/// The constraints needs also the `Issuer` to be provided, this is the name -/// of the identity provider that was used by the user to authenticate. -/// -/// This verifier can be used to check keyless signatures produced in -/// non-interactive mode inside of GitHub Actions. -/// -/// For example, `cosign` produces the following signature when the -/// OIDC token is extracted from the GITHUB_TOKEN: -/// -/// ```hcl -/// { -/// "critical": { -/// // not relevant -/// }, -/// "optional": { -/// "Bundle": { -/// // not relevant -/// }, -/// "Issuer": "https://token.actions.githubusercontent.com", -/// "Subject": "https://github.com/flavio/policy-secure-pod-images/.github/workflows/release.yml@refs/heads/main" -/// } -/// } -/// ``` -/// -/// The following constraint would be able to enforce this signature to be -/// found: -/// -/// ```rust -/// use sigstore::cosign::verification_constraint::CertSubjectUrlVerifier; -/// -/// let vc = CertSubjectUrlVerifier{ -/// url: String::from("https://github.com/flavio/policy-secure-pod-images/.github/workflows/release.yml@refs/heads/main"), -/// issuer: String::from("https://token.actions.githubusercontent.com"), -/// }; -/// ``` -#[derive(Default, Debug)] -pub struct CertSubjectUrlVerifier { - pub url: String, - pub issuer: String, -} - -impl VerificationConstraint for CertSubjectUrlVerifier { - fn verify(&self, signature_layer: &SignatureLayer) -> Result { - let verified = match &signature_layer.certificate_signature { - Some(signature) => { - let url_matches = match &signature.subject { - CertificateSubject::Uri(u) => u == &self.url, - _ => false, - }; - let issuer_matches = Some(self.issuer.clone()) == signature.issuer; - - url_matches && issuer_matches - } - _ => false, - }; - Ok(verified) - } -} - -/// Verification Constraint for the annotations added by `cosign sign` -/// -/// The `SimpleSigning` object produced at signature time can be enriched by -/// signer with so called "anntoations". -/// -/// This constraint ensures that all the annotations specified by the user are -/// found inside of the SignatureLayer. -/// -/// It's perfectly find for the SignatureLayer to have additional annotations. -/// These will be simply be ignored by the verifier. -#[derive(Default, Debug)] -pub struct AnnotationVerifier { - pub annotations: HashMap, -} - -impl VerificationConstraint for AnnotationVerifier { - fn verify(&self, signature_layer: &SignatureLayer) -> Result { - let verified = signature_layer - .simple_signing - .satisfies_annotations(&self.annotations); - Ok(verified) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cosign::signature_layers::tests::{ - build_correct_signature_layer_with_certificate, - build_correct_signature_layer_without_bundle, - }; - use crate::cosign::signature_layers::CertificateSubject; - - #[test] - fn pub_key_verifier() { - let (sl, key) = build_correct_signature_layer_without_bundle(); - - let vc = PublicKeyVerifier { key }; - assert!(vc.verify(&sl).unwrap()); - - let sl = build_correct_signature_layer_with_certificate(); - assert!(!vc.verify(&sl).unwrap()); - } - - #[test] - fn cert_email_verifier_only_email() { - let email = "alice@example.com".to_string(); - let mut sl = build_correct_signature_layer_with_certificate(); - let mut cert_signature = sl.certificate_signature.unwrap(); - let cert_subj = CertificateSubject::Email(email.clone()); - cert_signature.issuer = None; - cert_signature.subject = cert_subj; - sl.certificate_signature = Some(cert_signature); - - let vc = CertSubjectEmailVerifier { - email, - issuer: None, - }; - assert!(vc.verify(&sl).unwrap()); - - let vc = CertSubjectEmailVerifier { - email: "different@email.com".to_string(), - issuer: None, - }; - assert!(!vc.verify(&sl).unwrap()); - } - - #[test] - fn cert_email_verifier_email_and_issuer() { - let email = "alice@example.com".to_string(); - let mut sl = build_correct_signature_layer_with_certificate(); - let mut cert_signature = sl.certificate_signature.unwrap(); - - // The cerificate subject doesn't have an issuer - let cert_subj = CertificateSubject::Email(email.clone()); - cert_signature.issuer = None; - cert_signature.subject = cert_subj; - sl.certificate_signature = Some(cert_signature.clone()); - - // fail because the issuer we want doesn't exist - let vc = CertSubjectEmailVerifier { - email: email.clone(), - issuer: Some("an issuer".to_string()), - }; - assert!(!vc.verify(&sl).unwrap()); - - // The cerificate subject has an issuer - let issuer = "the issuer".to_string(); - let cert_subj = CertificateSubject::Email(email.clone()); - cert_signature.issuer = Some(issuer.clone()); - cert_signature.subject = cert_subj; - sl.certificate_signature = Some(cert_signature); - - let vc = CertSubjectEmailVerifier { - email: email.clone(), - issuer: Some(issuer.clone()), - }; - assert!(vc.verify(&sl).unwrap()); - - let vc = CertSubjectEmailVerifier { - email, - issuer: Some("another issuer".to_string()), - }; - assert!(!vc.verify(&sl).unwrap()); - - // another verifier should fail - let vc = CertSubjectUrlVerifier { - url: "https://sigstore.dev/test".to_string(), - issuer, - }; - assert!(!vc.verify(&sl).unwrap()); - } - - #[test] - fn cert_email_verifier_no_signature() { - let (sl, _) = build_correct_signature_layer_without_bundle(); - - let vc = CertSubjectEmailVerifier { - email: "alice@example.com".to_string(), - issuer: None, - }; - assert!(!vc.verify(&sl).unwrap()); - } - - #[test] - fn cert_subject_url_verifier() { - let url = "https://sigstore.dev/test".to_string(); - let issuer = "the issuer".to_string(); - - let mut sl = build_correct_signature_layer_with_certificate(); - let mut cert_signature = sl.certificate_signature.unwrap(); - let cert_subj = CertificateSubject::Uri(url.clone()); - cert_signature.issuer = Some(issuer.clone()); - cert_signature.subject = cert_subj; - sl.certificate_signature = Some(cert_signature); - - let vc = CertSubjectUrlVerifier { - url: url.clone(), - issuer: issuer.clone(), - }; - assert!(vc.verify(&sl).unwrap()); - - let vc = CertSubjectUrlVerifier { - url: "a different url".to_string(), - issuer: issuer.clone(), - }; - assert!(!vc.verify(&sl).unwrap()); - - let vc = CertSubjectUrlVerifier { - url, - issuer: "a different issuer".to_string(), - }; - assert!(!vc.verify(&sl).unwrap()); - - // A Cert email verifier should also report a non match - let vc = CertSubjectEmailVerifier { - email: "alice@example.com".to_string(), - issuer: Some(issuer), - }; - assert!(!vc.verify(&sl).unwrap()); - } - - #[test] - fn cert_subject_verifier_no_signature() { - let (sl, _) = build_correct_signature_layer_without_bundle(); - - let vc = CertSubjectUrlVerifier { - url: "https://sigstore.dev/test".to_string(), - issuer: "an issuer".to_string(), - }; - assert!(!vc.verify(&sl).unwrap()); - } -} diff --git a/src/cosign/verification_constraint/annotation_verifier.rs b/src/cosign/verification_constraint/annotation_verifier.rs new file mode 100644 index 0000000000..37d3bdd508 --- /dev/null +++ b/src/cosign/verification_constraint/annotation_verifier.rs @@ -0,0 +1,29 @@ +use std::collections::HashMap; + +use super::VerificationConstraint; +use crate::cosign::signature_layers::SignatureLayer; +use crate::errors::Result; + +/// Verification Constraint for the annotations added by `cosign sign` +/// +/// The `SimpleSigning` object produced at signature time can be enriched by +/// signer with so called "anntoations". +/// +/// This constraint ensures that all the annotations specified by the user are +/// found inside of the SignatureLayer. +/// +/// It's perfectly find for the SignatureLayer to have additional annotations. +/// These will be simply be ignored by the verifier. +#[derive(Default, Debug)] +pub struct AnnotationVerifier { + pub annotations: HashMap, +} + +impl VerificationConstraint for AnnotationVerifier { + fn verify(&self, signature_layer: &SignatureLayer) -> Result { + let verified = signature_layer + .simple_signing + .satisfies_annotations(&self.annotations); + Ok(verified) + } +} diff --git a/src/cosign/verification_constraint/cert_subject_email_verifier.rs b/src/cosign/verification_constraint/cert_subject_email_verifier.rs new file mode 100644 index 0000000000..1b7a8b2f08 --- /dev/null +++ b/src/cosign/verification_constraint/cert_subject_email_verifier.rs @@ -0,0 +1,211 @@ +use super::VerificationConstraint; +use crate::cosign::signature_layers::{CertificateSubject, SignatureLayer}; +use crate::errors::Result; + +/// Verification Constraint for signatures produced in keyless mode. +/// +/// Keyless signatures have a x509 certificate associated to them. This +/// verifier ensures the SAN portion of the certificate has an email +/// attribute that matches the one provided by the user. +/// +/// It's also possible to specify the `Issuer`, this is the name of the +/// identity provider that was used by the user to authenticate. +/// +/// For example, `cosign` produces the following signature when the user +/// relies on GitHub to authenticate himself: +/// +/// ```hcl +/// { +/// "critical": { +/// // not relevant +/// }, +/// "optional": { +/// "Bundle": { +/// // not relevant +/// }, +/// "Issuer": "https://github.com/login/oauth", +/// "Subject": "alice@example.com" +/// } +/// } +/// ``` +/// +/// The following constraints would be able to enforce this signature to be +/// found: +/// +/// ```rust +/// use sigstore::cosign::verification_constraint::CertSubjectEmailVerifier; +/// +/// // This looks only for the email address of the trusted user +/// let vc_email = CertSubjectEmailVerifier{ +/// email: String::from("alice@example.com"), +/// ..Default::default() +/// }; +/// +/// // This ensures the user authenticated via GitHub (see the issuer value), +/// // plus the email associated to his GitHub account must be the one specified. +/// let vc_email_and_issuer = CertSubjectEmailVerifier{ +/// email: String::from("alice@example.com"), +/// issuer: Some(String::from("https://github.com/login/oauth")), +/// }; +/// ``` +/// +/// When `issuer` is `None`, the value found inside of the signature's certificate +/// is not checked. +/// +/// For example, given the following constraint: +/// ```rust +/// use sigstore::cosign::verification_constraint::CertSubjectEmailVerifier; +/// +/// let constraint = CertSubjectEmailVerifier{ +/// email: String::from("alice@example.com"), +/// ..Default::default() +/// }; +/// ``` +/// +/// Both these signatures would be trusted: +/// ```hcl +/// [ +/// { +/// "critical": { +/// // not relevant +/// }, +/// "optional": { +/// "Bundle": { +/// // not relevant +/// }, +/// "Issuer": "https://github.com/login/oauth", +/// "Subject": "alice@example.com" +/// } +/// }, +/// { +/// "critical": { +/// // not relevant +/// }, +/// "optional": { +/// "Bundle": { +/// // not relevant +/// }, +/// "Issuer": "https://example.com/login/oauth", +/// "Subject": "alice@example.com" +/// } +/// } +/// ] +/// ``` +#[derive(Default, Debug)] +pub struct CertSubjectEmailVerifier { + pub email: String, + pub issuer: Option, +} + +impl VerificationConstraint for CertSubjectEmailVerifier { + fn verify(&self, signature_layer: &SignatureLayer) -> Result { + let verified = match &signature_layer.certificate_signature { + Some(signature) => { + let email_matches = match &signature.subject { + CertificateSubject::Email(e) => e == &self.email, + _ => false, + }; + + let issuer_matches = match self.issuer { + Some(_) => self.issuer == signature.issuer, + None => true, + }; + + email_matches && issuer_matches + } + _ => false, + }; + Ok(verified) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cosign::signature_layers::tests::{ + build_correct_signature_layer_with_certificate, + build_correct_signature_layer_without_bundle, + }; + use crate::cosign::signature_layers::CertificateSubject; + use crate::cosign::verification_constraint::CertSubjectUrlVerifier; + + #[test] + fn cert_email_verifier_only_email() { + let email = "alice@example.com".to_string(); + let mut sl = build_correct_signature_layer_with_certificate(); + let mut cert_signature = sl.certificate_signature.unwrap(); + let cert_subj = CertificateSubject::Email(email.clone()); + cert_signature.issuer = None; + cert_signature.subject = cert_subj; + sl.certificate_signature = Some(cert_signature); + + let vc = CertSubjectEmailVerifier { + email, + issuer: None, + }; + assert!(vc.verify(&sl).unwrap()); + + let vc = CertSubjectEmailVerifier { + email: "different@email.com".to_string(), + issuer: None, + }; + assert!(!vc.verify(&sl).unwrap()); + } + + #[test] + fn cert_email_verifier_email_and_issuer() { + let email = "alice@example.com".to_string(); + let mut sl = build_correct_signature_layer_with_certificate(); + let mut cert_signature = sl.certificate_signature.unwrap(); + + // The cerificate subject doesn't have an issuer + let cert_subj = CertificateSubject::Email(email.clone()); + cert_signature.issuer = None; + cert_signature.subject = cert_subj; + sl.certificate_signature = Some(cert_signature.clone()); + + // fail because the issuer we want doesn't exist + let vc = CertSubjectEmailVerifier { + email: email.clone(), + issuer: Some("an issuer".to_string()), + }; + assert!(!vc.verify(&sl).unwrap()); + + // The cerificate subject has an issuer + let issuer = "the issuer".to_string(); + let cert_subj = CertificateSubject::Email(email.clone()); + cert_signature.issuer = Some(issuer.clone()); + cert_signature.subject = cert_subj; + sl.certificate_signature = Some(cert_signature); + + let vc = CertSubjectEmailVerifier { + email: email.clone(), + issuer: Some(issuer.clone()), + }; + assert!(vc.verify(&sl).unwrap()); + + let vc = CertSubjectEmailVerifier { + email, + issuer: Some("another issuer".to_string()), + }; + assert!(!vc.verify(&sl).unwrap()); + + // another verifier should fail + let vc = CertSubjectUrlVerifier { + url: "https://sigstore.dev/test".to_string(), + issuer, + }; + assert!(!vc.verify(&sl).unwrap()); + } + + #[test] + fn cert_email_verifier_no_signature() { + let (sl, _) = build_correct_signature_layer_without_bundle(); + + let vc = CertSubjectEmailVerifier { + email: "alice@example.com".to_string(), + issuer: None, + }; + assert!(!vc.verify(&sl).unwrap()); + } +} diff --git a/src/cosign/verification_constraint/cert_subject_url_verifier.rs b/src/cosign/verification_constraint/cert_subject_url_verifier.rs new file mode 100644 index 0000000000..739aa9e66d --- /dev/null +++ b/src/cosign/verification_constraint/cert_subject_url_verifier.rs @@ -0,0 +1,128 @@ +use super::VerificationConstraint; +use crate::cosign::signature_layers::{CertificateSubject, SignatureLayer}; +use crate::errors::Result; + +/// Verification Constraint for signatures produced in keyless mode. +/// +/// Keyless signatures have a x509 certificate associated to them. This +/// verifier ensures the SAN portion of the certificate has a URI +/// attribute that matches the one provided by the user. +/// +/// The constraints needs also the `Issuer` to be provided, this is the name +/// of the identity provider that was used by the user to authenticate. +/// +/// This verifier can be used to check keyless signatures produced in +/// non-interactive mode inside of GitHub Actions. +/// +/// For example, `cosign` produces the following signature when the +/// OIDC token is extracted from the GITHUB_TOKEN: +/// +/// ```hcl +/// { +/// "critical": { +/// // not relevant +/// }, +/// "optional": { +/// "Bundle": { +/// // not relevant +/// }, +/// "Issuer": "https://token.actions.githubusercontent.com", +/// "Subject": "https://github.com/flavio/policy-secure-pod-images/.github/workflows/release.yml@refs/heads/main" +/// } +/// } +/// ``` +/// +/// The following constraint would be able to enforce this signature to be +/// found: +/// +/// ```rust +/// use sigstore::cosign::verification_constraint::CertSubjectUrlVerifier; +/// +/// let vc = CertSubjectUrlVerifier{ +/// url: String::from("https://github.com/flavio/policy-secure-pod-images/.github/workflows/release.yml@refs/heads/main"), +/// issuer: String::from("https://token.actions.githubusercontent.com"), +/// }; +/// ``` +#[derive(Default, Debug)] +pub struct CertSubjectUrlVerifier { + pub url: String, + pub issuer: String, +} + +impl VerificationConstraint for CertSubjectUrlVerifier { + fn verify(&self, signature_layer: &SignatureLayer) -> Result { + let verified = match &signature_layer.certificate_signature { + Some(signature) => { + let url_matches = match &signature.subject { + CertificateSubject::Uri(u) => u == &self.url, + _ => false, + }; + let issuer_matches = Some(self.issuer.clone()) == signature.issuer; + + url_matches && issuer_matches + } + _ => false, + }; + Ok(verified) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cosign::signature_layers::tests::{ + build_correct_signature_layer_with_certificate, + build_correct_signature_layer_without_bundle, + }; + use crate::cosign::signature_layers::CertificateSubject; + use crate::cosign::verification_constraint::CertSubjectEmailVerifier; + + #[test] + fn cert_subject_url_verifier() { + let url = "https://sigstore.dev/test".to_string(); + let issuer = "the issuer".to_string(); + + let mut sl = build_correct_signature_layer_with_certificate(); + let mut cert_signature = sl.certificate_signature.unwrap(); + let cert_subj = CertificateSubject::Uri(url.clone()); + cert_signature.issuer = Some(issuer.clone()); + cert_signature.subject = cert_subj; + sl.certificate_signature = Some(cert_signature); + + let vc = CertSubjectUrlVerifier { + url: url.clone(), + issuer: issuer.clone(), + }; + assert!(vc.verify(&sl).unwrap()); + + let vc = CertSubjectUrlVerifier { + url: "a different url".to_string(), + issuer: issuer.clone(), + }; + assert!(!vc.verify(&sl).unwrap()); + + let vc = CertSubjectUrlVerifier { + url, + issuer: "a different issuer".to_string(), + }; + assert!(!vc.verify(&sl).unwrap()); + + // A Cert email verifier should also report a non match + let vc = CertSubjectEmailVerifier { + email: "alice@example.com".to_string(), + issuer: Some(issuer), + }; + assert!(!vc.verify(&sl).unwrap()); + } + + #[test] + fn cert_subject_verifier_no_signature() { + let (sl, _) = build_correct_signature_layer_without_bundle(); + + let vc = CertSubjectUrlVerifier { + url: "https://sigstore.dev/test".to_string(), + issuer: "an issuer".to_string(), + }; + assert!(!vc.verify(&sl).unwrap()); + } +} diff --git a/src/cosign/verification_constraint/mod.rs b/src/cosign/verification_constraint/mod.rs new file mode 100644 index 0000000000..a358893b5f --- /dev/null +++ b/src/cosign/verification_constraint/mod.rs @@ -0,0 +1,79 @@ +// +// Copyright 2021 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. + +//! Structs that can be used to verify [`crate::cosign::SignatureLayer`] +//! with special business logic. +//! +//! This module provides already the most common kind of verification constraints: +//! * [`PublicKeyVerifier`]: ensure a signature has been produced by a specific +//! cosign key +//! * [`CertSubjectEmailVerifier`]: ensure a signature has been produced in keyless mode, +//! plus the email address associated with the signer matches a specific one +//! * [`CertSubjectUrlVerifier`]: ensure a signature has been produced in keyless mode, +//! plus the certificate SAN has a specific URI inside of it. This can be used to verify +//! signatures produced by GitHub Actions. +//! +//! Developers can define ad-hoc validation logic by creating a Struct that implements +//! the [`VerificationConstraintVec`] trait. + +use super::signature_layers::SignatureLayer; +use crate::errors::Result; + +/// A list of objects implementing the [`VerificationConstraint`] trait +pub type VerificationConstraintVec = Vec>; + +/// A list of references to objects implementing the [`VerificationConstraint`] trait +pub type VerificationConstraintRefVec<'a> = Vec<&'a Box>; + +/// A trait that can be used to define verification constraints objects +/// that use a custom verification logic. +pub trait VerificationConstraint: std::fmt::Debug { + /// Given the `signature_layer` object, return `true` if the verification + /// check is satisfied. + /// + /// Developer can use the + /// [`errors::SigstoreError::VerificationConstraintError`](crate::errors::SigstoreError::VerificationConstraintError) + /// error when something goes wrong inside of the verification logic. + /// + /// ``` + /// use sigstore::{ + /// cosign::verification_constraint::VerificationConstraint, + /// cosign::signature_layers::SignatureLayer, + /// errors::{SigstoreError, Result}, + /// }; + /// + /// #[derive(Debug)] + /// struct MyVerifier{} + /// + /// impl VerificationConstraint for MyVerifier { + /// fn verify(&self, _sl: &SignatureLayer) -> Result { + /// Err(SigstoreError::VerificationConstraintError( + /// "something went wrong!".to_string())) + /// } + /// } + fn verify(&self, signature_layer: &SignatureLayer) -> Result; +} + +pub mod public_key_verifier; +pub use public_key_verifier::PublicKeyVerifier; + +pub mod cert_subject_email_verifier; +pub use cert_subject_email_verifier::CertSubjectEmailVerifier; + +pub mod cert_subject_url_verifier; +pub use cert_subject_url_verifier::CertSubjectUrlVerifier; + +pub mod annotation_verifier; +pub use annotation_verifier::AnnotationVerifier; diff --git a/src/cosign/verification_constraint/public_key_verifier.rs b/src/cosign/verification_constraint/public_key_verifier.rs new file mode 100644 index 0000000000..7e7546e982 --- /dev/null +++ b/src/cosign/verification_constraint/public_key_verifier.rs @@ -0,0 +1,59 @@ +use super::VerificationConstraint; +use crate::cosign::signature_layers::SignatureLayer; +use crate::crypto::{CosignVerificationKey, SigningScheme}; +use crate::errors::Result; + +/// Verification Constraint for signatures produced with public/private keys +#[derive(Debug)] +pub struct PublicKeyVerifier { + key: CosignVerificationKey, +} + +impl PublicKeyVerifier { + /// Create a new instance of `PublicKeyVerifier`. + /// The `key_raw` variable holds a PEM encoded representation of the + /// public key to be used at verification time. + pub fn new(key_raw: &[u8], signing_scheme: &SigningScheme) -> Result { + let key = CosignVerificationKey::from_pem(key_raw, signing_scheme)?; + Ok(PublicKeyVerifier { key }) + } + + /// Create a new instance of `PublicKeyVerifier`. + /// The `key_raw` variable holds a PEM encoded representation of the + /// public key to be used at verification time. The verification + /// algorithm will be derived from the public key type: + /// * `RSA public key`: `RSA_PSS_SHA256` + /// * `EC public key with P-256 curve`: `ECDSA_P256_SHA256_ASN1` + /// * `EC public key with P-384 curve`: `ECDSA_P384_SHA384_ASN1` + /// * `Ed25519 public key`: `Ed25519` + pub fn try_from(key_raw: &[u8]) -> Result { + let key = CosignVerificationKey::try_from_pem(key_raw)?; + Ok(PublicKeyVerifier { key }) + } +} + +impl VerificationConstraint for PublicKeyVerifier { + fn verify(&self, signature_layer: &SignatureLayer) -> Result { + Ok(signature_layer.is_signed_by_key(&self.key)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cosign::signature_layers::tests::{ + build_correct_signature_layer_with_certificate, + build_correct_signature_layer_without_bundle, + }; + + #[test] + fn pub_key_verifier() { + let (sl, key) = build_correct_signature_layer_without_bundle(); + + let vc = PublicKeyVerifier { key }; + assert!(vc.verify(&sl).unwrap()); + + let sl = build_correct_signature_layer_with_certificate(); + assert!(!vc.verify(&sl).unwrap()); + } +} From 3834f14dcb4d7fd3f020430ea6087f99677ffe62 Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Mon, 14 Nov 2022 17:30:50 +0100 Subject: [PATCH 5/7] feat: add certificate verification constraint Add a new verification constraint that uses a certificate provided by the user to verify signatures. This can be used to verify all the signatures created via `cosign sign --cert` or with a PKCS11 token Signed-off-by: Flavio Castelli --- .../certificate_verifier.rs | 277 ++++++++++++++++++ src/cosign/verification_constraint/mod.rs | 3 + 2 files changed, 280 insertions(+) create mode 100644 src/cosign/verification_constraint/certificate_verifier.rs diff --git a/src/cosign/verification_constraint/certificate_verifier.rs b/src/cosign/verification_constraint/certificate_verifier.rs new file mode 100644 index 0000000000..74fb31ee93 --- /dev/null +++ b/src/cosign/verification_constraint/certificate_verifier.rs @@ -0,0 +1,277 @@ +use std::convert::TryFrom; +use tracing::warn; +use x509_parser::{ + certificate::X509Certificate, + pem::parse_x509_pem, + prelude::{ASN1Time, FromDer}, +}; + +use super::VerificationConstraint; +use crate::cosign::signature_layers::SignatureLayer; +use crate::crypto::{certificate_pool::CertificatePool, CosignVerificationKey}; +use crate::errors::Result; + +/// Verify signature layers using the public key defined inside of a x509 certificate +#[derive(Debug)] +pub struct CertificateVerifier { + cert_verification_key: CosignVerificationKey, + cert_validity: x509_parser::certificate::Validity, + require_rekor_bundle: bool, +} + +impl CertificateVerifier { + /// Create a new instance of `CertificateVerifier` using the PEM encoded + /// certificate. + /// + /// * `cert_bytes`: PEM encoded certificate + /// * `require_rekor_bundle`: require the signature layer to have a Rekor + /// bundle. Having a Rekor bundle allows further checks to be performed, + /// like ensuring the signature has been produced during the validity + /// time frame of the certificate. It is recommended to set this value + /// to `true` to have a more secure verification process. + /// * `cert_chain`: the certificate chain that is used to verify the provided + /// certificate. When not specified, the certificate is assumed to be trusted + pub fn from_pem( + cert_bytes: &[u8], + require_rekor_bundle: bool, + cert_chain: Option<&[crate::registry::Certificate]>, + ) -> Result { + let (_, pem) = parse_x509_pem(cert_bytes)?; + Self::from_der(&pem.contents, require_rekor_bundle, cert_chain) + } + + /// Create a new instance of `CertificateVerifier` using the DER encoded + /// certificate. + /// + /// * `cert_bytes`: DER encoded certificate + /// * `require_rekor_bundle`: require the signature layer to have a Rekor + /// bundle. Having a Rekor bundle allows further checks to be performed, + /// like ensuring the signature has been produced during the validity + /// time frame of the certificate. It is recommended to set this value + /// to `true` to have a more secure verification process. + /// * `cert_chain`: the certificate chain that is used to verify the provided + /// certificate. When not specified, the certificate is assumed to be trusted + pub fn from_der( + cert_bytes: &[u8], + require_rekor_bundle: bool, + cert_chain: Option<&[crate::registry::Certificate]>, + ) -> Result { + let (_, cert) = X509Certificate::from_der(cert_bytes)?; + crate::crypto::certificate::verify_key_usages(&cert)?; + crate::crypto::certificate::verify_has_san(&cert)?; + 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 subject_public_key_info = cert.public_key(); + let cosign_verification_key = CosignVerificationKey::try_from(subject_public_key_info)?; + + Ok(Self { + cert_verification_key: cosign_verification_key, + cert_validity: cert.validity().to_owned(), + require_rekor_bundle, + }) + } +} + +impl VerificationConstraint for CertificateVerifier { + fn verify(&self, signature_layer: &SignatureLayer) -> Result { + if !signature_layer.is_signed_by_key(&self.cert_verification_key) { + return Ok(false); + } + match &signature_layer.bundle { + Some(bundle) => { + let it = ASN1Time::from_timestamp(bundle.payload.integrated_time)?; + if it < self.cert_validity.not_before { + warn!( + integrated_time = it.to_string(), + not_before = self.cert_validity.not_before.to_string(), + "certificate verification: ignoring layer, certificate expired before signature submitted to rekor" + ); + return Ok(false); + } + + if it > self.cert_validity.not_after { + warn!( + integrated_time = it.to_string(), + not_after = self.cert_validity.not_after.to_string(), + "certificate verification: ignoring layer, certificate issued after signatured submitted to rekor" + ); + return Ok(false); + } + Ok(true) + } + None => { + if self.require_rekor_bundle { + warn!("certificate verifier: ignoring layer because rekor bundle is missing"); + Ok(false) + } else { + Ok(true) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cosign::bundle::Bundle; + use crate::crypto::tests::*; + use crate::registry; + + use serde_json::json; + + #[test] + fn verify_certificate_() -> anyhow::Result<()> { + // use the correct CA chain + let ca_data = generate_certificate(None, CertGenerationOptions::default())?; + let ca_cert = registry::Certificate { + encoding: registry::CertificateEncoding::Pem, + data: ca_data.cert.to_pem()?, + }; + let cert_chain = vec![ca_cert]; + + let issued_cert = generate_certificate(Some(&ca_data), CertGenerationOptions::default())?; + let issued_cert_pem = issued_cert.cert.to_pem()?; + + let verifier = CertificateVerifier::from_pem(&issued_cert_pem, false, Some(&cert_chain)); + assert!(verifier.is_ok()); + + // Use a different CA chain + let another_ca_data = generate_certificate(None, CertGenerationOptions::default())?; + let another_ca_cert = registry::Certificate { + encoding: registry::CertificateEncoding::Pem, + data: another_ca_data.cert.to_pem()?, + }; + let cert_chain = vec![another_ca_cert]; + let verifier = CertificateVerifier::from_pem(&issued_cert_pem, false, Some(&cert_chain)); + assert!(verifier.is_err()); + + // No cert chain + let verifier = CertificateVerifier::from_pem(&issued_cert_pem, false, None); + assert!(verifier.is_ok()); + + Ok(()) + } + + /// Create a SignatureLayer using some hard coded value. Returns the + /// certificate that can be used to successfully verify the layer + fn test_data() -> (SignatureLayer, String) { + let ss_value = json!({ + "critical": { + "identity": { + "docker-reference": "registry-testing.svc.lan/kubewarden/pod-privileged" + }, + "image": { + "docker-manifest-digest": "sha256:f1143ec2786e13d7d3335dbb498528438d910648469d3f39647e1cde6914da8d" + }, + "type": "cosign container image signature" + }, + "optional": null + }); + + let bundle = build_bundle(); + + let cert_pem_raw = r#"-----BEGIN CERTIFICATE----- +MIICsTCCAligAwIBAgIUR8wkyvHURfBVH6K2uhfTJZItw3owCgYIKoZIzj0EAwIw +gZIxCzAJBgNVBAYTAkRFMRAwDgYDVQQIEwdCYXZhcmlhMRIwEAYDVQQHEwlOdXJl +bWJlcmcxEzARBgNVBAoTCkt1YmV3YXJkZW4xIzAhBgNVBAsTGkt1YmV3YXJkZW4g +SW50ZXJtZWRpYXRlIENBMSMwIQYDVQQDExpLdWJld2FyZGVuIEludGVybWVkaWF0 +ZSBDQTAeFw0yMjExMTAxMDM4MDBaFw0yMzExMTAxMDM4MDBaMIGFMQswCQYDVQQG +EwJERTEQMA4GA1UECBMHQmF2YXJpYTESMBAGA1UEBxMJTnVyZW1iZXJnMRMwEQYD +VQQKEwpLdWJld2FyZGVuMRgwFgYDVQQLEw9LdWJld2FyZGVuIFVzZXIxITAfBgNV +BAMTGHVzZXIxLmN1c3RvbS13aWRnZXRzLmNvbTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABEKjBtYLmtwhXNV1/uBanNn5YLD/QY/lfhPleBzenCL7CC2iocu8m3WM +PMfd06tE/9HbBAITf64Oc4Mp7abrzp2jgZYwgZMwDgYDVR0PAQH/BAQDAgeAMBMG +A1UdJQQMMAoGCCsGAQUFBwMDMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFHsx7jle +7PzGarNvliop+/aTj9GsMB8GA1UdIwQYMBaAFKJu6pRjVGUXVCVkft0YQ+3o1GbQ +MB4GA1UdEQQXMBWBE3VzZXIxQGt1YmV3YXJkZW4uaW8wCgYIKoZIzj0EAwIDRwAw +RAIgPixAn47x4qLpu7Y/d0oyvbnOGtD5cY7rywdMOO7LYRsCIDsCyGUZIYMFfSrt +3K/aLG49dcv6FKBtZpF5+hYj1zKe +-----END CERTIFICATE-----"# + .to_string(); + + let signature_layer = SignatureLayer { + simple_signing: serde_json::from_value(ss_value.clone()).unwrap(), + oci_digest: String::from("sha256:f9b817c013972c75de8689d55c0d441c3eb84f6233ac75f6a9c722ea5db0058b"), + signature: String::from("MEYCIQCIqLEe6hnjEXP/YC2P9OIwEr2yMmwPNHLzvCPaoaXFOQIhALyTouhKNKc2ZVrR0GUQ7J0U5AtlyDZDLGnasAi7XnV/"), + bundle: Some(bundle), + certificate_signature: None, + raw_data: serde_json::to_vec(&ss_value).unwrap(), + }; + + (signature_layer, cert_pem_raw) + } + + fn build_bundle() -> Bundle { + let bundle_value = json!({ + "SignedEntryTimestamp": "MEUCIG5TYOXkiPm7RGYgDIPHwRQW5NyoSPuwxvJe4ByB9c37AiEAyD0dVcsiJ5Lp+QY5SL80jDxfc75BtjRnticVf7SiFD0=", + "Payload": { + "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmOWI4MTdjMDEzOTcyYzc1ZGU4Njg5ZDU1YzBkNDQxYzNlYjg0ZjYyMzNhYzc1ZjZhOWM3MjJlYTVkYjAwNThiIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNJcUxFZTZobmpFWFAvWUMyUDlPSXdFcjJ5TW13UE5ITHp2Q1Bhb2FYRk9RSWhBTHlUb3VoS05LYzJaVnJSMEdVUTdKMFU1QXRseURaRExHbmFzQWk3WG5WLyIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnpWRU5EUVd4cFowRjNTVUpCWjBsVlVqaDNhM2wyU0ZWU1prSldTRFpMTW5Wb1psUktXa2wwZHpOdmQwTm5XVWxMYjFwSmVtb3dSVUYzU1hjS1oxcEplRU42UVVwQ1owNVdRa0ZaVkVGclVrWk5Va0YzUkdkWlJGWlJVVWxGZDJSRFdWaGFhR050YkdoTlVrbDNSVUZaUkZaUlVVaEZkMnhQWkZoS2JBcGlWMHBzWTIxamVFVjZRVkpDWjA1V1FrRnZWRU5yZERGWmJWWXpXVmhLYTFwWE5IaEpla0ZvUW1kT1ZrSkJjMVJIYTNReFdXMVdNMWxZU210YVZ6Um5DbE5YTlRCYVdFcDBXbGRTY0ZsWVVteEpSVTVDVFZOTmQwbFJXVVJXVVZGRVJYaHdUR1JYU214a01rWjVXa2RXZFVsRmJIVmtSMVo1WWxkV2EyRlhSakFLV2xOQ1JGRlVRV1ZHZHpCNVRXcEZlRTFVUVhoTlJFMDBUVVJDWVVaM01IbE5la1Y0VFZSQmVFMUVUVFJOUkVKaFRVbEhSazFSYzNkRFVWbEVWbEZSUndwRmQwcEZVbFJGVVUxQk5FZEJNVlZGUTBKTlNGRnRSakpaV0Vwd1dWUkZVMDFDUVVkQk1WVkZRbmhOU2xSdVZubGFWekZwV2xoS2JrMVNUWGRGVVZsRUNsWlJVVXRGZDNCTVpGZEtiR1F5Um5sYVIxWjFUVkpuZDBabldVUldVVkZNUlhjNVRHUlhTbXhrTWtaNVdrZFdkVWxHVm5wYVdFbDRTVlJCWmtKblRsWUtRa0ZOVkVkSVZucGFXRWw0VEcxT01XTXpVblppVXpFellWZFNibHBZVW5wTWJVNTJZbFJDV2sxQ1RVZENlWEZIVTAwME9VRm5SVWREUTNGSFUwMDBPUXBCZDBWSVFUQkpRVUpGUzJwQ2RGbE1iWFIzYUZoT1ZqRXZkVUpoYms1dU5WbE1SQzlSV1M5c1ptaFFiR1ZDZW1WdVEwdzNRME15YVc5amRUaHRNMWROQ2xCTlptUXdOblJGTHpsSVlrSkJTVlJtTmpSUFl6Uk5jRGRoWW5KNmNESnFaMXBaZDJkYVRYZEVaMWxFVmxJd1VFRlJTQzlDUVZGRVFXZGxRVTFDVFVjS1FURlZaRXBSVVUxTlFXOUhRME56UjBGUlZVWkNkMDFFVFVGM1IwRXhWV1JGZDBWQ0wzZFJRMDFCUVhkSVVWbEVWbEl3VDBKQ1dVVkdTSE40TjJwc1pRbzNVSHBIWVhKT2RteHBiM0FyTDJGVWFqbEhjMDFDT0VkQk1WVmtTWGRSV1UxQ1lVRkdTMHAxTm5CU2FsWkhWVmhXUTFaclpuUXdXVkVyTTI4eFIySlJDazFDTkVkQk1WVmtSVkZSV0UxQ1YwSkZNMVo2V2xoSmVGRkhkREZaYlZZeldWaEthMXBYTkhWaFZ6aDNRMmRaU1V0dldrbDZhakJGUVhkSlJGSjNRWGNLVWtGSloxQnBlRUZ1TkRkNE5IRk1jSFUzV1M5a01HOTVkbUp1VDBkMFJEVmpXVGR5ZVhka1RVOVBOMHhaVW5ORFNVUnpRM2xIVlZwSldVMUdabE55ZEFvelN5OWhURWMwT1dSamRqWkdTMEowV25CR05TdG9XV294ZWt0bENpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19", + "integratedTime": 1668077126, + "logIndex": 6821636, + "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" + } + }); + let bundle: Bundle = serde_json::from_value(bundle_value).expect("Cannot parse bundle"); + bundle + } + + #[test] + fn verify_correct_layer() { + let (signature_layer, cert_pem_raw) = test_data(); + + let vc = CertificateVerifier::from_pem(cert_pem_raw.as_bytes(), true, None) + .expect("cannot create verification constraint"); + assert!(vc.verify(&signature_layer).expect("error while verifying")); + } + + #[test] + fn rekor_integration() { + let (signature_layer, cert_pem_raw) = test_data(); + let signature_layer_without_rekor_bundle = SignatureLayer { + bundle: None, + ..signature_layer.clone() + }; + assert!(signature_layer_without_rekor_bundle.bundle.is_none()); + + let vc = CertificateVerifier::from_pem(cert_pem_raw.as_bytes(), true, None) + .expect("cannot create verification constraint"); + assert!(vc.verify(&signature_layer).expect("error while verifying")); + + // layer verification fails because there's no rekor bundle + assert!(!vc + .verify(&signature_layer_without_rekor_bundle) + .expect("error while verifying")); + + // verification constraint that does not enforce rekor integration + let vc = CertificateVerifier::from_pem(cert_pem_raw.as_bytes(), false, None) + .expect("cannot create verification constraint"); + assert!(vc + .verify(&signature_layer_without_rekor_bundle) + .expect("error while verifying")); + } + + #[test] + fn detect_signature_created_at_invalid_time() { + let (signature_layer, cert_pem_raw) = test_data(); + + let mut vc = CertificateVerifier::from_pem(cert_pem_raw.as_bytes(), true, None) + .expect("cannot create verification constraint"); + let now = ASN1Time::now().timestamp(); + let not_before = + ASN1Time::from_timestamp(now - 60).expect("cannot create not_before timestamp"); + let not_after = + ASN1Time::from_timestamp(now + 60).expect("cannot create not_after timestamp"); + let validity = x509_parser::certificate::Validity { + not_before, + not_after, + }; + vc.cert_validity = validity; + assert!(!vc.verify(&signature_layer).expect("error while verifying")); + } +} diff --git a/src/cosign/verification_constraint/mod.rs b/src/cosign/verification_constraint/mod.rs index a358893b5f..e9976972b8 100644 --- a/src/cosign/verification_constraint/mod.rs +++ b/src/cosign/verification_constraint/mod.rs @@ -66,6 +66,9 @@ pub trait VerificationConstraint: std::fmt::Debug { fn verify(&self, signature_layer: &SignatureLayer) -> Result; } +pub mod certificate_verifier; +pub use certificate_verifier::CertificateVerifier; + pub mod public_key_verifier; pub use public_key_verifier::PublicKeyVerifier; From 8316ee2f9c3cd51975733f794ac4ac5246302aae Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Mon, 14 Nov 2022 17:41:12 +0100 Subject: [PATCH 6/7] feat: expand verification example Expand the verify example to handle also certificate based verification. This is similar to what `cosign verify --cert` does. Signed-off-by: Flavio Castelli --- Cargo.toml | 2 +- examples/cosign/verify/main.rs | 58 +++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c8304fda88..22eea5a2ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ pkcs1 = "0.4.0" reqwest = { version = "0.11", default-features = false, features = ["json", "multipart"] } [dev-dependencies] -anyhow = "1.0.54" +anyhow = { version = "1.0", features = ["backtrace"] } assert-json-diff = "2.0.2" chrono = "0.4.20" clap = { version = "4.0.8", features = ["derive"] } diff --git a/examples/cosign/verify/main.rs b/examples/cosign/verify/main.rs index b99e59d955..096bb72ce6 100644 --- a/examples/cosign/verify/main.rs +++ b/examples/cosign/verify/main.rs @@ -15,8 +15,8 @@ extern crate sigstore; use sigstore::cosign::verification_constraint::{ - AnnotationVerifier, CertSubjectEmailVerifier, CertSubjectUrlVerifier, PublicKeyVerifier, - VerificationConstraintVec, + AnnotationVerifier, CertSubjectEmailVerifier, CertSubjectUrlVerifier, CertificateVerifier, + PublicKeyVerifier, VerificationConstraintVec, }; use sigstore::cosign::{CosignCapabilities, SignatureLayer}; use sigstore::crypto::SigningScheme; @@ -27,7 +27,7 @@ use std::convert::TryFrom; use std::time::Instant; extern crate anyhow; -use anyhow::anyhow; +use anyhow::{anyhow, Result}; extern crate clap; use clap::Parser; @@ -36,7 +36,7 @@ use std::{collections::HashMap, fs}; use tokio::task::spawn_blocking; extern crate tracing_subscriber; -use tracing::info; +use tracing::{info, warn}; use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; @@ -47,6 +47,14 @@ struct Cli { #[clap(short, long, required(false))] key: Option, + /// Path to verification certificate + #[clap(long, required(false))] + cert: Option, + + /// Path to certificate chain bundle file + #[clap(long, required(false))] + cert_chain: Option, + /// Signing scheme when signing and verifying #[clap(long, required(false))] signing_scheme: Option, @@ -106,6 +114,10 @@ async fn run_app( )); } + if cli.key.is_some() && cli.cert.is_some() { + return Err(anyhow!("'key' and 'cert' cannot be used at the same time")); + } + let auth = &sigstore::registry::Auth::Anonymous; let mut client_builder = sigstore::cosign::ClientBuilder::default(); @@ -114,6 +126,11 @@ async fn run_app( client_builder = client_builder.with_rekor_pub_key(key); } + 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); } @@ -163,6 +180,24 @@ async fn run_app( verification_constraints.push(Box::new(verifier)); } + 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() { + true + } else { + warn!("certificate based verification is weaker when Rekor integration is disabled"); + false + }; + + let verifier = CertificateVerifier::from_pem( + &cert, + require_rekor_bundle, + cert_chain.as_ref().map(|v| v.as_slice()), + ) + .map_err(|e| anyhow!("Cannot create certificate verifier: {}", e))?; + + verification_constraints.push(Box::new(verifier)); + } if !cli.annotations.is_empty() { let mut values: HashMap = HashMap::new(); @@ -266,6 +301,7 @@ pub async fn main() { &trusted_layers, verification_constraints.iter(), ); + match filter_result { Ok(()) => { println!("Image successfully verified"); @@ -291,3 +327,17 @@ pub async fn main() { } } } + +fn parse_cert_bundle(bundle_path: &str) -> Result> { + let data = + fs::read(bundle_path).map_err(|e| anyhow!("Error reading {}: {}", bundle_path, e))?; + let pems = pem::parse_many(&data)?; + + Ok(pems + .iter() + .map(|pem| sigstore::registry::Certificate { + encoding: sigstore::registry::CertificateEncoding::Der, + data: pem.contents.clone(), + }) + .collect()) +} From 23f4c927502e8fd02fa5632172c69f92f23a1c54 Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Tue, 15 Nov 2022 11:22:52 +0100 Subject: [PATCH 7/7] docs: fix small issue Fix the inline documentation of `PublicKeyVerifier::try_from` about which RSA padding scheme is used when loading the key from a PEM file. Signed-off-by: Flavio Castelli --- src/cosign/verification_constraint/public_key_verifier.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cosign/verification_constraint/public_key_verifier.rs b/src/cosign/verification_constraint/public_key_verifier.rs index 7e7546e982..6ec37c6386 100644 --- a/src/cosign/verification_constraint/public_key_verifier.rs +++ b/src/cosign/verification_constraint/public_key_verifier.rs @@ -22,7 +22,7 @@ impl PublicKeyVerifier { /// The `key_raw` variable holds a PEM encoded representation of the /// public key to be used at verification time. The verification /// algorithm will be derived from the public key type: - /// * `RSA public key`: `RSA_PSS_SHA256` + /// * `RSA public key`: `RSA_PKCS1_SHA256` /// * `EC public key with P-256 curve`: `ECDSA_P256_SHA256_ASN1` /// * `EC public key with P-384 curve`: `ECDSA_P384_SHA384_ASN1` /// * `Ed25519 public key`: `Ed25519`