From 890cec1f43a8ec0754e0dd5a5d120847b63b6c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Batuhan=20Apayd=C4=B1n?= Date: Wed, 29 Jun 2022 19:53:10 +0300 Subject: [PATCH] feat: cert-extensions verify (#1626) 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/dockerfile.go | 33 +++--- cmd/cosign/cli/manifest.go | 33 +++--- cmd/cosign/cli/options/certificate.go | 32 +++++- cmd/cosign/cli/policy_init.go | 3 +- cmd/cosign/cli/verify.go | 78 +++++++------ cmd/cosign/cli/verify/verify.go | 97 ++++++++++++---- cmd/cosign/cli/verify/verify_attestation.go | 46 +++++--- cmd/cosign/cli/verify/verify_blob.go | 16 ++- doc/cosign_dockerfile_verify.md | 5 + doc/cosign_manifest_verify.md | 5 + doc/cosign_verify-attestation.md | 5 + doc/cosign_verify-blob.md | 5 + doc/cosign_verify.md | 5 + pkg/cosign/certextensions.go | 84 ++++++++++++++ pkg/cosign/certextensions_test.go | 66 +++++++++++ pkg/cosign/verifiers.go | 1 + pkg/cosign/verify.go | 75 +++++++++--- pkg/cosign/verify_test.go | 120 ++++++++++++++++++++ pkg/signature/keys.go | 41 ------- pkg/signature/keys_test.go | 41 ------- test/cert_utils.go | 39 ++++++- test/e2e_test.go | 14 +-- 22 files changed, 624 insertions(+), 220 deletions(-) create mode 100644 pkg/cosign/certextensions.go create mode 100644 pkg/cosign/certextensions_test.go diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go index 27e37b2b778..70aa9018672 100644 --- a/cmd/cosign/cli/dockerfile.go +++ b/cmd/cosign/cli/dockerfile.go @@ -85,20 +85,25 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val } v := &dockerfile.VerifyDockerfileCommand{ VerifyCommand: 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, + 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, }, BaseOnly: o.BaseImageOnly, } diff --git a/cmd/cosign/cli/manifest.go b/cmd/cosign/cli/manifest.go index 5854065f43e..a6b0fb0e323 100644 --- a/cmd/cosign/cli/manifest.go +++ b/cmd/cosign/cli/manifest.go @@ -80,20 +80,25 @@ against the transparency log.`, } v := &manifest.VerifyManifestCommand{ VerifyCommand: 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, + 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, }, } return v.Exec(cmd.Context(), args) diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go index d383857c1fe..31c1247a457 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 cc40aa335bf..a754ab9ea76 100644 --- a/cmd/cosign/cli/policy_init.go +++ b/cmd/cosign/cli/policy_init.go @@ -204,7 +204,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 7a365cfd158..08a4fe9bfcb 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) @@ -172,21 +177,26 @@ against the transparency log.`, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { v := verify.VerifyAttestationCommand{ - RegistryOptions: o.Registry, - CheckClaims: o.CheckClaims, - CertRef: o.CertVerify.Cert, - CertEmail: o.CertVerify.CertEmail, - CertOidcIssuer: o.CertVerify.CertOidcIssuer, - CertChain: o.CertVerify.CertChain, - EnforceSCT: o.CertVerify.EnforceSCT, - KeyRef: o.Key, - Sk: o.SecurityKey.Use, - Slot: o.SecurityKey.Slot, - Output: o.Output, - RekorURL: o.Rekor.URL, - PredicateType: o.Predicate.Type, - Policies: o.Policies, - LocalImage: o.LocalImage, + RegistryOptions: o.Registry, + CheckClaims: o.CheckClaims, + CertRef: o.CertVerify.Cert, + CertEmail: o.CertVerify.CertEmail, + CertOidcIssuer: o.CertVerify.CertOidcIssuer, + CertChain: o.CertVerify.CertChain, + CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger, + CertGithubWorkflowSha: o.CertVerify.CertGithubWorkflowSha, + CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, + CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, + CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, + EnforceSCT: o.CertVerify.EnforceSCT, + KeyRef: o.Key, + Sk: o.SecurityKey.Use, + Slot: o.SecurityKey.Slot, + Output: o.Output, + RekorURL: o.Rekor.URL, + PredicateType: o.Predicate.Type, + Policies: o.Policies, + LocalImage: o.LocalImage, } return v.Exec(cmd.Context(), args) }, @@ -258,7 +268,9 @@ The blob may be specified as a path to a file or - for stdin.`, } if err := verify.VerifyBlobCmd(cmd.Context(), ko, o.CertVerify.Cert, o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.CertVerify.CertChain, - o.Signature, args[0], o.CertVerify.EnforceSCT); err != nil { + o.Signature, args[0], o.CertVerify.CertGithubWorkflowTrigger, o.CertVerify.CertGithubWorkflowSha, + o.CertVerify.CertGithubWorkflowName, o.CertVerify.CertGithubWorkflowRepository, o.CertVerify.CertGithubWorkflowRef, + o.CertVerify.EnforceSCT); err != nil { return fmt.Errorf("verifying blob %s: %w", args, err) } return nil diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index d4a5ffa2243..48767e309cf 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 @@ -239,10 +249,30 @@ 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) } + + if githubWorkflowTrigger := ce.GetCertExtensionGithubWorkflowTrigger(); githubWorkflowTrigger != "" { + fmt.Fprintln(os.Stderr, "Certificate extension GitHub Workflow Trigger:", githubWorkflowTrigger) + } + + if githubWorkflowSha := ce.GetExtensionGithubWorkflowSha(); githubWorkflowSha != "" { + fmt.Fprintln(os.Stderr, "Certificate extension GitHub Workflow SHA:", githubWorkflowSha) + } + if githubWorkflowName := ce.GetCertExtensionGithubWorkflowName(); githubWorkflowName != "" { + fmt.Fprintln(os.Stderr, "Certificate extension GitHub Workflow Name:", githubWorkflowName) + } + + if githubWorkflowRepository := ce.GetCertExtensionGithubWorkflowRepository(); githubWorkflowRepository != "" { + fmt.Fprintln(os.Stderr, "Certificate extension GitHub Workflow Trigger", githubWorkflowRepository) + } + + if githubWorkflowRef := ce.GetCertExtensionGithubWorkflowRef(); githubWorkflowRef != "" { + fmt.Fprintln(os.Stderr, "Certificate extension GitHub Workflow Ref:", githubWorkflowRef) + } } p, err := sig.Payload() @@ -269,13 +299,32 @@ 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 } + if githubWorkflowTrigger := ce.GetCertExtensionGithubWorkflowTrigger(); githubWorkflowTrigger != "" { + ss.Optional[cosign.CertExtensionGithubWorkflowTrigger] = githubWorkflowTrigger + } + + if githubWorkflowSha := ce.GetExtensionGithubWorkflowSha(); githubWorkflowSha != "" { + ss.Optional[cosign.CertExtensionGithubWorkflowSha] = githubWorkflowSha + } + if githubWorkflowName := ce.GetCertExtensionGithubWorkflowName(); githubWorkflowName != "" { + ss.Optional[cosign.CertExtensionGithubWorkflowName] = githubWorkflowName + } + + if githubWorkflowRepository := ce.GetCertExtensionGithubWorkflowRepository(); githubWorkflowRepository != "" { + ss.Optional[cosign.CertExtensionGithubWorkflowRepository] = githubWorkflowRepository + } + + if githubWorkflowRef := ce.GetCertExtensionGithubWorkflowRef(); githubWorkflowRef != "" { + ss.Optional[cosign.CertExtensionGithubWorkflowRef] = githubWorkflowRef + } } if bundle, err := sig.Bundle(); err == nil && bundle != nil { if ss.Optional == nil { diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index b330fb8601d..cde32f683cf 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -44,20 +44,25 @@ import ( // nolint type VerifyAttestationCommand 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 - PredicateType string - Policies []string - 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 + PredicateType string + Policies []string + LocalImage bool } // Exec runs the verification command @@ -75,10 +80,15 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return fmt.Errorf("constructing client options: %w", err) } co := &cosign.CheckOpts{ - RegistryClientOpts: ociremoteOpts, - CertEmail: c.CertEmail, - CertOidcIssuer: c.CertOidcIssuer, - EnforceSCT: c.EnforceSCT, + 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, } if c.CheckClaims { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 0d2cffe587a..d49b471a96f 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -61,7 +61,10 @@ func isb64(data []byte) bool { // nolint func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, - certOidcIssuer, certChain, sigRef, blobRef string, enforceSCT bool) error { + certOidcIssuer, certChain, sigRef, blobRef, certGithubWorkflowTrigger, certGithubWorkflowSha, + certGithubWorkflowName, + certGithubWorkflowRepository, + certGithubWorkflowRef string, enforceSCT bool) error { var verifier signature.Verifier var cert *x509.Certificate @@ -106,9 +109,14 @@ func VerifyBlobCmd(ctx context.Context, ko options.KeyOpts, certRef, certEmail, return err } co := &cosign.CheckOpts{ - CertEmail: certEmail, - CertOidcIssuer: certOidcIssuer, - EnforceSCT: enforceSCT, + CertEmail: certEmail, + CertOidcIssuer: certOidcIssuer, + CertGithubWorkflowTrigger: certGithubWorkflowTrigger, + CertGithubWorkflowSha: certGithubWorkflowSha, + CertGithubWorkflowName: certGithubWorkflowName, + CertGithubWorkflowRepository: certGithubWorkflowRepository, + CertGithubWorkflowRef: certGithubWorkflowRef, + EnforceSCT: enforceSCT, } if certChain == "" { err = cosign.CheckCertificatePolicy(cert, co) diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index e852a0c96c3..d8222c51453 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 042b6c7d652..9bccdc40f9c 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 c6aef009fbc..09715d4a0c5 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 ef9c7acc549..416b513568f 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 a5cbb988a64..d85f4a90a5b 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 00000000000..376a2add8bd --- /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/certextensions_test.go b/pkg/cosign/certextensions_test.go new file mode 100644 index 00000000000..d966c958b87 --- /dev/null +++ b/pkg/cosign/certextensions_test.go @@ -0,0 +1,66 @@ +// 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" + "crypto/x509/pkix" + "encoding/asn1" + "testing" +) + +func createCert(t *testing.T) *x509.Certificate { + t.Helper() + return &x509.Certificate{ + Extensions: []pkix.Extension{ + {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, Value: []byte("myIssuer")}, + {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 2}, Value: []byte("myWorkflowTrigger")}, + {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 3}, Value: []byte("myWorkflowSha")}, + {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 4}, Value: []byte("myWorkflowName")}, + {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 5}, Value: []byte("myWorkflowRepository")}, + {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 6}, Value: []byte("myWorkflowRef")}, + }, + } +} + +func TestCertExtensions(t *testing.T) { + t.Parallel() + cert := createCert(t) + exts := CertExtensions{Cert: cert} + + if val := exts.GetIssuer(); val != "myIssuer" { + t.Fatal("CertExtension does not extract field 'oidcIssuer' correctly") + } + + if val := exts.GetCertExtensionGithubWorkflowTrigger(); val != "myWorkflowTrigger" { + t.Fatal("CertExtension does not extract field 'githubWorkflowTrigger' correctly") + } + + if val := exts.GetExtensionGithubWorkflowSha(); val != "myWorkflowSha" { + t.Fatal("CertExtension does not extract field 'githubWorkflowSha' correctly") + } + + if val := exts.GetCertExtensionGithubWorkflowName(); val != "myWorkflowName" { + t.Fatal("CertExtension does not extract field 'githubWorkflowName' correctly") + } + + if val := exts.GetCertExtensionGithubWorkflowRepository(); val != "myWorkflowRepository" { + t.Fatal("CertExtension does not extract field 'githubWorkflowRepository' correctly") + } + + if val := exts.GetCertExtensionGithubWorkflowRef(); val != "myWorkflowRef" { + t.Fatal("CertExtension does not extract field 'githubWorkflowRef' correctly") + } +} diff --git a/pkg/cosign/verifiers.go b/pkg/cosign/verifiers.go index 7e9efea202d..8dbae32f3ba 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/verify.go b/pkg/cosign/verify.go index e1c8be75eee..54bd627377d 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" @@ -74,6 +73,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 @@ -93,6 +93,18 @@ 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 is the GitHub Workflow SHA expected for a certificate to be valid. The empty string means any certificate can be valid. + CertGithubWorkflowSha string + // CertGithubWorkflowName is the GitHub Workflow Name expected for a certificate to be valid. The empty string means any certificate can be valid. + CertGithubWorkflowName string + // CertGithubWorkflowRepository is the GitHub Workflow Repository expected for a certificate to be valid. The empty string means any certificate can be valid. + CertGithubWorkflowRepository string + // CertGithubWorkflowRef is the GitHub Workflow Ref expected for a certificate to be valid. The empty string means any certificate can be valid. + 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 @@ -204,6 +216,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 { @@ -216,11 +229,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 } + issuer := ce.GetIssuer() // 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 { @@ -229,14 +242,13 @@ func CheckCertificatePolicy(cert *x509.Certificate, co *CheckOpts) error { switch { // Check the issuer first case identity.IssuerRegExp != "": - issuer := getIssuer(cert) if regex, err := regexp.Compile(identity.IssuerRegExp); err != nil { return fmt.Errorf("malformed issuer in identity: %s : %w", identity.IssuerRegExp, err) } else if regex.MatchString(issuer) { issuerMatches = true } case identity.Issuer != "": - if identity.Issuer == getIssuer(cert) { + if identity.Issuer == issuer { issuerMatches = true } default: @@ -279,6 +291,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 @@ -297,16 +348,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. diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index 115aba52006..bc6fdc3f905 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -552,6 +552,126 @@ func TestValidateAndUnpackCertInvalidEmail(t *testing.T) { require.Contains(t, err.Error(), "expected email not found in certificate") } +func TestValidateAndUnpackCertInvalidGithubWorkflowTrigger(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + githubWorkFlowTrigger := "myTrigger" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCertWithGitHubOIDs(subject, oidcIssuer, githubWorkFlowTrigger, "", "", "", "", rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + CertEmail: subject, + CertGithubWorkflowTrigger: "otherTrigger", + CertOidcIssuer: oidcIssuer, + } + + _, err := ValidateAndUnpackCert(leafCert, co) + require.Contains(t, err.Error(), "expected GitHub Workflow Trigger not found in certificate") + err = CheckCertificatePolicy(leafCert, co) + require.Contains(t, err.Error(), "expected GitHub Workflow Trigger not found in certificate") +} + +func TestValidateAndUnpackCertInvalidGithubWorkflowSHA(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + githubWorkFlowSha := "mySHA" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCertWithGitHubOIDs(subject, oidcIssuer, "", githubWorkFlowSha, "", "", "", rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + CertEmail: subject, + CertGithubWorkflowSha: "otherSHA", + CertOidcIssuer: oidcIssuer, + } + + _, err := ValidateAndUnpackCert(leafCert, co) + require.Contains(t, err.Error(), "expected GitHub Workflow SHA not found in certificate") + err = CheckCertificatePolicy(leafCert, co) + require.Contains(t, err.Error(), "expected GitHub Workflow SHA not found in certificate") +} + +func TestValidateAndUnpackCertInvalidGithubWorkflowName(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + githubWorkFlowName := "myName" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCertWithGitHubOIDs(subject, oidcIssuer, "", "", githubWorkFlowName, "", "", rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + CertEmail: subject, + CertGithubWorkflowName: "otherName", + CertOidcIssuer: oidcIssuer, + } + + _, err := ValidateAndUnpackCert(leafCert, co) + require.Contains(t, err.Error(), "expected GitHub Workflow Name not found in certificate") + err = CheckCertificatePolicy(leafCert, co) + require.Contains(t, err.Error(), "expected GitHub Workflow Name not found in certificate") +} + +func TestValidateAndUnpackCertInvalidGithubWorkflowRepository(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + githubWorkFlowRepository := "myRepository" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCertWithGitHubOIDs(subject, oidcIssuer, "", "", "", githubWorkFlowRepository, "", rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + CertEmail: subject, + CertGithubWorkflowRepository: "otherRepository", + CertOidcIssuer: oidcIssuer, + } + + _, err := ValidateAndUnpackCert(leafCert, co) + require.Contains(t, err.Error(), "expected GitHub Workflow Repository not found in certificate") + err = CheckCertificatePolicy(leafCert, co) + require.Contains(t, err.Error(), "expected GitHub Workflow Repository not found in certificate") +} + +func TestValidateAndUnpackCertInvalidGithubWorkflowRef(t *testing.T) { + subject := "email@email" + oidcIssuer := "https://accounts.google.com" + githubWorkFlowRef := "myRef" + + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCertWithGitHubOIDs(subject, oidcIssuer, "", "", "", "", githubWorkFlowRef, rootCert, rootKey) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + co := &CheckOpts{ + RootCerts: rootPool, + CertEmail: subject, + CertGithubWorkflowRef: "otherRef", + CertOidcIssuer: oidcIssuer, + } + + _, err := ValidateAndUnpackCert(leafCert, co) + require.Contains(t, err.Error(), "expected GitHub Workflow Ref not found in certificate") + err = CheckCertificatePolicy(leafCert, co) + require.Contains(t, err.Error(), "expected GitHub Workflow Ref not found in certificate") +} + func TestValidateAndUnpackCertWithChainSuccess(t *testing.T) { subject := "email@email" oidcIssuer := "https://accounts.google.com" diff --git a/pkg/signature/keys.go b/pkg/signature/keys.go index 90d3e6d1991..724777735a8 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 5f1c0ac4389..ef5600d104d 100644 --- a/pkg/signature/keys_test.go +++ b/pkg/signature/keys_test.go @@ -16,9 +16,6 @@ package signature import ( "context" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" "os" "testing" @@ -142,41 +139,3 @@ func pass(s string) cosign.PassFunc { return []byte(s), nil } } - -func createCert(t *testing.T) *x509.Certificate { - t.Helper() - return &x509.Certificate{ - Extensions: []pkix.Extension{ - {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, Value: []byte("myIssuer")}, - {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 4}, Value: []byte("myWorkflowName")}, - {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 6}, Value: []byte("myWorkflowRef")}, - {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 42}, Value: []byte("myCustomExtension")}, - }, - } -} - -func TestCertExtensions(t *testing.T) { - t.Parallel() - cert := createCert(t) - exts := CertExtensions(cert) - - if len(exts) != 4 { - t.Fatalf("Unexpected extension-count: %v", len(exts)) - } - - if val, ok := exts["oidcIssuer"]; !ok || val != "myIssuer" { - t.Fatal("CertExtension does not extract field 'oidcIssuer' correctly") - } - - if val, ok := exts["githubWorkflowName"]; !ok || val != "myWorkflowName" { - t.Fatal("CertExtension does not extract field 'githubWorkflowName' correctly") - } - - if val, ok := exts["githubWorkflowRef"]; !ok || 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") - } -} diff --git a/test/cert_utils.go b/test/cert_utils.go index e39e84707c5..6908a5f67e7 100644 --- a/test/cert_utils.go +++ b/test/cert_utils.go @@ -131,7 +131,44 @@ func GenerateLeafCert(subject string, oidcIssuer string, parentTemplate *x509.Ce Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, Critical: false, Value: []byte(oidcIssuer), - }}, + }, + }, + } + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + cert, err := createCertificate(certTemplate, parentTemplate, &priv.PublicKey, parentPriv) + if err != nil { + return nil, nil, err + } + + return cert, priv, nil +} + +func GenerateLeafCertWithGitHubOIDs(subject string, oidcIssuer string, githubWorkflowTrigger, githubWorkflowSha, githubWorkflowName, + githubWorkflowRepository, githubWorkflowRef string, parentTemplate *x509.Certificate, parentPriv crypto.Signer) (*x509.Certificate, *ecdsa.PrivateKey, error) { + certTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + EmailAddresses: []string{subject}, + NotBefore: time.Now().Add(-1 * time.Minute), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + IsCA: false, + ExtraExtensions: []pkix.Extension{{ + // OID for OIDC Issuer extension + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, + Critical: false, + Value: []byte(oidcIssuer), + }, + {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 2}, Value: []byte(githubWorkflowTrigger)}, + {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 3}, Value: []byte(githubWorkflowSha)}, + {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 4}, Value: []byte(githubWorkflowName)}, + {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 5}, Value: []byte(githubWorkflowRepository)}, + {Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 6}, Value: []byte(githubWorkflowRef)}}, } priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) diff --git a/test/e2e_test.go b/test/e2e_test.go index 2a1f22d7fae..79431e00fc7 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -544,8 +544,8 @@ func TestSignBlob(t *testing.T) { KeyRef: pubKeyPath2, } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, "" /*certGithubWorkflowTrigger*/, "" /*certGithubWorkflowName*/, "", "" /*certGithubWorkflowRepository*/, "" /*certGithubWorkflowRef*/, false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, "badsig", blob, "" /*certGithubWorkflowTrigger*/, "" /*certGithubWorkflowName*/, "", "" /*certGithubWorkflowRepository*/, "" /*certGithubWorkflowRef*/, false), t) // Now sign the blob with one key ko := options.KeyOpts{ @@ -557,8 +557,8 @@ func TestSignBlob(t *testing.T) { t.Fatal(err) } // Now verify should work with that one, but not the other - must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, false), t) - mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, false), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, "", "", "", "", "", false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko2, "" /*certRef*/, "" /*certEmail*/, "" /*certOidcIssuer*/, "" /*certChain*/, string(sig), bp, "", "", "", "", "", false), t) } func TestSignBlobBundle(t *testing.T) { @@ -583,7 +583,7 @@ func TestSignBlobBundle(t *testing.T) { BundlePath: bundlePath, } // Verify should fail on a bad input - mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob, false), t) + mustErr(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", blob, "", "", "", "", "", false), t) // Now sign the blob with one key ko := options.KeyOpts{ @@ -596,7 +596,7 @@ func TestSignBlobBundle(t *testing.T) { t.Fatal(err) } // Now verify should work - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, false), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, "", "", "", "", "", false), t) // Now we turn on the tlog and sign again defer setenv(t, options.ExperimentalEnv, "1")() @@ -606,7 +606,7 @@ func TestSignBlobBundle(t *testing.T) { // Point to a fake rekor server to make sure offline verification of the tlog entry works os.Setenv(serverEnv, "notreal") - must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, false), t) + must(cliverify.VerifyBlobCmd(ctx, ko1, "", "", "", "", "", bp, "", "", "", "", "", false), t) } func TestGenerate(t *testing.T) {