use regex::Regex;
use std::fmt::Debug;
use super::VerificationConstraint;
use crate::cosign::signature_layers::{CertificateSubject, SignatureLayer};
use crate::errors::Result;
pub struct CertSubjectEmailVerifier {
pub email: StringVerifier,
pub issuer: Option<StringVerifier>,
}
impl Debug for CertSubjectEmailVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut issuer_str = String::new();
if let Some(issuer) = &self.issuer {
issuer_str.push_str(&format!(" and {issuer}"));
}
f.write_fmt(format_args!(
"email {}{}",
&self.email.to_string(),
issuer_str
))
}
}
pub enum StringVerifier {
ExactMatch(String),
Regex(Regex),
}
impl StringVerifier {
fn verify(&self, s: &str) -> bool {
match self {
StringVerifier::ExactMatch(s2) => s == *s2,
StringVerifier::Regex(r) => r.is_match(s),
}
}
}
impl std::fmt::Display for StringVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StringVerifier::ExactMatch(s) => f.write_fmt(format_args!("is exactly {s}")),
StringVerifier::Regex(r) => f.write_fmt(format_args!("matches regular expression {r}")),
}
}
}
impl VerificationConstraint for CertSubjectEmailVerifier {
fn verify(&self, signature_layer: &SignatureLayer) -> Result<bool> {
let verified = match &signature_layer.certificate_signature {
Some(signature) => {
let email_matches = match &signature.subject {
CertificateSubject::Email(e) => self.email.verify(e),
_ => false,
};
let issuer_matches = match &self.issuer {
Some(issuer) => {
if let Some(signature_issuer) = &signature.issuer {
issuer.verify(signature_issuer)
} else {
false
}
}
None => true,
};
email_matches && issuer_matches
}
_ => false,
};
Ok(verified)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
cosign::{
bundle::{Bundle, Payload},
signature_layers::{
CertificateSignature, SignatureLayer,
tests::{
build_correct_signature_layer_with_certificate,
build_correct_signature_layer_without_bundle,
},
},
verification_constraint::CertSubjectUrlVerifier,
},
crypto::{
certificate_pool::CertificatePool,
tests::{CertGenerationOptions, generate_certificate},
},
registry,
};
use chrono::{TimeDelta, Utc};
use serde_json::json;
#[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.to_string());
cert_signature.issuer = None;
cert_signature.subject = cert_subj;
sl.certificate_signature = Some(cert_signature);
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(email),
issuer: None,
};
assert!(vc.verify(&sl).unwrap());
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch("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();
let cert_subj = CertificateSubject::Email(email.clone());
cert_signature.issuer = None;
cert_signature.subject = cert_subj;
sl.certificate_signature = Some(cert_signature.clone());
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(email.clone()),
issuer: Some(StringVerifier::ExactMatch("an issuer".to_string())),
};
assert!(!vc.verify(&sl).unwrap());
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: StringVerifier::ExactMatch(email.clone()),
issuer: Some(StringVerifier::ExactMatch(issuer.clone())),
};
assert!(vc.verify(&sl).unwrap());
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(email),
issuer: Some(StringVerifier::ExactMatch("another issuer".to_string())),
};
assert!(!vc.verify(&sl).unwrap());
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: StringVerifier::ExactMatch("alice@example.com".to_string()),
issuer: None,
};
assert!(!vc.verify(&sl).unwrap());
}
#[test]
fn cert_email_verifier_only_email_regex() {
let mut sl = build_correct_signature_layer_with_certificate();
let mut cert_signature = sl.certificate_signature.unwrap();
let cert_subj = CertificateSubject::Email("alice@example.com".to_string());
cert_signature.issuer = None;
cert_signature.subject = cert_subj;
sl.certificate_signature = Some(cert_signature);
let vc = CertSubjectEmailVerifier {
email: StringVerifier::Regex(Regex::new(".*@example.com").unwrap()),
issuer: None,
};
assert!(vc.verify(&sl).unwrap());
let mut sl = build_correct_signature_layer_with_certificate();
let mut cert_signature = sl.certificate_signature.unwrap();
let cert_subj = CertificateSubject::Email("bob@example.com".to_string());
cert_signature.issuer = None;
cert_signature.subject = cert_subj;
sl.certificate_signature = Some(cert_signature);
assert!(vc.verify(&sl).unwrap());
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch("different@email.com".to_string()),
issuer: None,
};
assert!(!vc.verify(&sl).unwrap());
}
#[test]
fn cert_email_verifier_email_and_issuer_regex() {
let mut sl = build_correct_signature_layer_with_certificate();
let mut cert_signature = sl.certificate_signature.unwrap();
let cert_subj = CertificateSubject::Email("alice@example.com".to_string());
cert_signature.issuer = None;
cert_signature.subject = cert_subj;
sl.certificate_signature = Some(cert_signature.clone());
let vc = CertSubjectEmailVerifier {
email: StringVerifier::Regex(Regex::new(".*@example.com").unwrap()),
issuer: Some(StringVerifier::Regex(
Regex::new(r#".*\.github.com"#).unwrap(),
)),
};
assert!(!vc.verify(&sl).unwrap());
let mut sl = build_correct_signature_layer_with_certificate();
let mut cert_signature = sl.certificate_signature.unwrap();
let issuer = "some-action.github.com".to_string();
let cert_subj = CertificateSubject::Email("alice@example.com".to_string());
cert_signature.issuer = Some(issuer.clone());
cert_signature.subject = cert_subj;
sl.certificate_signature = Some(cert_signature);
let vc = CertSubjectEmailVerifier {
email: StringVerifier::Regex(Regex::new(".*@example.com").unwrap()),
issuer: Some(StringVerifier::Regex(
Regex::new(r#".*\.github.com"#).unwrap(),
)),
};
assert!(vc.verify(&sl).unwrap());
let mut sl = build_correct_signature_layer_with_certificate();
let mut cert_signature = sl.certificate_signature.unwrap();
let issuer = "invalid issuer".to_string();
let cert_subj = CertificateSubject::Email("alice@example.com".to_string());
cert_signature.issuer = Some(issuer.clone());
cert_signature.subject = cert_subj;
sl.certificate_signature = Some(cert_signature);
let vc = CertSubjectEmailVerifier {
email: StringVerifier::Regex(Regex::new(".*@example.com").unwrap()),
issuer: Some(StringVerifier::Regex(
Regex::new(r#".*\.github.com"#).unwrap(),
)),
};
assert!(!vc.verify(&sl).unwrap());
let mut sl = build_correct_signature_layer_with_certificate();
let mut cert_signature = sl.certificate_signature.unwrap();
let issuer = "some-action.github.com".to_string();
let cert_subj = CertificateSubject::Email("alice@somedomain.com".to_string());
cert_signature.issuer = Some(issuer.clone());
cert_signature.subject = cert_subj;
sl.certificate_signature = Some(cert_signature);
let vc = CertSubjectEmailVerifier {
email: StringVerifier::Regex(Regex::new(".*@example.com").unwrap()),
issuer: Some(StringVerifier::Regex(
Regex::new(r#".*\.github.com"#).unwrap(),
)),
};
assert!(!vc.verify(&sl).unwrap());
}
#[test]
fn cert_email_verifier_issuer_read_from_certificate_extension() -> anyhow::Result<()> {
let expected_email = "test@sigstore.dev".to_string();
let expected_issuer = "https://sigstore.dev/oauth".to_string();
let ca_data = generate_certificate(None, CertGenerationOptions::default())?;
let issued_cert = generate_certificate(
Some(&ca_data),
CertGenerationOptions {
subject_email: Some(expected_email.clone()),
subject_issuer: Some(expected_issuer.clone()),
..Default::default()
},
)?;
let cert_pool = CertificatePool::from_certificates(
vec![
registry::Certificate {
encoding: registry::CertificateEncoding::Pem,
data: ca_data.cert_pem.clone(),
}
.try_into()?,
],
[],
)
.unwrap();
let integrated_time = Utc::now()
.checked_sub_signed(TimeDelta::try_minutes(1).unwrap())
.unwrap();
let bundle = Bundle {
signed_entry_timestamp: "not relevant".to_string(),
payload: Payload {
body: "not relevant".to_string(),
integrated_time: integrated_time.timestamp(),
log_index: 0,
log_id: "not relevant".to_string(),
},
};
let certificate_signature = CertificateSignature::from_certificate(
&issued_cert.cert_pem,
&cert_pool,
bundle.payload.integrated_time,
)
.expect("Cannot create CertificateSignature");
let sl = SignatureLayer {
certificate_signature: Some(certificate_signature),
simple_signing: serde_json::from_value(json!({
"critical": {
"identity": { "docker-reference": "registry.example.com/test" },
"image": { "docker-manifest-digest": "sha256:aaaa" },
"type": "cosign container image signature"
},
"optional": null
}))
.unwrap(),
oci_digest: String::new(),
signature: None,
bundle: None,
raw_data: vec![],
};
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(expected_email.clone()),
issuer: Some(StringVerifier::ExactMatch(expected_issuer.clone())),
};
assert!(
vc.verify(&sl)?,
"expected verification to pass with correct email and issuer"
);
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(expected_email.clone()),
issuer: Some(StringVerifier::ExactMatch(
"https://wrong.issuer.example.com".to_string(),
)),
};
assert!(
!vc.verify(&sl)?,
"expected verification to fail with wrong issuer"
);
let vc = CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(expected_email.clone()),
issuer: None,
};
assert!(
vc.verify(&sl)?,
"expected verification to pass with no issuer constraint"
);
Ok(())
}
}