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/certificate.go b/cmd/cosign/cli/options/certificate.go index d383857c1fe7..31c1247a457b 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -20,11 +20,16 @@ import ( // CertVerifyOptions is the wrapper for certificate verification. type CertVerifyOptions struct { - Cert string - CertEmail string - CertOidcIssuer string - CertChain string - EnforceSCT bool + Cert string + CertEmail string + CertOidcIssuer string + CertGithubWorkflowTrigger string + CertGithubWorkflowSha string + CertGithubWorkflowName string + CertGithubWorkflowRepository string + CertGithubWorkflowRef string + CertChain string + EnforceSCT bool } var _ Interface = (*RekorOptions)(nil) @@ -40,6 +45,23 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.CertOidcIssuer, "certificate-oidc-issuer", "", "the OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth") + // -- Cert extensions begin -- + // Source: https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md + cmd.Flags().StringVar(&o.CertGithubWorkflowTrigger, "certificate-github-workflow-trigger", "", + "contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run") + + cmd.Flags().StringVar(&o.CertGithubWorkflowSha, "certificate-github-workflow-sha", "", + "contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon.") + + cmd.Flags().StringVar(&o.CertGithubWorkflowName, "certificate-github-workflow-name", "", + "contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow.") + + cmd.Flags().StringVar(&o.CertGithubWorkflowRepository, "certificate-github-workflow-repository", "", + "contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon") + + cmd.Flags().StringVar(&o.CertGithubWorkflowRef, "certificate-github-workflow-ref", "", + "contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon.") + // -- Cert extensions end -- cmd.Flags().StringVar(&o.CertChain, "certificate-chain", "", "path to a list of CA certificates in PEM format which will be needed "+ "when building the certificate chain for the signing certificate. "+ diff --git a/cmd/cosign/cli/policy_init.go b/cmd/cosign/cli/policy_init.go index a4ab37108917..92d3106caf18 100644 --- a/cmd/cosign/cli/policy_init.go +++ b/cmd/cosign/cli/policy_init.go @@ -202,7 +202,8 @@ func signPolicy() *cobra.Command { return errors.New("error decoding certificate") } signerEmail := sigs.CertSubject(certs[0]) - signerIssuer := sigs.CertIssuerExtension(certs[0]) + ce := cosign.CertExtensions{Cert: certs[0]} + signerIssuer := ce.GetIssuer() // 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..dc15ec435f85 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -93,23 +93,28 @@ against the transparency log.`, } v := verify.VerifyCommand{ - RegistryOptions: o.Registry, - CheckClaims: o.CheckClaims, - KeyRef: o.Key, - CertRef: o.CertVerify.Cert, - CertEmail: o.CertVerify.CertEmail, - CertOidcIssuer: o.CertVerify.CertOidcIssuer, - CertChain: o.CertVerify.CertChain, - EnforceSCT: o.CertVerify.EnforceSCT, - Sk: o.SecurityKey.Use, - Slot: o.SecurityKey.Slot, - Output: o.Output, - RekorURL: o.Rekor.URL, - Attachment: o.Attachment, - Annotations: annotations, - HashAlgorithm: hashAlgorithm, - SignatureRef: o.SignatureRef, - LocalImage: o.LocalImage, + RegistryOptions: o.Registry, + CheckClaims: o.CheckClaims, + KeyRef: o.Key, + CertRef: o.CertVerify.Cert, + CertEmail: o.CertVerify.CertEmail, + CertOidcIssuer: o.CertVerify.CertOidcIssuer, + CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger, + CertGithubWorkflowSha: o.CertVerify.CertGithubWorkflowSha, + CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, + CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, + CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, + CertChain: o.CertVerify.CertChain, + EnforceSCT: o.CertVerify.EnforceSCT, + Sk: o.SecurityKey.Use, + Slot: o.SecurityKey.Slot, + Output: o.Output, + RekorURL: o.Rekor.URL, + Attachment: o.Attachment, + Annotations: annotations, + HashAlgorithm: hashAlgorithm, + SignatureRef: o.SignatureRef, + LocalImage: o.LocalImage, } return v.Exec(cmd.Context(), args) diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index e7d524c93325..c929693364ca 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -48,22 +48,27 @@ import ( // nolint type VerifyCommand struct { options.RegistryOptions - CheckClaims bool - KeyRef string - CertRef string - CertEmail string - CertOidcIssuer string - CertChain string - EnforceSCT bool - Sk bool - Slot string - Output string - RekorURL string - Attachment string - Annotations sigs.AnnotationsMap - SignatureRef string - HashAlgorithm crypto.Hash - LocalImage bool + CheckClaims bool + KeyRef string + CertRef string + CertEmail string + CertOidcIssuer string + CertGithubWorkflowTrigger string + CertGithubWorkflowSha string + CertGithubWorkflowName string + CertGithubWorkflowRepository string + CertGithubWorkflowRef string + CertChain string + EnforceSCT bool + Sk bool + Slot string + Output string + RekorURL string + Attachment string + Annotations sigs.AnnotationsMap + SignatureRef string + HashAlgorithm crypto.Hash + LocalImage bool } // Exec runs the verification command @@ -92,12 +97,17 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return fmt.Errorf("constructing client options: %w", err) } co := &cosign.CheckOpts{ - Annotations: c.Annotations.Annotations, - RegistryClientOpts: ociremoteOpts, - CertEmail: c.CertEmail, - CertOidcIssuer: c.CertOidcIssuer, - EnforceSCT: c.EnforceSCT, - SignatureRef: c.SignatureRef, + Annotations: c.Annotations.Annotations, + RegistryClientOpts: ociremoteOpts, + CertEmail: c.CertEmail, + CertOidcIssuer: c.CertOidcIssuer, + CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, + CertGithubWorkflowSha: c.CertGithubWorkflowSha, + CertGithubWorkflowName: c.CertGithubWorkflowName, + CertGithubWorkflowRepository: c.CertGithubWorkflowRepository, + CertGithubWorkflowRef: c.CertGithubWorkflowRef, + EnforceSCT: c.EnforceSCT, + SignatureRef: c.SignatureRef, } if c.CheckClaims { co.ClaimVerifier = cosign.SimpleClaimVerifier @@ -233,8 +243,9 @@ func PrintVerification(imgRef string, verified []oci.Signature, output string) { case "text": for _, sig := range verified { if cert, err := sig.Cert(); err == nil && cert != nil { + ce := cosign.CertExtensions{Cert: cert} fmt.Fprintln(os.Stderr, "Certificate subject: ", sigs.CertSubject(cert)) - if issuerURL := sigs.CertIssuerExtension(cert); issuerURL != "" { + if issuerURL := ce.GetIssuer(); issuerURL != "" { fmt.Fprintln(os.Stderr, "Certificate issuer URL: ", issuerURL) } } @@ -263,11 +274,12 @@ func PrintVerification(imgRef string, verified []oci.Signature, output string) { } if cert, err := sig.Cert(); err == nil && cert != nil { + ce := cosign.CertExtensions{Cert: cert} if ss.Optional == nil { ss.Optional = make(map[string]interface{}) } ss.Optional["Subject"] = sigs.CertSubject(cert) - if issuerURL := sigs.CertIssuerExtension(cert); issuerURL != "" { + if issuerURL := ce.GetIssuer(); issuerURL != "" { ss.Optional["Issuer"] = issuerURL } } diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index 97d17aad5112..f02c09852c8a 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -60,6 +60,11 @@ cosign dockerfile verify [flags] --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 + --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. + --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. + --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon + --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. + --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run --certificate-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) --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index b98168ffe661..00d8e738e1b4 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -54,6 +54,11 @@ cosign manifest verify [flags] --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 + --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. + --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. + --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon + --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. + --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run --certificate-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) --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index 6acd31adf0f0..3d699c46e0cd 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -64,6 +64,11 @@ cosign verify-attestation [flags] --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 + --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. + --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. + --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon + --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. + --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run --certificate-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) --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index 6c17df3d07bc..f0a6667b6732 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -67,6 +67,11 @@ cosign verify-blob [flags] --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 + --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. + --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. + --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon + --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. + --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run --certificate-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 --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log -h, --help help for verify-blob diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index 8bfae767cb1a..7c87c916d9e7 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -73,6 +73,11 @@ cosign verify [flags] --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 + --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. + --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. + --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon + --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. + --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run --certificate-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) --enforce-sct whether to enforce that a certificate contain an embedded SCT, a proof of inclusion in a certificate transparency log diff --git a/pkg/cosign/certextensions.go b/pkg/cosign/certextensions.go new file mode 100644 index 000000000000..376a2add8bd1 --- /dev/null +++ b/pkg/cosign/certextensions.go @@ -0,0 +1,84 @@ +// +// 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" + +type CertExtensions struct { + Cert *x509.Certificate +} + +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 (ce *CertExtensions) certExtensions() map[string]string { + extensions := map[string]string{} + for _, ext := range ce.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 +} + +// GetIssuer returns the issuer for a Certificate +func (ce *CertExtensions) GetIssuer() string { + return ce.certExtensions()["oidcIssuer"] +} + +// GetCertExtensionGithubWorkflowTrigger returns the GitHub Workflow Trigger for a Certificate +func (ce *CertExtensions) GetCertExtensionGithubWorkflowTrigger() string { + return ce.certExtensions()["githubWorkflowTrigger"] +} + +// GetExtensionGithubWorkflowSha returns the GitHub Workflow SHA for a Certificate +func (ce *CertExtensions) GetExtensionGithubWorkflowSha() string { + return ce.certExtensions()["githubWorkflowSha"] +} + +// GetCertExtensionGithubWorkflowName returns the GitHub Workflow Name for a Certificate +func (ce *CertExtensions) GetCertExtensionGithubWorkflowName() string { + return ce.certExtensions()["githubWorkflowName"] +} + +// GetCertExtensionGithubWorkflowRepository returns the GitHub Workflow Repository for a Certificate +func (ce *CertExtensions) GetCertExtensionGithubWorkflowRepository() string { + return ce.certExtensions()["githubWorkflowRepository"] +} + +// GetCertExtensionGithubWorkflowRef returns the GitHub Workflow Ref for a Certificate +func (ce *CertExtensions) GetCertExtensionGithubWorkflowRef() string { + return ce.certExtensions()["githubWorkflowRef"] +} diff --git a/pkg/cosign/verifiers.go b/pkg/cosign/verifiers.go index 7e9efea202d2..8dbae32f3ba6 100644 --- a/pkg/cosign/verifiers.go +++ b/pkg/cosign/verifiers.go @@ -51,6 +51,7 @@ func SimpleClaimVerifier(sig oci.Signature, imageDigest v1.Hash, annotations map return errors.New("missing or incorrect annotation") } } + return nil } 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..612dd74a4529 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -21,7 +21,6 @@ import ( "crypto/ecdsa" "crypto/sha256" "crypto/x509" - "encoding/asn1" "encoding/base64" "encoding/hex" "encoding/json" @@ -71,6 +70,7 @@ type CheckOpts struct { // Annotations optionally specifies image signature annotations to verify. Annotations map[string]interface{} + // ClaimVerifier, if provided, verifies claims present in the oci.Signature. ClaimVerifier func(sig oci.Signature, imageDigest v1.Hash, annotations map[string]interface{}) error @@ -90,6 +90,14 @@ type CheckOpts struct { CertEmail string // CertOidcIssuer is the OIDC issuer expected for a certificate to be valid. The empty string means any certificate can be valid. CertOidcIssuer string + + // CertGithubWorkflowTrigger is the GitHub Workflow Trigger name expected for a certificate to be valid. The empty string means any certificate can be valid. + CertGithubWorkflowTrigger string + CertGithubWorkflowSha string + CertGithubWorkflowName string + CertGithubWorkflowRepository string + CertGithubWorkflowRef string + // EnforceSCT requires that a certificate contain an embedded SCT during verification. An SCT is proof of inclusion in a // certificate transparency log. EnforceSCT bool @@ -201,6 +209,7 @@ func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Ver // CheckCertificatePolicy checks that the certificate subject and issuer match // the expected values. func CheckCertificatePolicy(cert *x509.Certificate, co *CheckOpts) error { + ce := CertExtensions{Cert: cert} if co.CertEmail != "" { emailVerified := false for _, em := range cert.EmailAddresses { @@ -213,11 +222,11 @@ func CheckCertificatePolicy(cert *x509.Certificate, co *CheckOpts) error { return errors.New("expected email not found in certificate") } } - if co.CertOidcIssuer != "" { - if getIssuer(cert) != co.CertOidcIssuer { - return errors.New("expected oidc issuer not found in certificate") - } + + if err := validateCertExtensions(ce, co); err != nil { + return err } + // If there are identities given, go through them and if one of them // matches, call that good, otherwise, return an error. if len(co.Identities) > 0 { @@ -225,7 +234,7 @@ func CheckCertificatePolicy(cert *x509.Certificate, co *CheckOpts) error { issuerMatches := false // Check the issuer first if identity.Issuer != "" { - issuer := getIssuer(cert) + issuer := ce.GetIssuer() if regex, err := regexp.Compile(identity.Issuer); err != nil { return fmt.Errorf("malformed issuer in identity: %s : %w", identity.Issuer, err) } else if regex.MatchString(issuer) { @@ -263,6 +272,45 @@ func CheckCertificatePolicy(cert *x509.Certificate, co *CheckOpts) error { return nil } +func validateCertExtensions(ce CertExtensions, co *CheckOpts) error { + if co.CertOidcIssuer != "" { + if ce.GetIssuer() != co.CertOidcIssuer { + return errors.New("expected oidc issuer not found in certificate") + } + } + + if co.CertGithubWorkflowTrigger != "" { + if ce.GetCertExtensionGithubWorkflowTrigger() != co.CertGithubWorkflowTrigger { + return errors.New("expected GitHub Workflow Trigger not found in certificate") + } + } + + if co.CertGithubWorkflowSha != "" { + if ce.GetExtensionGithubWorkflowSha() != co.CertGithubWorkflowSha { + return errors.New("expected GitHub Workflow SHA not found in certificate") + } + } + + if co.CertGithubWorkflowName != "" { + if ce.GetCertExtensionGithubWorkflowName() != co.CertGithubWorkflowName { + return errors.New("expected GitHub Workflow Name not found in certificate") + } + } + + if co.CertGithubWorkflowRepository != "" { + if ce.GetCertExtensionGithubWorkflowRepository() != co.CertGithubWorkflowRepository { + return errors.New("expected GitHub Workflow Repository not found in certificate") + } + } + + if co.CertGithubWorkflowRef != "" { + if ce.GetCertExtensionGithubWorkflowRef() != co.CertGithubWorkflowRef { + return errors.New("expected GitHub Workflow Ref not found in certificate") + } + } + return nil +} + // getSubjectAlternateNames returns all of the following for a Certificate. // DNSNames // EmailAddresses @@ -281,16 +329,6 @@ func getSubjectAlternateNames(cert *x509.Certificate) []string { return sans } -// getIssuer returns the issuer for a Certificate -func getIssuer(cert *x509.Certificate) string { - for _, ext := range cert.Extensions { - if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}) { - return string(ext.Value) - } - } - return "" -} - // ValidateAndUnpackCertWithChain creates a Verifier from a certificate. Verifies that the certificate // chains up to the provided root. Chain should start with the parent of the certificate and end with the root. // Optionally verifies the subject and issuer of the certificate. @@ -966,3 +1004,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..16f16e297f2b 100644 --- a/pkg/signature/keys_test.go +++ b/pkg/signature/keys_test.go @@ -158,25 +158,17 @@ func createCert(t *testing.T) *x509.Certificate { func TestCertExtensions(t *testing.T) { t.Parallel() cert := createCert(t) - exts := CertExtensions(cert) + exts := cosign.CertExtensions{Cert: cert} - if len(exts) != 4 { - t.Fatalf("Unexpected extension-count: %v", len(exts)) - } - - if val, ok := exts["oidcIssuer"]; !ok || val != "myIssuer" { + if val := exts.GetIssuer(); val != "myIssuer" { t.Fatal("CertExtension does not extract field 'oidcIssuer' correctly") } - if val, ok := exts["githubWorkflowName"]; !ok || val != "myWorkflowName" { + if val := exts.GetCertExtensionGithubWorkflowName(); val != "myWorkflowName" { t.Fatal("CertExtension does not extract field 'githubWorkflowName' correctly") } - if val, ok := exts["githubWorkflowRef"]; !ok || val != "myWorkflowRef" { + if val := exts.GetCertExtensionGithubWorkflowRef(); val != "myWorkflowRef" { t.Fatal("CertExtension does not extract field 'githubWorkflowRef' correctly") } - - if val, ok := exts["1.3.6.1.4.1.57264.1.42"]; !ok || val != "myCustomExtension" { - t.Fatal("CertExtension does not extract field '1.3.6.1.4.1.57264.1.42' correctly") - } }