Skip to content

Commit

Permalink
cosign: Allow use of regex in CertSubjectEmailVerifier
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
dave-tucker committed Sep 20, 2023
1 parent 7960dff commit c67f5cb
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 35 deletions.
8 changes: 6 additions & 2 deletions examples/cosign/verify/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -156,10 +157,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,
}));
}
Expand Down
19 changes: 10 additions & 9 deletions src/cosign/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ mod tests {
use std::collections::HashMap;

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;
Expand Down Expand Up @@ -396,13 +397,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));
Expand Down Expand Up @@ -445,13 +446,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));
Expand Down Expand Up @@ -493,13 +494,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));
Expand Down
214 changes: 192 additions & 22 deletions src/cosign/verification_constraint/cert_subject_email_verifier.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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("[email protected]"),
/// ..Default::default()
/// email: StringVerifier::ExactMatch("[email protected]".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("[email protected]"),
/// issuer: Some(String::from("https://github.com/login/oauth")),
/// email: StringVerifier::ExactMatch("[email protected]".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())),
/// };
/// ```
///
Expand All @@ -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("[email protected]"),
/// ..Default::default()
/// email: StringVerifier::ExactMatch("[email protected]".to_string()),
/// issuer: None,
/// };
/// ```
///
Expand Down Expand Up @@ -91,23 +111,69 @@ use crate::errors::Result;
/// }
/// ]
/// ```
#[derive(Default, Debug)]
pub struct CertSubjectEmailVerifier {
pub email: String,
pub issuer: Option<String>,
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) => 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,
};

Expand All @@ -134,19 +200,19 @@ mod tests {
let email = "[email protected]".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: "[email protected]".to_string(),
email: StringVerifier::ExactMatch("[email protected]".to_string()),
issuer: None,
};
assert!(!vc.verify(&sl).unwrap());
Expand All @@ -166,8 +232,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());

Expand All @@ -179,14 +245,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());

Expand All @@ -203,9 +269,113 @@ mod tests {
let (sl, _) = build_correct_signature_layer_without_bundle();

let vc = CertSubjectEmailVerifier {
email: "[email protected]".to_string(),
email: StringVerifier::ExactMatch("[email protected]".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("[email protected]".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("[email protected]".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("[email protected]".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("[email protected]".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("[email protected]".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("[email protected]".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("[email protected]".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());
}
}
Loading

0 comments on commit c67f5cb

Please sign in to comment.