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: add cosign keyless support to trust policy #1503

Merged
merged 21 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ e2e-helm-deploy-ratify:
--set notationCerts[0]="$$(cat ~/.config/notation/localkeys/ratify-bats-test.crt)" \
--set cosignKeys[0]="$$(cat .staging/cosign/cosign.pub)" \
--set cosign.key="$$(cat .staging/cosign/cosign.pub)" \
--set cosign.tLogVerify=false \
susanshi marked this conversation as resolved.
Show resolved Hide resolved
--set oras.useHttp=true \
--set-file dockerConfig="mount_config.json" \
--set logger.level=debug
Expand All @@ -606,6 +607,7 @@ e2e-helm-deploy-ratify-without-tls-certs:
--set notaryCert="$$(cat ~/.config/notation/localkeys/ratify-bats-test.crt)" \
--set cosign.key="$$(cat .staging/cosign/cosign.pub)" \
--set cosignKeys[0]="$$(cat .staging/cosign/cosign.pub)" \
--set cosign.tLogVerify=false \
--set oras.useHttp=true \
--set-file dockerConfig="mount_config.json" \
--set logger.level=debug
Expand Down
7 changes: 7 additions & 0 deletions charts/ratify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ Values marked `# DEPRECATED` in the `values.yaml` as well as **DEPRECATED** in t
| cosignKeys | An array of public keys used to create inline key management providers used by Cosign verifier | `[]` |
| cosign.enabled | Enables/disables cosign tag-based signature lookup in ORAS store. MUST be set to true for cosign verification. | `true` |
| cosign.scopes | An array of scopes relevant to the single trust policy configured in Cosign verifier. A scope of '*' is a global wildcard character to represent all images apply. | `["*"]` |
| cosign.rekorURL | URL string reference to remote rekor server. If not specified, implementation will default to use Rekor public good instance `https://rekor.sigstore.dev`. | `` |
| cosign.tLogVerify | Enables/disables verification of presence of signature in Transparency log. | `true` |
| cosign.keyless.ctLogVerify | Enables/disables verification of presence of Secure Certificate Timestamp (SCT) in transparency log | `true` |
| cosign.keyless.certificateIdentity | String certificate identity used for exact identity match during verification. Either `certificateIdentity` or `certificateIdentityRegExp` MUST be defined, but both cannot be defined at together | `` |
| cosign.keyless.certificateIdentityRegExp | String certificate identity regular expression for identity matching during verification. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either `certificateIdentity` or `certificateIdentityRegExp` MUST be defined, but both cannot be defined together | `` |
| cosign.keyless.certificateOIDCIssuer | String certificate OIDC issuer for exact issuer matching during verification. Either `certificateOIDCIssuer` or `certificateOIDCIssuerRegExp` MUST be defined, but both cannot be defined together | `` |
| cosign.keyless.certificateOIDCIssuerRegExp | String certificate OIDC issuer regular expression for issuer matching during verification. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either `certificateOIDCIssuer` or `certificateOIDCIssuerRegExp` MUST be defined, but both cannot be defined together | `` |
| vulnerabilityreport.enabled | Enables/disables installation of vulnerability report verifier | `false` |
| vulnerabilityreport.passthrough | Enables/disables passthrough. All validation except `maximumAge` are disregarded and report content is added to verifier report | `false` |
| vulnerabilityreport.schemaURL | URL for JSON schema to validate report against | `` |
Expand Down
11 changes: 11 additions & 0 deletions charts/ratify/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,15 @@ Set the namespace exclusions for Assign
{{- if and (ne .Release.Namespace $gkNamespace) (ne .Release.Namespace "kube-system") }}
- {{ .Release.Namespace | quote}}
{{- end }}
{{- end }}

{{/*
Choose cosign legacy or not
*/}}
akashsinghal marked this conversation as resolved.
Show resolved Hide resolved
{{- define "ratify.cosignLegacy" -}}
{{- if or (gt (len .Values.cosignKeys) 0) (and .Values.azurekeyvault.enabled (gt (len .Values.azurekeyvault.keys) 0)) .Values.cosign.keyless.certificateIdentity .Values.cosign.keyless.certificateIdentityRegExp .Values.cosign.keyless.certificateOIDCIssuer .Values.cosign.keyless.certificateOIDCIssuerExp -}}
akashsinghal marked this conversation as resolved.
Show resolved Hide resolved
false
{{- else }}
true
{{- end }}
{{- end }}
12 changes: 11 additions & 1 deletion charts/ratify/templates/verifier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ spec:
name: cosign
artifactTypes: application/vnd.dev.cosign.artifact.sig.v1+json
parameters:
{{- if or (gt (len .Values.cosignKeys) 0) (and .Values.azurekeyvault.enabled (gt (len .Values.azurekeyvault.keys) 0)) }}
{{- if (eq (include "ratify.cosignLegacy" .) "false") }}
trustPolicies:
- name: default
version: 1.0.0
Expand All @@ -65,6 +65,16 @@ spec:
{{- if and .Values.azurekeyvault.enabled (gt (len .Values.azurekeyvault.keys) 0) }}
- provider: kmprovider-akv
{{- end }}
tLogVerify: {{ .Values.cosign.tLogVerify }}
rekorURL: {{ .Values.cosign.rekorURL }}
{{- if or .Values.cosign.keyless.certificateIdentity .Values.cosign.keyless.certificateIdentityRegExp .Values.cosign.keyless.certificateOIDCIssuer .Values.cosign.keyless.certificateOIDCIssuerRegExp }}
keyless:
ctLogVerify: {{ .Values.cosign.keyless.ctLogVerify }}
certificateIdentity: {{ .Values.cosign.keyless.certificateIdentity }}
certificateIdentityRegExp: {{ .Values.cosign.keyless.certificateIdentityRegExp }}
certificateOIDCIssuer: {{ .Values.cosign.keyless.certificateOIDCIssuer }}
certificateOIDCIssuerRegExp: {{ .Values.cosign.keyless.certificateOIDCIssuerRegExp }}
{{- end }}
{{- else }}
key: /usr/local/ratify-certs/cosign/cosign.pub
{{- end }}
Expand Down
9 changes: 9 additions & 0 deletions charts/ratify/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ cosign:
enabled: true
scopes: ["*"] # corresponds to a single trust policy
key: "" # DEPRECATED: Use cosignKeys instead
rekorURL: ""
tLogVerify: true
keyless:
ctLogVerify: true
certificateIdentity: ""
certificateIdentityRegExp: ""
certificateOIDCIssuer: ""
certificateOIDCIssuerRegExp: ""

vulnerabilityreport:
enabled: false
passthrough: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ spec:
scopes:
- "*"
keys:
- provider: ratify-cosign-inline-key-0
- provider: ratify-cosign-inline-key-0
tLogVerify: false
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ spec:
- "*"
keys:
- provider: default/ratify-cosign-inline-key-0
tLogVerify: false
56 changes: 54 additions & 2 deletions pkg/verifier/cosign/cosign.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
BundleVerified bool `json:"bundleVerified"`
Err string `json:"error,omitempty"`
KeyInformation PKKey `json:"keyInformation,omitempty"`
Summary []string `json:"summary,omitempty"`
}

type cosignVerifier struct {
Expand All @@ -119,7 +120,16 @@
// used for mocking purposes
var getKeysMaps = getKeysMapsDefault

const verifierType string = "cosign"
const (
verifierType string = "cosign"
akashsinghal marked this conversation as resolved.
Show resolved Hide resolved
annotationMessage string = "The specified annotations were verified."
claimsMessage string = "The cosign claims were validated."
offlineBundleMessage string = "Existence of the claims in the transparency log was verified offline."
rekorClaimsMessage string = "The claims were present in the transparency log."
rekorSigMessage string = "The signatures were integrated into the transparency log when the certificate was valid."
sigVerifierMessage string = "The signatures were verified against the specified public key."
certVerifierMessage string = "The code-signing certificate was verified using trusted certificate authority certificates."
)

// init() registers the cosign verifier with the factory
func init() {
Expand Down Expand Up @@ -148,6 +158,7 @@
legacy := true
// if trustPolicies are provided and non-legacy, create the trust policies
if config.KeyRef == "" && config.RekorURL == "" && len(config.TrustPolicies) > 0 {
logger.GetLogger(context.Background(), logOpt).Debugf("legacy cosign verifier configuration not found, creating trust policies")
trustPolicies, err = CreateTrustPolicies(config.TrustPolicies, verifierName)
if err != nil {
return nil, err
Expand Down Expand Up @@ -275,12 +286,29 @@
extension.IsSuccess = false
extension.Err = err.Error()
} else {
extension.Summary = verificationMessage(bundleVerified, &cosignOpts)
hasValidSignature = true
}
extensionListEntry.Verifications = append(extensionListEntry.Verifications, extension)
}

// TODO: perform keyless verification instead if no keys are found
// if no keys are found, perform keyless verification
if len(keysMap) == 0 {
akashsinghal marked this conversation as resolved.
Show resolved Hide resolved
// verify signature with cosign options + perform bundle verification
bundleVerified, err := cosign.VerifyImageSignature(ctx, sig, subjectDescHash, &cosignOpts)
extension := cosignExtension{
IsSuccess: true,
BundleVerified: bundleVerified,

Check warning on line 301 in pkg/verifier/cosign/cosign.go

View check run for this annotation

Codecov / codecov/patch

pkg/verifier/cosign/cosign.go#L298-L301

Added lines #L298 - L301 were not covered by tests
}
if err != nil {
extension.IsSuccess = false
extension.Err = err.Error()
} else {
extension.Summary = verificationMessage(bundleVerified, &cosignOpts)
akashsinghal marked this conversation as resolved.
Show resolved Hide resolved
hasValidSignature = true

Check warning on line 308 in pkg/verifier/cosign/cosign.go

View check run for this annotation

Codecov / codecov/patch

pkg/verifier/cosign/cosign.go#L303-L308

Added lines #L303 - L308 were not covered by tests
}
extensionListEntry.Verifications = append(extensionListEntry.Verifications, extension)

Check warning on line 310 in pkg/verifier/cosign/cosign.go

View check run for this annotation

Codecov / codecov/patch

pkg/verifier/cosign/cosign.go#L310

Added line #L310 was not covered by tests
}
sigExtensions = append(sigExtensions, extensionListEntry)
}

Expand Down Expand Up @@ -594,3 +622,27 @@
}
return hashType, staticSig, nil
}

// verificationMessage returns a string list of all verifications performed
// based on https://github.com/sigstore/cosign/blob/5ae2e31c30ee87e035cc57ebbbe2ecf3b6549ff5/cmd/cosign/cli/verify/verify.go#L318
func verificationMessage(bundleVerified bool, co *cosign.CheckOpts) []string {
akashsinghal marked this conversation as resolved.
Show resolved Hide resolved
var messages []string
if co.ClaimVerifier != nil {
if co.Annotations != nil {
messages = append(messages, annotationMessage)
}
messages = append(messages, claimsMessage)
}
if bundleVerified {
messages = append(messages, offlineBundleMessage)
} else if co.RekorClient != nil {
messages = append(messages, rekorClaimsMessage)
messages = append(messages, rekorSigMessage)
}
if co.SigVerifier != nil {
messages = append(messages, sigVerifierMessage)
} else {
messages = append(messages, certVerifierMessage)
akashsinghal marked this conversation as resolved.
Show resolved Hide resolved
}
return messages
}
71 changes: 62 additions & 9 deletions pkg/verifier/cosign/cosign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ package cosign

import (
"context"
"crypto"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"fmt"
"io"
"slices"
"strings"
"testing"
Expand All @@ -37,11 +39,23 @@ import (
imgspec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sigstore/cosign/v2/pkg/cosign"
"github.com/sigstore/cosign/v2/pkg/oci/static"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
)

const ratifySampleImageRef string = "ghcr.io/deislabs/ratify:v1"

type mockNoOpVerifier struct{}

func (m *mockNoOpVerifier) PublicKey(_ ...signature.PublicKeyOption) (crypto.PublicKey, error) {
return nil, nil
}

func (m *mockNoOpVerifier) VerifySignature(_, _ io.Reader, _ ...signature.VerifyOption) error {
return nil
}

// TestCreate tests the Create function of the cosign verifier
func TestCreate(t *testing.T) {
tests := []struct {
Expand All @@ -57,7 +71,7 @@ func TestCreate(t *testing.T) {
"trustPolicies": []TrustPolicyConfig{
{
Name: "test",
Keyless: KeylessConfig{RekorURL: DefaultRekorURL},
Keyless: KeylessConfig{CertificateIdentity: "test-identity", CertificateOIDCIssuer: "https://test-issuer.com"},
Scopes: []string{"*"},
},
},
Expand Down Expand Up @@ -104,7 +118,7 @@ func TestCreate(t *testing.T) {
"trustPolicies": []TrustPolicyConfig{
{
Name: "test",
Keyless: KeylessConfig{RekorURL: DefaultRekorURL},
Keyless: KeylessConfig{CertificateIdentity: "test-identity", CertificateOIDCIssuer: "https://test-issuer.com"},
Scopes: []string{"*"},
},
},
Expand Down Expand Up @@ -135,7 +149,7 @@ func TestName(t *testing.T) {
"trustPolicies": []TrustPolicyConfig{
{
Name: "test",
Keyless: KeylessConfig{RekorURL: DefaultRekorURL},
Keyless: KeylessConfig{CertificateIdentity: "test-identity", CertificateOIDCIssuer: "https://test-issuer.com"},
Scopes: []string{"*"},
},
},
Expand All @@ -159,7 +173,7 @@ func TestType(t *testing.T) {
"trustPolicies": []TrustPolicyConfig{
{
Name: "test",
Keyless: KeylessConfig{RekorURL: DefaultRekorURL},
Keyless: KeylessConfig{CertificateIdentity: "test-identity", CertificateOIDCIssuer: "https://test-issuer.com"},
Scopes: []string{"*"},
},
},
Expand Down Expand Up @@ -212,7 +226,7 @@ func TestCanVerify(t *testing.T) {
"trustPolicies": []TrustPolicyConfig{
{
Name: "test",
Keyless: KeylessConfig{RekorURL: DefaultRekorURL},
Keyless: KeylessConfig{CertificateIdentity: "test-identity", CertificateOIDCIssuer: "https://test-issuer.com"},
Scopes: []string{"*"},
},
},
Expand All @@ -238,7 +252,7 @@ func TestGetNestedReferences(t *testing.T) {
"trustPolicies": []TrustPolicyConfig{
{
Name: "test",
Keyless: KeylessConfig{RekorURL: DefaultRekorURL},
Keyless: KeylessConfig{CertificateIdentity: "test-identity", CertificateOIDCIssuer: "https://test-issuer.com"},
Scopes: []string{"*"},
},
},
Expand Down Expand Up @@ -445,7 +459,7 @@ func TestGetKeysMaps_Success(t *testing.T) {
trustPolciesConfig := []TrustPolicyConfig{
{
Name: "test-policy",
Keyless: KeylessConfig{RekorURL: DefaultRekorURL},
Keyless: KeylessConfig{CertificateIdentity: "test-identity", CertificateOIDCIssuer: "https://test-issuer.com"},
Scopes: []string{"ghcr.io/*"},
},
}
Expand All @@ -464,7 +478,7 @@ func TestGetKeysMaps_FailingTrustPolicies(t *testing.T) {
trustPolciesConfig := []TrustPolicyConfig{
{
Name: "test-policy",
Keyless: KeylessConfig{RekorURL: DefaultRekorURL},
Keyless: KeylessConfig{CertificateIdentity: "test-identity", CertificateOIDCIssuer: "https://test-issuer.com"},
Scopes: []string{"myregistry.io/*"},
},
}
Expand Down Expand Up @@ -916,7 +930,7 @@ mmBwUAwwW0Uc+Nt3bDOCiB1nUsICv1ry
trustPoliciesConfig := []TrustPolicyConfig{
{
Name: "test-policy",
Keyless: KeylessConfig{RekorURL: DefaultRekorURL},
Keyless: KeylessConfig{CertificateIdentity: "test-identity", CertificateOIDCIssuer: "https://test-issuer.com"},
Scopes: []string{"*"},
},
}
Expand All @@ -937,3 +951,42 @@ mmBwUAwwW0Uc+Nt3bDOCiB1nUsICv1ry
})
}
}

// TestVerificationMessage tests the verificationMessage function
func TestVerificationMessage(t *testing.T) {
tc := []struct {
name string
expectedMessages []string
bundleVerified bool
checkOpts cosign.CheckOpts
}{
{
name: "keyed, offline bundle, claims with annotations",
expectedMessages: []string{annotationMessage, claimsMessage, offlineBundleMessage, sigVerifierMessage},
bundleVerified: true,
checkOpts: cosign.CheckOpts{
ClaimVerifier: cosign.SimpleClaimVerifier,
Annotations: map[string]interface{}{
"test": "test",
},
SigVerifier: &mockNoOpVerifier{},
},
},
{
name: "keyless, rekor, fulcio",
expectedMessages: []string{rekorClaimsMessage, rekorSigMessage, certVerifierMessage},
bundleVerified: false,
checkOpts: cosign.CheckOpts{
RekorClient: &client.Rekor{},
},
},
}
for i, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
result := verificationMessage(tt.bundleVerified, &tc[i].checkOpts)
if !slices.Equal(result, tt.expectedMessages) {
t.Errorf("verificationMessage() = %v, want %v", result, tt.expectedMessages)
}
})
}
}
Loading
Loading