use std::collections::BTreeMap;
use async_trait::async_trait;
use tracing::warn;
use crate::errors::{Result, SigstoreApplicationConstraintsError, SigstoreVerifyConstraintsError};
use crate::registry::{Auth, PushResponse};
use crate::crypto::{CosignVerificationKey, Signature};
use crate::errors::SigstoreError;
use pkcs8::der::Decode;
use x509_cert::Certificate;
pub mod bundle;
pub mod bundle_content;
pub(crate) mod constants;
pub(crate) mod intoto;
pub mod signature_layers;
pub use signature_layers::SignatureLayer;
pub mod client;
pub use self::client::Client;
pub mod client_builder;
pub use self::client_builder::ClientBuilder;
pub mod verification_constraint;
pub use self::constraint::{Constraint, SignConstraintRefVec};
use self::verification_constraint::{VerificationConstraint, VerificationConstraintRefVec};
pub mod payload;
use crate::registry::oci_reference::OciReference;
pub use payload::simple_signing;
pub mod constraint;
#[async_trait]
pub trait CosignCapabilities {
async fn triangulate(
&mut self,
image: &OciReference,
auth: &Auth,
) -> Result<(OciReference, String)>;
async fn trusted_signature_layers(
&mut self,
auth: &Auth,
source_image: &OciReference,
) -> Result<Vec<SignatureLayer>>;
async fn push_signature(
&mut self,
annotations: Option<BTreeMap<String, String>>,
auth: &Auth,
target_reference: &OciReference,
signature_layers: Vec<SignatureLayer>,
) -> Result<PushResponse>;
fn verify_blob(cert: &str, signature: &str, blob: &[u8]) -> Result<()> {
let pem = pem::parse(cert)?;
let cert = Certificate::from_der(pem.contents()).map_err(|e| {
SigstoreError::CertificateParsingError(format!("parse der into cert failed: {e}"))
})?;
let spki = cert.tbs_certificate.subject_public_key_info;
let ver_key = CosignVerificationKey::try_from(&spki)?;
let signature = Signature::Base64Encoded(signature.as_bytes());
ver_key.verify_signature(signature, blob)?;
Ok(())
}
fn verify_blob_with_public_key(public_key: &str, signature: &str, blob: &[u8]) -> Result<()> {
let ver_key = CosignVerificationKey::try_from_pem(public_key.as_bytes())?;
let signature = Signature::Base64Encoded(signature.as_bytes());
ver_key.verify_signature(signature, blob)?;
Ok(())
}
}
pub fn verify_constraints<'a, 'b, I>(
signature_layers: &'a [SignatureLayer],
constraints: I,
) -> std::result::Result<(), SigstoreVerifyConstraintsError<'b>>
where
I: Iterator<Item = &'b Box<dyn VerificationConstraint>>,
{
let unsatisfied_constraints: VerificationConstraintRefVec = constraints.filter(|c| {
let mut is_c_unsatisfied = true;
signature_layers.iter().any( | sl | {
match c.verify(sl) {
Ok(is_sl_verified) => {
is_c_unsatisfied = !is_sl_verified;
is_sl_verified }
Err(e) => {
warn!(error = ?e, constraint = ?c, "Skipping layer because constraint verification returned an error");
is_c_unsatisfied = true;
false }
}
});
is_c_unsatisfied }).collect();
if unsatisfied_constraints.is_empty() {
Ok(())
} else {
Err(SigstoreVerifyConstraintsError {
unsatisfied_constraints,
})
}
}
pub fn apply_constraints<'a, 'b, I>(
signature_layer: &'a mut SignatureLayer,
constraints: I,
) -> std::result::Result<(), SigstoreApplicationConstraintsError<'b>>
where
I: Iterator<Item = &'b Box<dyn Constraint>>,
{
let unapplied_constraints: SignConstraintRefVec = constraints
.filter(|c| match c.add_constraint(signature_layer) {
Ok(is_applied) => !is_applied,
Err(e) => {
warn!(error = ?e, constraint = ?c, "Applying constraint failed due to error");
true
}
})
.collect();
if unapplied_constraints.is_empty() {
Ok(())
} else {
Err(SigstoreApplicationConstraintsError {
unapplied_constraints,
})
}
}
#[cfg(test)]
mod tests {
use pki_types::CertificateDer;
use serde_json::json;
use super::constraint::{AnnotationMarker, PrivateKeySigner};
use super::verification_constraint::cert_subject_email_verifier::StringVerifier;
use super::*;
use crate::cosign::signature_layers::CertificateSubject;
use crate::cosign::signature_layers::tests::build_correct_signature_layer_with_certificate;
use crate::cosign::simple_signing::Optional;
use crate::cosign::verification_constraint::{
AnnotationVerifier, CertSubjectEmailVerifier, VerificationConstraintVec,
};
use crate::crypto::SigningScheme;
use crate::crypto::certificate_pool::CertificatePool;
#[cfg(feature = "test-registry")]
use testcontainers::{core::WaitFor, runners::AsyncRunner};
#[cfg(feature = "test-remote-registry")]
mod remote_registry {
use crate::cosign::verification_constraint::CertSubjectUrlVerifier;
use crate::cosign::verification_constraint::VerificationConstraintVec;
use crate::cosign::{Client, ClientBuilder, CosignCapabilities, verify_constraints};
use crate::registry::Auth;
use crate::trust::sigstore::SigstoreTrustRoot;
const KUBEWARDEN_CONTROLLER_IMAGE: &str = "ghcr.io/kubewarden/kubewarden-controller";
const GITHUB_ACTIONS_OIDC_ISSUER: &str = "https://token.actions.githubusercontent.com";
async fn build_remote_cosign_client() -> Client {
let trust_root = SigstoreTrustRoot::new(None)
.await
.expect("failed to fetch Sigstore trust root");
ClientBuilder::default()
.with_trust_repository(&trust_root)
.expect("failed to configure trust repository")
.build()
.expect("failed to build cosign client")
}
#[tokio::test]
async fn verify_sigstore_bundle_only() {
let mut client = build_remote_cosign_client().await;
let image = format!("{KUBEWARDEN_CONTROLLER_IMAGE}:v1.30.0-rc1")
.parse()
.expect("failed to parse image reference");
let layers = client
.trusted_signature_layers(&Auth::Anonymous, &image)
.await
.expect("failed to get trusted signature layers");
assert_eq!(
layers.len(),
1,
"expected exactly 1 signature layer from Sigstore Bundle path"
);
assert!(
layers[0].certificate_signature.is_some(),
"expected certificate_signature to be present (Fulcio validation passed)"
);
let vc = CertSubjectUrlVerifier {
url: "https://github.com/kubewarden/kubewarden-controller/.github/workflows/release.yml@refs/tags/v1.30.0-rc1".to_string(),
issuer: GITHUB_ACTIONS_OIDC_ISSUER.to_string(),
};
let constraints: VerificationConstraintVec = vec![Box::new(vc)];
verify_constraints(&layers, constraints.iter())
.expect("verification constraints should be satisfied");
}
#[tokio::test]
async fn verify_simple_signing_only() {
let mut client = build_remote_cosign_client().await;
let image = format!("{KUBEWARDEN_CONTROLLER_IMAGE}:v1.29.0-rc1")
.parse()
.expect("failed to parse image reference");
let layers = client
.trusted_signature_layers(&Auth::Anonymous, &image)
.await
.expect("failed to get trusted signature layers");
assert_eq!(
layers.len(),
1,
"expected exactly 1 signature layer from SimpleSigning path"
);
assert!(
layers[0].certificate_signature.is_some(),
"expected certificate_signature to be present (Fulcio validation passed)"
);
let vc = CertSubjectUrlVerifier {
url: "https://github.com/kubewarden/kubewarden-controller/.github/workflows/release.yml@refs/tags/v1.29.0-rc1".to_string(),
issuer: GITHUB_ACTIONS_OIDC_ISSUER.to_string(),
};
let constraints: VerificationConstraintVec = vec![Box::new(vc)];
verify_constraints(&layers, constraints.iter())
.expect("verification constraints should be satisfied");
}
#[tokio::test]
async fn verify_both_simple_signing_and_sigstore_bundle() {
let mut client = build_remote_cosign_client().await;
let image = format!("{KUBEWARDEN_CONTROLLER_IMAGE}:v1.31.0-rc1")
.parse()
.expect("failed to parse image reference");
let layers = client
.trusted_signature_layers(&Auth::Anonymous, &image)
.await
.expect("failed to get trusted signature layers");
assert_eq!(
layers.len(),
2,
"expected exactly 2 signature layers (one from SimpleSigning, one from Sigstore Bundle)"
);
for layer in &layers {
assert!(
layer.certificate_signature.is_some(),
"expected certificate_signature to be present on all layers (Fulcio validation passed)"
);
}
let vc = CertSubjectUrlVerifier {
url: "https://github.com/kubewarden/kubewarden-controller/.github/workflows/release.yml@refs/tags/v1.31.0-rc1".to_string(),
issuer: GITHUB_ACTIONS_OIDC_ISSUER.to_string(),
};
let constraints: VerificationConstraintVec = vec![Box::new(vc)];
verify_constraints(&layers, constraints.iter())
.expect("verification constraints should be satisfied");
}
}
pub(crate) const REKOR_PUB_KEY: &str = r#"-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr
kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==
-----END PUBLIC KEY-----"#;
pub(crate) const REKOR_PUB_KEY_ID: &str =
"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d";
const FULCIO_CRT_1_PEM: &str = r#"-----BEGIN CERTIFICATE-----
MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAq
MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx
MDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUu
ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSy
A7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0Jcas
taRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6Nm
MGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE
FMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2u
Su1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJx
Ve/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uup
Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==
-----END CERTIFICATE-----"#;
const FULCIO_CRT_2_PEM: &str = r#"-----BEGIN CERTIFICATE-----
MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw
KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y
MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl
LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7
XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex
X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j
YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY
wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ
KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM
WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9
TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ
-----END CERTIFICATE-----"#;
#[cfg(feature = "test-registry")]
const SIGNED_IMAGE: &str = "busybox:1.34";
pub(crate) fn get_fulcio_cert_pool() -> CertificatePool {
fn pem_to_der(input: &str) -> CertificateDer<'_> {
let pem_cert = pem::parse(input).unwrap();
assert_eq!(pem_cert.tag(), "CERTIFICATE");
CertificateDer::from(pem_cert.into_contents())
}
let certificates = vec![pem_to_der(FULCIO_CRT_1_PEM), pem_to_der(FULCIO_CRT_2_PEM)];
CertificatePool::from_certificates(certificates, []).unwrap()
}
pub(crate) fn get_rekor_public_key() -> (String, CosignVerificationKey) {
let key =
CosignVerificationKey::from_pem(REKOR_PUB_KEY.as_bytes(), &SigningScheme::default())
.expect("Cannot create test REKOR_PUB_KEY");
(REKOR_PUB_KEY_ID.to_string(), key)
}
#[test]
fn verify_constraints_all_satisfied() {
let email = "alice@example.com".to_string();
let issuer = "an issuer".to_string();
let mut annotations: BTreeMap<String, String> = BTreeMap::new();
annotations.insert("key1".into(), "value1".into());
annotations.insert("key2".into(), "value2".into());
let mut layers: Vec<SignatureLayer> = Vec::new();
for _ in 0..5 {
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 = Some(issuer.clone());
cert_signature.subject = cert_subj;
sl.certificate_signature = Some(cert_signature);
let mut extra: BTreeMap<String, serde_json::Value> = annotations
.iter()
.map(|(k, v)| (k.clone(), json!(v)))
.collect();
extra.insert("something extra".into(), json!("value extra"));
let mut simple_signing = sl.simple_signing;
let optional = Optional {
creator: Some("test".into()),
timestamp: None,
extra,
};
simple_signing.optional = Some(optional);
sl.simple_signing = simple_signing;
layers.push(sl);
}
let mut constraints: VerificationConstraintVec = Vec::new();
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(email.clone()),
issuer: Some(StringVerifier::ExactMatch(issuer)),
};
constraints.push(Box::new(vc));
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(email),
issuer: None,
};
constraints.push(Box::new(vc));
let vc = AnnotationVerifier { annotations };
constraints.push(Box::new(vc));
verify_constraints(&layers, constraints.iter()).expect("should not return an error");
}
#[test]
fn verify_constraints_none_satisfied() {
let email = "alice@example.com".to_string();
let issuer = "an issuer".to_string();
let wrong_email = "bob@example.com".to_string();
let mut layers: Vec<SignatureLayer> = Vec::new();
for _ in 0..5 {
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 = Some(issuer.clone());
cert_signature.subject = cert_subj;
sl.certificate_signature = Some(cert_signature);
let mut extra: BTreeMap<String, serde_json::Value> = BTreeMap::new();
extra.insert("something extra".into(), json!("value extra"));
let mut simple_signing = sl.simple_signing;
let optional = Optional {
creator: Some("test".into()),
timestamp: None,
extra,
};
simple_signing.optional = Some(optional);
sl.simple_signing = simple_signing;
layers.push(sl);
}
let mut constraints: VerificationConstraintVec = Vec::new();
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(wrong_email.clone()),
issuer: Some(StringVerifier::ExactMatch(issuer)), };
constraints.push(Box::new(vc));
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(wrong_email),
issuer: None, };
constraints.push(Box::new(vc));
let err =
verify_constraints(&layers, constraints.iter()).expect_err("we should have an err");
assert_eq!(err.unsatisfied_constraints.len(), 2);
}
#[test]
fn verify_constraints_some_unsatisfied() {
let email = "alice@example.com".to_string();
let issuer = "an issuer".to_string();
let email_incorrect = "bob@example.com".to_string();
let mut layers: Vec<SignatureLayer> = Vec::new();
for _ in 0..5 {
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 = Some(issuer.clone());
cert_signature.subject = cert_subj;
sl.certificate_signature = Some(cert_signature);
let mut extra: BTreeMap<String, serde_json::Value> = BTreeMap::new();
extra.insert("something extra".into(), json!("value extra"));
let mut simple_signing = sl.simple_signing;
let optional = Optional {
creator: Some("test".into()),
timestamp: None,
extra,
};
simple_signing.optional = Some(optional);
sl.simple_signing = simple_signing;
layers.push(sl);
}
let mut constraints: VerificationConstraintVec = Vec::new();
let satisfied_constraint = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(email),
issuer: Some(StringVerifier::ExactMatch(issuer)),
};
constraints.push(Box::new(satisfied_constraint));
let unsatisfied_constraint = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(email_incorrect),
issuer: None,
};
constraints.push(Box::new(unsatisfied_constraint));
let err =
verify_constraints(&layers, constraints.iter()).expect_err("we should have an err");
assert_eq!(err.unsatisfied_constraints.len(), 1);
}
#[test]
fn add_constrains_all_succeed() {
let mut signature_layer = SignatureLayer::new_unsigned(
&"test_image".parse().unwrap(),
"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
)
.expect("create SignatureLayer failed");
let signer = SigningScheme::ECDSA_P256_SHA256_ASN1
.create_signer()
.expect("create signer failed");
let signer = PrivateKeySigner::new_with_signer(signer);
let annotations = [(String::from("key"), String::from("value"))].into();
let annotations = AnnotationMarker::new(annotations);
let constrains: Vec<Box<dyn Constraint>> = vec![Box::new(signer), Box::new(annotations)];
apply_constraints(&mut signature_layer, constrains.iter()).expect("no error should occur");
}
#[test]
fn add_constrain_some_failed() {
let mut signature_layer = SignatureLayer::new_unsigned(
&"test_image".parse().unwrap(),
"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
)
.expect("create SignatureLayer failed");
let signer = SigningScheme::ECDSA_P256_SHA256_ASN1
.create_signer()
.expect("create signer failed");
let signer = PrivateKeySigner::new_with_signer(signer);
let another_signer_of_same_layer = SigningScheme::ECDSA_P256_SHA256_ASN1
.create_signer()
.expect("create signer failed");
let another_signer_of_same_layer =
PrivateKeySigner::new_with_signer(another_signer_of_same_layer);
let annotations = [(String::from("key"), String::from("value"))].into();
let annotations = AnnotationMarker::new(annotations);
let constrains: Vec<Box<dyn Constraint>> = vec![
Box::new(signer),
Box::new(annotations),
Box::new(another_signer_of_same_layer),
];
apply_constraints(&mut signature_layer, constrains.iter())
.expect_err("no error should occur");
}
#[cfg(feature = "test-registry")]
#[rstest::rstest]
#[case(SigningScheme::RSA_PSS_SHA256(2048))]
#[case(SigningScheme::RSA_PKCS1_SHA256(2048))]
#[case(SigningScheme::ECDSA_P256_SHA256_ASN1)]
#[case(SigningScheme::ECDSA_P384_SHA384_ASN1)]
#[case(SigningScheme::ED25519)]
#[tokio::test]
#[serial_test::serial]
async fn sign_verify_image(#[case] signing_scheme: SigningScheme) {
let test_container = registry_image()
.start()
.await
.expect("failed to start registry");
let port = test_container
.get_host_port_ipv4(5000)
.await
.expect("failed to get port");
let mut client = ClientBuilder::default()
.enable_registry_caching()
.with_oci_client_config(crate::registry::ClientConfig {
protocol: crate::registry::ClientProtocol::HttpsExcept(vec![format!(
"localhost:{}",
port
)]),
..Default::default()
})
.build()
.expect("failed to create oci client");
let image_ref = format!("localhost:{}/{}", port, SIGNED_IMAGE)
.parse::<OciReference>()
.expect("failed to parse reference");
prepare_image_to_be_signed(&mut client, &image_ref).await;
let (cosign_signature_image, source_image_digest) = client
.triangulate(&image_ref, &crate::registry::Auth::Anonymous)
.await
.expect("get manifest failed");
let mut signature_layer = SignatureLayer::new_unsigned(&image_ref, &source_image_digest)
.expect("create SignatureLayer failed");
let signer = signing_scheme
.create_signer()
.expect("create signer failed");
let pubkey = signer
.to_sigstore_keypair()
.expect("to keypair failed")
.public_key_to_pem()
.expect("derive public key failed");
let signer = PrivateKeySigner::new_with_signer(signer);
if !signer
.add_constraint(&mut signature_layer)
.expect("sign SignatureLayer failed")
{
panic!("failed to sign SignatureLayer");
};
client
.push_signature(
None,
&Auth::Anonymous,
&cosign_signature_image,
vec![signature_layer],
)
.await
.expect("push signature failed");
dbg!("start to verify");
let signature_layers = client
.trusted_signature_layers(&Auth::Anonymous, &image_ref)
.await
.expect("get trusted signature layers failed");
let pk_verifier =
verification_constraint::PublicKeyVerifier::new(pubkey.as_bytes(), &signing_scheme)
.expect("create PublicKeyVerifier failed");
assert_eq!(signature_layers.len(), 1);
let res = pk_verifier
.verify(&signature_layers[0])
.expect("failed to verify");
assert!(res);
}
#[cfg(feature = "test-registry")]
async fn prepare_image_to_be_signed(client: &mut Client, image_ref: &OciReference) {
let data = client
.registry_client
.pull(
&SIGNED_IMAGE.parse().expect("failed to parse image ref"),
&oci_client::secrets::RegistryAuth::Anonymous,
vec![oci_client::manifest::IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE],
)
.await
.expect("pull test image failed");
client
.registry_client
.push(
&image_ref.oci_reference,
&data.layers[..],
data.config.clone(),
&oci_client::secrets::RegistryAuth::Anonymous,
None,
)
.await
.expect("push test image failed");
}
#[cfg(feature = "test-registry")]
fn registry_image() -> testcontainers::GenericImage {
testcontainers::GenericImage::new("docker.io/library/registry", "2")
.with_wait_for(WaitFor::message_on_stderr("listening on "))
}
}