sigstore 0.14.0

An experimental crate to interact with sigstore
Documentation
use chrono::{DateTime, Utc};
use pkcs8::der::Decode;
use pki_types::CertificateDer;
use tracing::warn;
use x509_cert::Certificate;

use super::VerificationConstraint;
use crate::cosign::signature_layers::SignatureLayer;
use crate::crypto::{CosignVerificationKey, certificate_pool::CertificatePool};
use crate::errors::{Result, SigstoreError};

/// 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_cert::time::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<Self> {
        let pem = pem::parse(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<Self> {
        let cert = Certificate::from_der(cert_bytes)
            .map_err(|e| SigstoreError::X509Error(format!("parse from der {e}")))?;
        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 certs = certs
                .iter()
                .map(|c| CertificateDer::try_from(c.clone()))
                .collect::<Result<Vec<_>>>()?;
            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;
        let cosign_verification_key = CosignVerificationKey::try_from(subject_public_key_info)?;

        Ok(Self {
            cert_verification_key: cosign_verification_key,
            cert_validity: cert.tbs_certificate.validity,
            require_rekor_bundle,
        })
    }
}

impl VerificationConstraint for CertificateVerifier {
    fn verify(&self, signature_layer: &SignatureLayer) -> Result<bool> {
        if !signature_layer.is_signed_by_key(&self.cert_verification_key) {
            return Ok(false);
        }
        match &signature_layer.bundle {
            Some(evidence) => {
                let it = DateTime::<Utc>::from_naive_utc_and_offset(
                    DateTime::from_timestamp(evidence.integrated_time(), 0)
                        .ok_or(SigstoreError::UnexpectedError(
                            "timestamp is not legal".into(),
                        ))?
                        .naive_utc(),
                    Utc,
                );
                let not_before: DateTime<Utc> =
                    self.cert_validity.not_before.to_system_time().into();
                if it < 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);
                }

                let not_after: DateTime<Utc> = self.cert_validity.not_after.to_system_time().into();
                if it > 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 transparency log evidence is missing"
                    );
                    Ok(false)
                } else {
                    Ok(true)
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use std::time::{Duration, SystemTime};

    use super::*;
    use crate::cosign::bundle::Bundle;
    use crate::cosign::bundle_content::BundleContent;
    use crate::crypto::tests::*;
    use crate::registry;

    use pkcs8::der::asn1::UtcTime;
    use rstest::rstest;
    use serde_json::json;
    use x509_cert::time::{Time, Validity};

    #[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_pem.clone(),
        };
        let cert_chain = vec![ca_cert];

        let issued_cert = generate_certificate(Some(&ca_data), CertGenerationOptions::default())?;
        let issued_cert_pem = issued_cert.cert_pem.clone();

        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_pem.clone(),
        };
        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.
    /// The cert has validity 2022-11-10 to 2023-11-10.
    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_rekor_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: Some(String::from(
                "MEYCIQCIqLEe6hnjEXP/YC2P9OIwEr2yMmwPNHLzvCPaoaXFOQIhALyTouhKNKc2ZVrR0GUQ7J0U5AtlyDZDLGnasAi7XnV/",
            )),
            bundle: Some(BundleContent::RekorBundle(bundle)),
            certificate_signature: None,
            raw_data: serde_json::to_vec(&ss_value).unwrap(),
        };

        (signature_layer, cert_pem_raw)
    }

    fn build_rekor_bundle() -> Bundle {
        let bundle_value = json!({
            "SignedEntryTimestamp": "MEUCIG5TYOXkiPm7RGYgDIPHwRQW5NyoSPuwxvJe4ByB9c37AiEAyD0dVcsiJ5Lp+QY5SL80jDxfc75BtjRnticVf7SiFD0=",
            "Payload": {
              "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmOWI4MTdjMDEzOTcyYzc1ZGU4Njg5ZDU1YzBkNDQxYzNlYjg0ZjYyMzNhYzc1ZjZhOWM3MjJlYTVkYjAwNThiIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNJcUxFZTZobmpFWFAvWUMyUDlPSXdFcjJ5TW13UE5ITHp2Q1Bhb2FYRk9RSWhBTHlUb3VoS05LYzJaVnJSMEdVUTdKMFU1QXRseURaRExHbmFzQWk3WG5WLyIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnpWRU5EUVd4cFowRjNTVUpCWjBsVlVqaDNhM2wyU0ZWU1prSldTRFpMTW5Wb1psUktXa2wwZHpOdmQwTm5XVWxMYjFwSmVtb3dSVUYzU1hjS1oxcEplRU42UVVwQ1owNVdRa0ZaVkVGclVrWk5Va0YzUkdkWlJGWlJVVWxGZDJSRFdWaGFhR050YkdoTlVrbDNSVUZaUkZaUlVVaEZkMnhQWkZoS2JBcGlWMHBzWTIxamVFVjZRVkpDWjA1V1FrRnZWRU5yZERGWmJWWXpXVmhLYTFwWE5IaEpla0ZvUW1kT1ZrSkJjMVJIYTNReFdXMVdNMWxZU210YVZ6Um5DbE5YTlRCYVdFcDBXbGRTY0ZsWVVteEpSVTVDVFZOTmQwbFJXVVJXVVZGRVJYaHdUR1JYU214a01rWjVXa2RXZFVsRmJIVmtSMVo1WWxkV2EyRlhSakFLV2xOQ1JGRlVRV1ZHZHpCNVRXcEZlRTFVUVhoTlJFMDBUVVJDWVVaM01IbE5la1Y0VFZSQmVFMUVUVFJOUkVKaFRVbEhSazFSYzNkRFVWbEVWbEZSUndwRmQwcEZVbFJGVVUxQk5FZEJNVlZGUTBKTlNGRnRSakpaV0Vwd1dWUkZVMDFDUVVkQk1WVkZRbmhOU2xSdVZubGFWekZwV2xoS2JrMVNUWGRGVVZsRUNsWlJVVXRGZDNCTVpGZEtiR1F5Um5sYVIxWjFUVkpuZDBabldVUldVVkZNUlhjNVRHUlhTbXhrTWtaNVdrZFdkVWxHVm5wYVdFbDRTVlJCWmtKblRsWUtRa0ZOVkVkSVZucGFXRWw0VEcxT01XTXpVblppVXpFellWZFNibHBZVW5wTWJVNTJZbFJDV2sxQ1RVZENlWEZIVTAwME9VRm5SVWREUTNGSFUwMDBPUXBCZDBWSVFUQkpRVUpGUzJwQ2RGbE1iWFIzYUZoT1ZqRXZkVUpoYms1dU5WbE1SQzlSV1M5c1ptaFFiR1ZDZW1WdVEwdzNRME15YVc5amRUaHRNMWROQ2xCTlptUXdOblJGTHpsSVlrSkJTVlJtTmpSUFl6Uk5jRGRoWW5KNmNESnFaMXBaZDJkYVRYZEVaMWxFVmxJd1VFRlJTQzlDUVZGRVFXZGxRVTFDVFVjS1FURlZaRXBSVVUxTlFXOUhRME56UjBGUlZVWkNkMDFFVFVGM1IwRXhWV1JGZDBWQ0wzZFJRMDFCUVhkSVVWbEVWbEl3VDBKQ1dVVkdTSE40TjJwc1pRbzNVSHBIWVhKT2RteHBiM0FyTDJGVWFqbEhjMDFDT0VkQk1WVmtTWGRSV1UxQ1lVRkdTMHAxTm5CU2FsWkhWVmhXUTFaclpuUXdXVkVyTTI4eFIySlJDazFDTkVkQk1WVmtSVkZSV0UxQ1YwSkZNMVo2V2xoSmVGRkhkREZaYlZZeldWaEthMXBYTkhWaFZ6aDNRMmRaU1V0dldrbDZhakJGUVhkSlJGSjNRWGNLVWtGSloxQ...",
              "integratedTime": 1668077126,
              "logIndex": 6821636,
              "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"
            }
        });
        serde_json::from_value(bundle_value).expect("Cannot parse bundle")
    }

    /// Build a minimal `BundleContent::SigstoreBundle` carrying only the
    /// given `integrated_time`.  All other fields are left at their protobuf
    /// defaults — `CertificateVerifier` only reads `integrated_time`.
    fn build_sigstore_bundle_content(integrated_time: i64) -> BundleContent {
        use sigstore_protobuf_specs::dev::sigstore::rekor::v1::TransparencyLogEntry;
        BundleContent::SigstoreBundle(TransparencyLogEntry {
            integrated_time,
            ..Default::default()
        })
    }

    // integrated_time 1668077126 == 2022-11-10, inside the cert validity
    // window of 2022-11-10 to 2023-11-10.
    #[rstest]
    #[case::rekor_bundle(BundleContent::RekorBundle(build_rekor_bundle()))]
    #[case::sigstore_bundle(build_sigstore_bundle_content(1668077126))]
    fn verify_with_valid_bundle(#[case] bundle: BundleContent) {
        let (mut signature_layer, cert_pem_raw) = test_data();
        signature_layer.bundle = Some(bundle);

        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 no_bundle_rejected_when_required() {
        let (signature_layer, cert_pem_raw) = test_data();
        let layer_without_bundle = SignatureLayer {
            bundle: None,
            ..signature_layer.clone()
        };

        let vc = CertificateVerifier::from_pem(cert_pem_raw.as_bytes(), true, None)
            .expect("cannot create verification constraint");

        // rejected when bundle is required
        assert!(
            !vc.verify(&layer_without_bundle)
                .expect("error while verifying")
        );

        // accepted when bundle is not required
        let vc = CertificateVerifier::from_pem(cert_pem_raw.as_bytes(), false, None)
            .expect("cannot create verification constraint");
        assert!(
            vc.verify(&layer_without_bundle)
                .expect("error while verifying")
        );
    }

    // integrated_time 1668077126 == 2022-11-10; the cert validity is
    // overridden to now ± 60s, so the timestamp falls outside the window.
    #[rstest]
    #[case::rekor_bundle(BundleContent::RekorBundle(build_rekor_bundle()))]
    #[case::sigstore_bundle(build_sigstore_bundle_content(1668077126))]
    fn reject_when_integrated_time_outside_cert_validity(#[case] bundle: BundleContent) {
        let (mut signature_layer, cert_pem_raw) = test_data();
        signature_layer.bundle = Some(bundle);

        let mut vc = CertificateVerifier::from_pem(cert_pem_raw.as_bytes(), true, None)
            .expect("cannot create verification constraint");

        // Shrink cert validity to now ± 60s so the 2022 integrated_time is outside it
        let not_before = UtcTime::from_system_time(
            SystemTime::now()
                .checked_sub(Duration::from_secs(60))
                .expect("cannot sub 60s from now"),
        )
        .expect("cannot create not_before");
        let not_after = UtcTime::from_system_time(
            SystemTime::now()
                .checked_add(Duration::from_secs(60))
                .expect("cannot add 60s to now"),
        )
        .expect("cannot create not_after");
        vc.cert_validity = Validity {
            not_before: Time::UtcTime(not_before),
            not_after: Time::UtcTime(not_after),
        };

        assert!(!vc.verify(&signature_layer).expect("error while verifying"));
    }
}