Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cert verification, support multiple certs #50

Merged
merged 3 commits into from
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,30 @@ Signature types:
env: prod
```

5. Certificate. It will verify that the image has been signed using the Certificate provided by the user.
The `certificate` must be PEM encoded. Optionally the settings can have
5. Certificate. It will verify that the image has been signed using all the
certificates provided by the user.
The certificates must be PEM encoded. Optionally the settings can have
the list of PEM encoded certificates that can create the `certificateChain`
used to verify the given `certificate`.
The `requireRekorBundle` should be set to `true` to have a stronger
verification process. When set to `true`, the signature must have a Rekor
bundle and the signature must have been created during the validity
time frame of the `certificate`.

The following configuration requires all the container images coming from
`registry.acme.org/secure-project` to be signed both by Alice and by Bob.

```yaml
signatures:
- image: "registry-testing.svc.lan/kubewarden/pod-privileged:v0.1.9"
certificate: |
- image: "registry.acme.org/secure-project/*"
certificates:
- |
-----BEGIN CERTIFICATE-----
alice's cert
-----END CERTIFICATE-----
- |
-----BEGIN CERTIFICATE-----
XXX
bob's cert
-----END CERTIFICATE-----
certificateChain:
- |
Expand All @@ -118,8 +127,11 @@ Signature types:
<root CA>
-----END CERTIFICATE-----
requireRekorBundle: true
annotations: #optional
env: prod
```


## License

```
Expand Down
21 changes: 16 additions & 5 deletions metadata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,30 @@ annotations:
env: prod
```

5. Certificate. It will verify that the image has been signed using the Certificate provided by the user.
The `certificate` must be PEM encoded. Optionally the settings can have
5. Certificate. It will verify that the image has been signed using all the
certificates provided by the user.
The certificates must be PEM encoded. Optionally the settings can have
the list of PEM encoded certificates that can create the `certificateChain`
used to verify the given `certificate`.
The `requireRekorBundle` should be set to `true` to have a stronger
verification process. When set to `true`, the signature must have a Rekor
bundle and the signature must have been created during the validity
time frame of the `certificate`.

The following configuration requires all the container images coming from
`registry.acme.org/secure-project` to be signed both by Alice and by Bob.

```yaml
signatures:
- image: "registry-testing.svc.lan/kubewarden/pod-privileged:v0.1.9"
certificate: |
- image: "registry.acme.org/secure-project/*"
certificates:
- |
-----BEGIN CERTIFICATE-----
XXX
alice's cert
-----END CERTIFICATE-----
- |
-----BEGIN CERTIFICATE-----
bob's cert
-----END CERTIFICATE-----
certificateChain:
- |
Expand All @@ -130,4 +139,6 @@ annotations:
<root CA>
-----END CERTIFICATE-----
requireRekorBundle: true
annotations: #optional
env: prod
```
96 changes: 80 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,18 +383,30 @@ where
Signature::Certificate(s) => {
// verify if the name matches the image name provided
if WildMatch::new(s.image.as_str()).matches(container_image.as_str()) {
handle_verification_response(
verify_certificate(
let mut response: Result<VerificationResponse> =
Err(anyhow::anyhow!("Cannot verify"));

for certificate in &s.certificates {
response = verify_certificate(
container_image.as_str(),
s.certificate.clone(),
certificate.clone(),
s.certificate_chain.clone(),
s.require_rekor_bundle,
s.annotations.clone(),
),
);
// All the certificates must be verified. As soon as one of
// them cannot be used to verify the image -> break from the
// loop and propagate the verification failure
if response.is_err() {
break;
}
}
handle_verification_response(
response,
container_image.as_str(),
&mut container_with_images_digests[i],
policy_verification_errors,
);
)
}
}
}
Expand Down Expand Up @@ -733,18 +745,61 @@ mod tests {
#[serial]
fn certificate_validation_pass_with_no_mutation() {
let ctx = mock_sdk::verify_certificate_context();
ctx.expect().times(1).returning(|_, _, _, _, _| {
Ok(VerificationResponse {
is_trusted: true,
digest: "sha256:89102e348749bb17a6a651a4b2a17420e1a66d2a44a675b981973d49a5af3a5e"
.to_string(),
})
});
ctx.expect()
.times(1)
.returning(|_, certificate, _, _, _| match certificate.as_str() {
"good-cert" => Ok(VerificationResponse {
is_trusted: true,
digest:
"sha256:89102e348749bb17a6a651a4b2a17420e1a66d2a44a675b981973d49a5af3a5e"
.to_string(),
}),
_ => Err(anyhow!("not good-cert")),
});

let settings: Settings = Settings {
signatures: vec![Signature::Certificate(Certificate {
image: "ghcr.io/kubewarden/test-verify-image-signatures:*".to_string(),
certificates: vec!["good-cert".to_string()],
certificate_chain: None,
require_rekor_bundle: true,
annotations: None,
})],
modify_images_with_digest: false,
};

let tc = Testcase {
name: String::from("It should successfully validate the ghcr.io/kubewarden/test-verify-image-signatures container"),
fixture_file: String::from("test_data/pod_creation_signed.json"),
settings,
expected_validation_result: true,
};

let response = tc.eval(validate).unwrap();
assert_eq!(response.accepted, true);
assert!(response.mutated_object.is_none());
}

#[test]
#[serial]
fn certificate_validation_pass_with_multiple_good_keys() {
let ctx = mock_sdk::verify_certificate_context();
ctx.expect()
.times(2)
.returning(|_, certificate, _, _, _| match certificate.as_str() {
"good-cert1" | "good-cert2" => Ok(VerificationResponse {
is_trusted: true,
digest:
"sha256:89102e348749bb17a6a651a4b2a17420e1a66d2a44a675b981973d49a5af3a5e"
.to_string(),
}),
_ => Err(anyhow!("not good-cert")),
});

let settings: Settings = Settings {
signatures: vec![Signature::Certificate(Certificate {
image: "ghcr.io/kubewarden/test-verify-image-signatures:*".to_string(),
certificate: "cert".to_string(),
certificates: vec!["good-cert1".to_string(), "good-cert2".to_string()],
certificate_chain: None,
require_rekor_bundle: true,
annotations: None,
Expand All @@ -769,13 +824,22 @@ mod tests {
fn certificate_validation_dont_pass() {
let ctx = mock_sdk::verify_certificate_context();
ctx.expect()
.times(1)
.returning(|_, _, _, _, _| Err(anyhow!("error")));
.times(2)
.returning(|_, certificate, _, _, _| match certificate.as_str() {
"good-cert" => Ok(VerificationResponse {
is_trusted: true,
digest:
"sha256:89102e348749bb17a6a651a4b2a17420e1a66d2a44a675b981973d49a5af3a5e"
.to_string(),
}),
_ => Err(anyhow!("not good-cert")),
});

// validation with 2 certs, one of the is good the other isn't
let settings: Settings = Settings {
signatures: vec![Signature::Certificate(Certificate {
image: "ghcr.io/kubewarden/test-verify-image-signatures:*".to_string(),
certificate: "cert".to_string(),
certificates: vec!["good-cert".to_string(), "bad-cert".to_string()],
certificate_chain: None,
require_rekor_bundle: true,
annotations: None,
Expand Down
2 changes: 1 addition & 1 deletion src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub(crate) struct Certificate {
/// String pointing to the object (e.g.: `registry.testing.lan/busybox:1.0.0`)
pub(crate) image: String,
/// PEM encoded certificate used to verify the signature
pub(crate) certificate: String,
pub(crate) certificates: Vec<String>,
/// Optional - the certificate chain that is used to verify the provided
/// certificate. When not specified, the certificate is assumed to be trusted
pub(crate) certificate_chain: Option<Vec<String>>,
Expand Down
3 changes: 2 additions & 1 deletion test_data/settings-pod_signed_with_cert_and_rekor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# behavior
signatures:
- image: "ghcr.io/kubewarden/tests/pod-privileged:v0.2.1"
certificate: |
certificates:
- |
-----BEGIN CERTIFICATE-----
MIICsjCCAligAwIBAgIUZfoeZRPm91CZevZ1fPbn4+KRd0owCgYIKoZIzj0EAwIw
gZIxCzAJBgNVBAYTAkRFMRAwDgYDVQQIEwdCYXZhcmlhMRIwEAYDVQQHEwlOdXJl
Expand Down