From ad950fc3feb89f93aa60958467b925e70a4248c8 Mon Sep 17 00:00:00 2001 From: Dave Tucker Date: Mon, 18 Sep 2023 14:10:19 +0100 Subject: [PATCH] cosign: Allow use of regex in CertSubjectEmailVerifier This allows for either an exact match [StringVerifier::ExactMatch] or it allows for a regular expression [StringVerifier::Regex] This supports the use case of trusting signatures from a collection of email addresses e.g .*@redhat.com and or from a collection of issuers. Fixes: #299 Signed-off-by: Dave Tucker --- examples/cosign/verify/main.rs | 8 +- src/cosign/mod.rs | 19 +- .../cert_subject_email_verifier.rs | 214 ++++++++++++++++-- .../cert_subject_url_verifier.rs | 16 +- 4 files changed, 218 insertions(+), 39 deletions(-) diff --git a/examples/cosign/verify/main.rs b/examples/cosign/verify/main.rs index 91112ec694..1c5b32217d 100644 --- a/examples/cosign/verify/main.rs +++ b/examples/cosign/verify/main.rs @@ -14,6 +14,7 @@ // limitations under the License. extern crate sigstore; +use sigstore::cosign::verification_constraint::cert_subject_email_verifier::StringVerifier; use sigstore::cosign::verification_constraint::{ AnnotationVerifier, CertSubjectEmailVerifier, CertSubjectUrlVerifier, CertificateVerifier, PublicKeyVerifier, VerificationConstraintVec, @@ -146,10 +147,13 @@ async fn run_app( // Build verification constraints let mut verification_constraints: VerificationConstraintVec = Vec::new(); if let Some(cert_email) = cli.cert_email.as_ref() { - let issuer = cli.cert_issuer.as_ref().map(|i| i.to_string()); + let issuer = cli + .cert_issuer + .as_ref() + .map(|i| StringVerifier::ExactMatch(i.to_string())); verification_constraints.push(Box::new(CertSubjectEmailVerifier { - email: cert_email.to_string(), + email: StringVerifier::ExactMatch(cert_email.to_string()), issuer, })); } diff --git a/src/cosign/mod.rs b/src/cosign/mod.rs index 379d0de366..b364bf943b 100644 --- a/src/cosign/mod.rs +++ b/src/cosign/mod.rs @@ -286,6 +286,7 @@ mod tests { use webpki::types::CertificateDer; use super::constraint::{AnnotationMarker, PrivateKeySigner}; + use super::verification_constraint::cert_subject_email_verifier::StringVerifier; use super::*; use crate::cosign::signature_layers::tests::build_correct_signature_layer_with_certificate; use crate::cosign::signature_layers::CertificateSubject; @@ -389,13 +390,13 @@ TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ let mut constraints: VerificationConstraintVec = Vec::new(); let vc = CertSubjectEmailVerifier { - email: email.clone(), - issuer: Some(issuer), + email: StringVerifier::ExactMatch(email.clone()), + issuer: Some(StringVerifier::ExactMatch(issuer)), }; constraints.push(Box::new(vc)); let vc = CertSubjectEmailVerifier { - email, + email: StringVerifier::ExactMatch(email), issuer: None, }; constraints.push(Box::new(vc)); @@ -438,13 +439,13 @@ TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ let mut constraints: VerificationConstraintVec = Vec::new(); let vc = CertSubjectEmailVerifier { - email: wrong_email.clone(), - issuer: Some(issuer), // correct issuer + email: StringVerifier::ExactMatch(wrong_email.clone()), + issuer: Some(StringVerifier::ExactMatch(issuer)), // correct issuer }; constraints.push(Box::new(vc)); let vc = CertSubjectEmailVerifier { - email: wrong_email, + email: StringVerifier::ExactMatch(wrong_email), issuer: None, // missing issuer, more relaxed }; constraints.push(Box::new(vc)); @@ -486,13 +487,13 @@ TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ let mut constraints: VerificationConstraintVec = Vec::new(); let satisfied_constraint = CertSubjectEmailVerifier { - email, - issuer: Some(issuer), + email: StringVerifier::ExactMatch(email), + issuer: Some(StringVerifier::ExactMatch(issuer)), }; constraints.push(Box::new(satisfied_constraint)); let unsatisfied_constraint = CertSubjectEmailVerifier { - email: email_incorrect, + email: StringVerifier::ExactMatch(email_incorrect), issuer: None, }; constraints.push(Box::new(unsatisfied_constraint)); diff --git a/src/cosign/verification_constraint/cert_subject_email_verifier.rs b/src/cosign/verification_constraint/cert_subject_email_verifier.rs index e1fe799e8a..913a1fafe1 100644 --- a/src/cosign/verification_constraint/cert_subject_email_verifier.rs +++ b/src/cosign/verification_constraint/cert_subject_email_verifier.rs @@ -1,3 +1,6 @@ +use regex::Regex; +use std::fmt::Debug; + use super::VerificationConstraint; use crate::cosign::signature_layers::{CertificateSubject, SignatureLayer}; use crate::errors::Result; @@ -33,19 +36,35 @@ use crate::errors::Result; /// found: /// /// ```rust +/// use regex::Regex; /// use sigstore::cosign::verification_constraint::CertSubjectEmailVerifier; +/// use sigstore::cosign::verification_constraint::cert_subject_email_verifier::StringVerifier; /// /// // This looks only for the email address of the trusted user /// let vc_email = CertSubjectEmailVerifier{ -/// email: String::from("alice@example.com"), -/// ..Default::default() +/// email: StringVerifier::ExactMatch("alice@example.com".to_string()), +/// issuer: None, +/// }; +/// +/// // This looks only for emails matching the a pattern +/// let vc_email_regex = CertSubjectEmailVerifier{ +/// email: StringVerifier::Regex(Regex::new(".*@example.com").unwrap()), +/// issuer: None, /// }; /// /// // 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")), +/// email: StringVerifier::ExactMatch("alice@example.com".to_string()), +/// issuer: Some(StringVerifier::ExactMatch("https://github.com/login/oauth".to_string())), +/// }; +/// +/// // This ensures the user authenticated via a service that has a domain +/// // matching the regex, plus the email associated to account also matches +/// // the regex. +/// let vc_email_and_issuer_regex = CertSubjectEmailVerifier{ +/// email: StringVerifier::Regex(Regex::new(".*@example.com").unwrap()), +/// issuer: Some(StringVerifier::Regex(Regex::new(r"https://github\.com/login/oauth|https://google\.com").unwrap())), /// }; /// ``` /// @@ -55,10 +74,11 @@ use crate::errors::Result; /// For example, given the following constraint: /// ```rust /// use sigstore::cosign::verification_constraint::CertSubjectEmailVerifier; +/// use sigstore::cosign::verification_constraint::cert_subject_email_verifier::StringVerifier; /// /// let constraint = CertSubjectEmailVerifier{ -/// email: String::from("alice@example.com"), -/// ..Default::default() +/// email: StringVerifier::ExactMatch("alice@example.com".to_string()), +/// issuer: None, /// }; /// ``` /// @@ -91,10 +111,48 @@ use crate::errors::Result; /// } /// ] /// ``` -#[derive(Default, Debug)] pub struct CertSubjectEmailVerifier { - pub email: String, - pub issuer: Option, + pub email: StringVerifier, + pub issuer: Option, +} + +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 { @@ -102,12 +160,20 @@ impl VerificationConstraint for CertSubjectEmailVerifier { let verified = match &signature_layer.certificate_signature { Some(signature) => { let email_matches = match &signature.subject { - CertificateSubject::Email(e) => e == &self.email, + CertificateSubject::Email(e) => self.email.verify(e), _ => false, }; - let issuer_matches = match self.issuer { - Some(_) => self.issuer == signature.issuer, + let issuer_matches = match &self.issuer { + Some(issuer) => { + if let Some(signature_issuer) = &signature.issuer { + issuer.verify(signature_issuer) + } else { + // if the issuer is not present in the signature, we + // consider it as a failed constriant + false + } + } None => true, }; @@ -133,19 +199,19 @@ mod tests { 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()); + 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, + email: StringVerifier::ExactMatch(email), issuer: None, }; assert!(vc.verify(&sl).unwrap()); let vc = CertSubjectEmailVerifier { - email: "different@email.com".to_string(), + email: StringVerifier::ExactMatch("different@email.com".to_string()), issuer: None, }; assert!(!vc.verify(&sl).unwrap()); @@ -165,8 +231,8 @@ mod tests { // fail because the issuer we want doesn't exist let vc = CertSubjectEmailVerifier { - email: email.clone(), - issuer: Some("an issuer".to_string()), + email: StringVerifier::ExactMatch(email.clone()), + issuer: Some(StringVerifier::ExactMatch("an issuer".to_string())), }; assert!(!vc.verify(&sl).unwrap()); @@ -178,14 +244,14 @@ mod tests { sl.certificate_signature = Some(cert_signature); let vc = CertSubjectEmailVerifier { - email: email.clone(), - issuer: Some(issuer.clone()), + email: StringVerifier::ExactMatch(email.clone()), + issuer: Some(StringVerifier::ExactMatch(issuer.clone())), }; assert!(vc.verify(&sl).unwrap()); let vc = CertSubjectEmailVerifier { - email, - issuer: Some("another issuer".to_string()), + email: StringVerifier::ExactMatch(email), + issuer: Some(StringVerifier::ExactMatch("another issuer".to_string())), }; assert!(!vc.verify(&sl).unwrap()); @@ -202,9 +268,113 @@ mod tests { let (sl, _) = build_correct_signature_layer_without_bundle(); let vc = CertSubjectEmailVerifier { - email: "alice@example.com".to_string(), + 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() { + // The cerificate subject doesn't have an issuer + 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()); + + // fail because the issuer we want doesn't exist + 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()); + + // The cerificate subject has an issuer + 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); + + // pass because the issuer matches the regex + 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()); + + // The cerificate subject has an incorrect issuer + 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); + + // fail because the issuer doesn't matches the regex + 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()); + + // The cerificate subject has an invalid email + 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); + + // fail because the email doesn't matches the regex + 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()); + } } diff --git a/src/cosign/verification_constraint/cert_subject_url_verifier.rs b/src/cosign/verification_constraint/cert_subject_url_verifier.rs index d171980351..2ef729a945 100644 --- a/src/cosign/verification_constraint/cert_subject_url_verifier.rs +++ b/src/cosign/verification_constraint/cert_subject_url_verifier.rs @@ -70,11 +70,15 @@ impl VerificationConstraint for CertSubjectUrlVerifier { #[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::tests::{ + build_correct_signature_layer_with_certificate, + build_correct_signature_layer_without_bundle, + }, + verification_constraint::{ + cert_subject_email_verifier::StringVerifier, CertSubjectEmailVerifier, + }, }; - use crate::cosign::verification_constraint::CertSubjectEmailVerifier; #[test] fn cert_subject_url_verifier() { @@ -108,8 +112,8 @@ mod tests { // A Cert email verifier should also report a non match let vc = CertSubjectEmailVerifier { - email: "alice@example.com".to_string(), - issuer: Some(issuer), + email: StringVerifier::ExactMatch("alice@example.com".to_string()), + issuer: Some(StringVerifier::ExactMatch(issuer)), }; assert!(!vc.verify(&sl).unwrap()); }