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 a4ab37108917..a26d5027c044 100644 --- a/cmd/cosign/cli/policy_init.go +++ b/cmd/cosign/cli/policy_init.go @@ -202,7 +202,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 7a365cfd158c..81412b2c46ec 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -87,6 +87,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 @@ -107,6 +112,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 e7d524c93325..f14644ad4c4a 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -61,6 +61,7 @@ type VerifyCommand struct { RekorURL string Attachment string Annotations sigs.AnnotationsMap + CertExtensions sigs.CertExtensionsMap SignatureRef string HashAlgorithm crypto.Hash LocalImage bool @@ -93,6 +94,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, @@ -234,7 +236,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) } } @@ -267,7 +269,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 97d17aad5112..50e4f2b2f047 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -57,6 +57,7 @@ cosign dockerfile verify [flags] --attachment string related image attachment to sign (sbom), default none --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] --base-image-only only verify the base image (the last FROM image in the Dockerfile) + --cert-extensions strings extra key=value pairs to verify --certificate string path to the public certificate --certificate-chain string path to a list of CA certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate --certificate-email string the email expected in a valid Fulcio certificate diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index b98168ffe661..a53abe9a6d41 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -51,6 +51,7 @@ cosign manifest verify [flags] -a, --annotations strings extra key=value pairs to sign --attachment string related image attachment to sign (sbom), default none --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-extensions strings extra key=value pairs to verify --certificate string path to the public certificate --certificate-chain string path to a list of CA certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate --certificate-email string the email expected in a valid Fulcio certificate diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index 8bfae767cb1a..3715efe7ba13 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -70,6 +70,7 @@ cosign verify [flags] -a, --annotations strings extra key=value pairs to sign --attachment string related image attachment to sign (sbom), default none --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-extensions strings extra key=value pairs to verify --certificate string path to the public certificate --certificate-chain string path to a list of CA certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate --certificate-email string the email expected in a valid Fulcio certificate 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 7e9efea202d2..ee9eddd2141f 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/verifiers_test.go b/pkg/cosign/verifiers_test.go index c8aa4889eb8c..e1e34eb4ca33 100644 --- a/pkg/cosign/verifiers_test.go +++ b/pkg/cosign/verifiers_test.go @@ -69,7 +69,7 @@ func Test_IntotoSubjectClaimVerifier(t *testing.T) { if err != nil { t.Fatal("Failed to create static.NewSignature: ", err) } - got := IntotoSubjectClaimVerifier(ociSig, tc.digest, nil) + got := IntotoSubjectClaimVerifier(ociSig, tc.digest, nil, nil, nil) if got != nil && !tc.shouldFail { t.Error("Expected ClaimVerifier to succeed but failed: ", got) } diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 8424e0fbd8bf..fb3dbb298f68 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -71,8 +71,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 @@ -468,6 +470,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() @@ -493,6 +496,7 @@ func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co } co.IntermediateCerts = pool } + actualCertExtensions = CertExtensions(cert) verifier, err = ValidateAndUnpackCert(cert, co) if err != nil { return bundleVerified, err @@ -505,7 +509,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 } } @@ -644,6 +648,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 @@ -672,6 +677,7 @@ func verifyImageAttestations(ctx context.Context, atts oci.Signatures, h v1.Hash } co.IntermediateCerts = pool } + actualCertExtensions = CertExtensions(cert) verifier, err = ValidateAndUnpackCert(cert, co) if err != nil { return err @@ -684,7 +690,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 } } @@ -966,3 +972,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 90d3e6d1991a..724777735a80 100644 --- a/pkg/signature/keys.go +++ b/pkg/signature/keys.go @@ -34,25 +34,6 @@ import ( "github.com/sigstore/sigstore/pkg/signature/kms" ) -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) @@ -254,25 +235,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 5f1c0ac43898..0c74798124e3 100644 --- a/pkg/signature/keys_test.go +++ b/pkg/signature/keys_test.go @@ -158,7 +158,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))