From fd446edbd152eb698c43ee786f9629e5a262b564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Batuhan=20Apayd=C4=B1n?= Date: Thu, 17 Mar 2022 12:16:15 +0300 Subject: [PATCH] feat: cert-extensions verify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Batuhan Apaydın Co-authored-by: Christian Kotzbauer <@ckotzbauer1> Signed-off-by: Batuhan Apaydın --- cmd/cosign/cli/options/certextensions.go | 53 +++++++++++++++++++++ cmd/cosign/cli/options/verify.go | 2 + cmd/cosign/cli/policy_init.go | 2 +- cmd/cosign/cli/verify.go | 6 +++ cmd/cosign/cli/verify/verify.go | 6 ++- doc/cosign_dockerfile_verify.md | 1 + doc/cosign_manifest_verify.md | 1 + doc/cosign_verify.md | 1 + pkg/cosign/certextensions.go | 59 ++++++++++++++++++++++++ pkg/cosign/verifiers.go | 11 ++++- pkg/cosign/verify.go | 21 +++++++-- pkg/signature/certextensions.go | 47 +++++++++++++++++++ pkg/signature/keys.go | 41 ---------------- pkg/signature/keys_test.go | 2 +- 14 files changed, 203 insertions(+), 50 deletions(-) create mode 100644 cmd/cosign/cli/options/certextensions.go create mode 100644 pkg/cosign/certextensions.go create mode 100644 pkg/signature/certextensions.go diff --git a/cmd/cosign/cli/options/certextensions.go b/cmd/cosign/cli/options/certextensions.go new file mode 100644 index 000000000000..88aa2749cd0d --- /dev/null +++ b/cmd/cosign/cli/options/certextensions.go @@ -0,0 +1,53 @@ +// +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + sigs "github.com/sigstore/cosign/pkg/signature" +) + +// CertExtensionOptions is the top level wrapper for the annotations. +type CertExtensionOptions struct { + CertExtensions []string +} + +var _ Interface = (*CertExtensionOptions)(nil) + +func (o *CertExtensionOptions) CertExtensionsMap() (sigs.CertExtensionsMap, error) { + ce := sigs.CertExtensionsMap{} + for _, a := range o.CertExtensions { + kv := strings.Split(a, "=") + if len(kv) != 2 { + return ce, fmt.Errorf("unable to parse cert extension: %s", a) + } + if ce.CertExtensions == nil { + ce.CertExtensions = map[string]string{} + } + ce.CertExtensions[kv[0]] = kv[1] + } + return ce, nil +} + +// AddFlags implements Interface +func (o *CertExtensionOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringSliceVarP(&o.CertExtensions, "cert-extensions", "", nil, + "extra key=value pairs to verify") +} diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go index 869197b33329..96fd38997751 100644 --- a/cmd/cosign/cli/options/verify.go +++ b/cmd/cosign/cli/options/verify.go @@ -34,6 +34,7 @@ type VerifyOptions struct { Registry RegistryOptions SignatureDigest SignatureDigestOptions AnnotationOptions + CertExtensionOptions } var _ Interface = (*VerifyOptions)(nil) @@ -46,6 +47,7 @@ func (o *VerifyOptions) AddFlags(cmd *cobra.Command) { o.Registry.AddFlags(cmd) o.SignatureDigest.AddFlags(cmd) o.AnnotationOptions.AddFlags(cmd) + o.CertExtensionOptions.AddFlags(cmd) cmd.Flags().StringVar(&o.Key, "key", "", "path to the public key file, KMS URI or Kubernetes Secret") diff --git a/cmd/cosign/cli/policy_init.go b/cmd/cosign/cli/policy_init.go index 2ce0e3fe6031..543189173041 100644 --- a/cmd/cosign/cli/policy_init.go +++ b/cmd/cosign/cli/policy_init.go @@ -197,7 +197,7 @@ func signPolicy() *cobra.Command { return errors.New("error decoding certificate") } signerEmail := sigs.CertSubject(certs[0]) - signerIssuer := sigs.CertIssuerExtension(certs[0]) + signerIssuer := cosign.CertIssuerExtension(certs[0]) // Retrieve root.json from registry. imgName := rootPath(o.ImageRef) diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index e518aada94b8..a7ae3daacd0e 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -81,6 +81,11 @@ against the transparency log.`, return err } + certExtensions, err := o.CertExtensionsMap() + if err != nil { + return err + } + hashAlgorithm, err := o.SignatureDigest.HashAlgorithm() if err != nil { return err @@ -99,6 +104,7 @@ against the transparency log.`, RekorURL: o.Rekor.URL, Attachment: o.Attachment, Annotations: annotations, + CertExtensions: certExtensions, HashAlgorithm: hashAlgorithm, SignatureRef: o.SignatureRef, LocalImage: o.LocalImage, diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 907701551779..0026edef6111 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -59,6 +59,7 @@ type VerifyCommand struct { RekorURL string Attachment string Annotations sigs.AnnotationsMap + CertExtensions sigs.CertExtensionsMap SignatureRef string HashAlgorithm crypto.Hash LocalImage bool @@ -91,6 +92,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } co := &cosign.CheckOpts{ Annotations: c.Annotations.Annotations, + CertExtensions: c.CertExtensions.CertExtensions, RegistryClientOpts: ociremoteOpts, CertEmail: c.CertEmail, CertOidcIssuer: c.CertOidcIssuer, @@ -214,7 +216,7 @@ func PrintVerification(imgRef string, verified []oci.Signature, output string) { for _, sig := range verified { if cert, err := sig.Cert(); err == nil && cert != nil { fmt.Fprintln(os.Stderr, "Certificate subject: ", sigs.CertSubject(cert)) - if issuerURL := sigs.CertIssuerExtension(cert); issuerURL != "" { + if issuerURL := cosign.CertIssuerExtension(cert); issuerURL != "" { fmt.Fprintln(os.Stderr, "Certificate issuer URL: ", issuerURL) } } @@ -247,7 +249,7 @@ func PrintVerification(imgRef string, verified []oci.Signature, output string) { ss.Optional = make(map[string]interface{}) } ss.Optional["Subject"] = sigs.CertSubject(cert) - if issuerURL := sigs.CertIssuerExtension(cert); issuerURL != "" { + if issuerURL := cosign.CertIssuerExtension(cert); issuerURL != "" { ss.Optional["Issuer"] = issuerURL } } diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index 0c812293baa0..1755c0d08d2f 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -59,6 +59,7 @@ cosign dockerfile verify [flags] --base-image-only only verify the base image (the last FROM image in the Dockerfile) --cert string path to the public certificate --cert-email string the email expected in a valid Fulcio certificate + --cert-extensions strings extra key=value pairs to verify --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) -h, --help help for verify diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index 13c31b1a93f9..e3aa46c2a52e 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -53,6 +53,7 @@ cosign manifest verify [flags] --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] --cert string path to the public certificate --cert-email string the email expected in a valid Fulcio certificate + --cert-extensions strings extra key=value pairs to verify --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) -h, --help help for verify diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index a4a21e0edc5b..60046a4e0490 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -66,6 +66,7 @@ cosign verify [flags] --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] --cert string path to the public certificate --cert-email string the email expected in a valid Fulcio certificate + --cert-extensions strings extra key=value pairs to verify --cert-oidc-issuer string the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth --check-claims whether to check the claims found (default true) -h, --help help for verify diff --git a/pkg/cosign/certextensions.go b/pkg/cosign/certextensions.go new file mode 100644 index 000000000000..81ff03262ff3 --- /dev/null +++ b/pkg/cosign/certextensions.go @@ -0,0 +1,59 @@ +// +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cosign + +import "crypto/x509" + +var ( + // Fulcio cert-extensions, documented here: https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md + CertExtensionOIDCIssuer = "1.3.6.1.4.1.57264.1.1" + CertExtensionGithubWorkflowTrigger = "1.3.6.1.4.1.57264.1.2" + CertExtensionGithubWorkflowSha = "1.3.6.1.4.1.57264.1.3" + CertExtensionGithubWorkflowName = "1.3.6.1.4.1.57264.1.4" + CertExtensionGithubWorkflowRepository = "1.3.6.1.4.1.57264.1.5" + CertExtensionGithubWorkflowRef = "1.3.6.1.4.1.57264.1.6" + + CertExtensionMap = map[string]string{ + CertExtensionOIDCIssuer: "oidcIssuer", + CertExtensionGithubWorkflowTrigger: "githubWorkflowTrigger", + CertExtensionGithubWorkflowSha: "githubWorkflowSha", + CertExtensionGithubWorkflowName: "githubWorkflowName", + CertExtensionGithubWorkflowRepository: "githubWorkflowRepository", + CertExtensionGithubWorkflowRef: "githubWorkflowRef", + } +) + +func CertIssuerExtension(cert *x509.Certificate) string { + for _, ext := range cert.Extensions { + if ext.Id.String() == CertExtensionOIDCIssuer { + return string(ext.Value) + } + } + return "" +} + +func CertExtensions(cert *x509.Certificate) map[string]string { + extensions := map[string]string{} + for _, ext := range cert.Extensions { + readableName, ok := CertExtensionMap[ext.Id.String()] + if ok { + extensions[readableName] = string(ext.Value) + } else { + extensions[ext.Id.String()] = string(ext.Value) + } + } + return extensions +} diff --git a/pkg/cosign/verifiers.go b/pkg/cosign/verifiers.go index 57641440043d..85e30339accc 100644 --- a/pkg/cosign/verifiers.go +++ b/pkg/cosign/verifiers.go @@ -30,7 +30,7 @@ import ( ) // SimpleClaimVerifier verifies that sig.Payload() is a SimpleContainerImage payload which references the given image digest and contains the given annotations. -func SimpleClaimVerifier(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}) error { +func SimpleClaimVerifier(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}, certExtensionsActual, certExtensionsExpected map[string]string) error { p, err := sig.Payload() if err != nil { return err @@ -51,11 +51,18 @@ func SimpleClaimVerifier(sig oci.Signature, imageDigest v1.Hash, annotations map return errors.New("missing or incorrect annotation") } } + + if certExtensionsExpected != nil { + if !correctCertExtensions(certExtensionsExpected, certExtensionsActual) { + return errors.New("missing or incorrect cert extension") + } + } + return nil } // IntotoSubjectClaimVerifier verifies that sig.Payload() is an Intoto statement which references the given image digest. -func IntotoSubjectClaimVerifier(sig oci.Signature, imageDigest v1.Hash, _ map[string]interface{}) error { +func IntotoSubjectClaimVerifier(sig oci.Signature, imageDigest v1.Hash, _ map[string]interface{}, _ map[string]string, _ map[string]string) error { p, err := sig.Payload() if err != nil { return err diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 70846fd3af6d..112cb250d9cf 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -62,8 +62,10 @@ type CheckOpts struct { // Annotations optionally specifies image signature annotations to verify. Annotations map[string]interface{} + // CertExtensions optionally specifies image signature cert extensions to verify. + CertExtensions map[string]string // ClaimVerifier, if provided, verifies claims present in the oci.Signature. - ClaimVerifier func(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}) error + ClaimVerifier func(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}, certExtensionsActual, certExtensionsExpected map[string]string) error // RekorClient, if set, is used to use to verify signatures and public keys. RekorClient *client.Rekor @@ -341,6 +343,7 @@ func verifySignatures(ctx context.Context, sigs oci.Signatures, h v1.Hash, co *C // VerifyImageSignature verifies a signature func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co *CheckOpts) (bundleVerified bool, err error) { verifier := co.SigVerifier + actualCertExtensions := map[string]string{} if verifier == nil { // If we don't have a public key to check against, we can try a root cert. cert, err := sig.Cert() @@ -350,6 +353,7 @@ func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co if cert == nil { return bundleVerified, errors.New("no certificate found on signature") } + actualCertExtensions = CertExtensions(cert) verifier, err = ValidateAndUnpackCert(cert, co) if err != nil { return bundleVerified, err @@ -362,7 +366,7 @@ func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co // We can't check annotations without claims, both require unmarshalling the payload. if co.ClaimVerifier != nil { - if err := co.ClaimVerifier(sig, h, co.Annotations); err != nil { + if err := co.ClaimVerifier(sig, h, co.Annotations, actualCertExtensions, co.CertExtensions); err != nil { return bundleVerified, err } } @@ -501,6 +505,7 @@ func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash } validationErrs := []string{} + actualCertExtensions := map[string]string{} for _, att := range sl { if err := func(att oci.Signature) error { verifier := co.SigVerifier @@ -513,6 +518,7 @@ func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash if cert == nil { return errors.New("no certificate found on attestation") } + actualCertExtensions = CertExtensions(cert) verifier, err = ValidateAndUnpackCert(cert, co) if err != nil { return err @@ -525,7 +531,7 @@ func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash // We can't check annotations without claims, both require unmarshalling the payload. if co.ClaimVerifier != nil { - if err := co.ClaimVerifier(att, h, co.Annotations); err != nil { + if err := co.ClaimVerifier(att, h, co.Annotations, actualCertExtensions, co.CertExtensions); err != nil { return err } } @@ -807,3 +813,12 @@ func correctAnnotations(wanted, have map[string]interface{}) bool { } return true } + +func correctCertExtensions(wanted, have map[string]string) bool { + for k, v := range wanted { + if have[k] != v { + return false + } + } + return true +} diff --git a/pkg/signature/certextensions.go b/pkg/signature/certextensions.go new file mode 100644 index 000000000000..854a589358e6 --- /dev/null +++ b/pkg/signature/certextensions.go @@ -0,0 +1,47 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package signature + +import ( + _ "crypto/sha256" // for `crypto.SHA256` + "fmt" + "strings" +) + +type CertExtensionsMap struct { + CertExtensions map[string]string +} + +func (a *CertExtensionsMap) Set(s string) error { + if a.CertExtensions == nil { + a.CertExtensions = map[string]string{} + } + kvp := strings.SplitN(s, "=", 2) + if len(kvp) != 2 { + return fmt.Errorf("invalid flag: %s, expected key=value", s) + } + + a.CertExtensions[kvp[0]] = kvp[1] + return nil +} + +func (a *CertExtensionsMap) String() string { + s := []string{} + for k, v := range a.CertExtensions { + s = append(s, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(s, ",") +} diff --git a/pkg/signature/keys.go b/pkg/signature/keys.go index 3fa1310354e5..594c7c7758eb 100644 --- a/pkg/signature/keys.go +++ b/pkg/signature/keys.go @@ -43,25 +43,6 @@ import ( _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" ) -var ( - // Fulcio cert-extensions, documented here: https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md - CertExtensionOIDCIssuer = "1.3.6.1.4.1.57264.1.1" - CertExtensionGithubWorkflowTrigger = "1.3.6.1.4.1.57264.1.2" - CertExtensionGithubWorkflowSha = "1.3.6.1.4.1.57264.1.3" - CertExtensionGithubWorkflowName = "1.3.6.1.4.1.57264.1.4" - CertExtensionGithubWorkflowRepository = "1.3.6.1.4.1.57264.1.5" - CertExtensionGithubWorkflowRef = "1.3.6.1.4.1.57264.1.6" - - CertExtensionMap = map[string]string{ - CertExtensionOIDCIssuer: "oidcIssuer", - CertExtensionGithubWorkflowTrigger: "githubWorkflowTrigger", - CertExtensionGithubWorkflowSha: "githubWorkflowSha", - CertExtensionGithubWorkflowName: "githubWorkflowName", - CertExtensionGithubWorkflowRepository: "githubWorkflowRepository", - CertExtensionGithubWorkflowRef: "githubWorkflowRef", - } -) - // LoadPublicKey is a wrapper for VerifierForKeyRef, hardcoding SHA256 as the hash algorithm func LoadPublicKey(ctx context.Context, keyRef string) (verifier signature.Verifier, err error) { return VerifierForKeyRef(ctx, keyRef, crypto.SHA256) @@ -259,25 +240,3 @@ func CertSubject(c *x509.Certificate) string { } return "" } - -func CertIssuerExtension(cert *x509.Certificate) string { - for _, ext := range cert.Extensions { - if ext.Id.String() == CertExtensionOIDCIssuer { - return string(ext.Value) - } - } - return "" -} - -func CertExtensions(cert *x509.Certificate) map[string]string { - extensions := map[string]string{} - for _, ext := range cert.Extensions { - readableName, ok := CertExtensionMap[ext.Id.String()] - if ok { - extensions[readableName] = string(ext.Value) - } else { - extensions[ext.Id.String()] = string(ext.Value) - } - } - return extensions -} diff --git a/pkg/signature/keys_test.go b/pkg/signature/keys_test.go index e95d335639e1..1578243e7a18 100644 --- a/pkg/signature/keys_test.go +++ b/pkg/signature/keys_test.go @@ -129,7 +129,7 @@ func createCert(t *testing.T) *x509.Certificate { func TestCertExtensions(t *testing.T) { t.Parallel() cert := createCert(t) - exts := CertExtensions(cert) + exts := cosign.CertExtensions(cert) if len(exts) != 4 { t.Fatalf("Unexpected extension-count: %v", len(exts))