From 3663f231cb36fff718abeab11bc17f7867ae38b3 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Tue, 2 Jan 2024 20:20:51 +0100 Subject: [PATCH 01/13] add --certificate-bundle flag to 'cosign verify' Related to issue #3462. Current commit adds the flag to verify the CLI options. The new flag doesn't have any effect yet (will add in follow-up PRs). Signed-off-by: Dmitry S --- cmd/cosign/cli/options/certificate.go | 9 ++++++++- cmd/cosign/cli/verify.go | 1 + cmd/cosign/cli/verify/verify.go | 1 + doc/cosign_dockerfile_verify.md | 3 ++- doc/cosign_manifest_verify.md | 3 ++- doc/cosign_verify-attestation.md | 3 ++- doc/cosign_verify-blob-attestation.md | 3 ++- doc/cosign_verify-blob.md | 3 ++- doc/cosign_verify.md | 3 ++- 9 files changed, 22 insertions(+), 7 deletions(-) diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go index e89f257f9e3..b63fa68ea30 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -33,6 +33,7 @@ type CertVerifyOptions struct { CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string + CertBundle string CertChain string SCT string IgnoreSCT bool @@ -75,12 +76,18 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { 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.CertBundle, "certificate-bundle", "", + "path to a bundle file of CA certificates in PEM format which will be needed "+ + "when building the certificate chains for the signing certificate. Conflicts with --certificate-chain.") + _ = cmd.Flags().SetAnnotation("certificate-bundle", cobra.BashCompFilenameExt, []string{"cert"}) + 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. "+ "Must start with the parent intermediate CA certificate of the "+ - "signing certificate and end with the root certificate") + "signing certificate and end with the root certificate. Conflicts with --certificate-bundle.") _ = cmd.Flags().SetAnnotation("certificate-chain", cobra.BashCompFilenameExt, []string{"cert"}) + cmd.MarkFlagsMutuallyExclusive("certificate-bundle", "certificate-chain") cmd.Flags().StringVar(&o.SCT, "sct", "", "path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. "+ diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index d8c7ed9cd06..af352e34ab1 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -115,6 +115,7 @@ against the transparency log.`, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, + CertBundle: o.CertVerify.CertBundle, CertChain: o.CertVerify.CertChain, IgnoreSCT: o.CertVerify.IgnoreSCT, SCTRef: o.CertVerify.SCT, diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 31cb88c2641..9c48736730b 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -59,6 +59,7 @@ type VerifyCommand struct { CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string + CertBundle string CertChain string CertOidcProvider string IgnoreSCT bool diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index 7817b32a67b..4cf576fadc7 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -56,7 +56,8 @@ cosign dockerfile verify [flags] --attachment-tag-prefix [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] optional custom prefix to use for attached image tags. Attachment images are tagged as: [AttachmentTagPrefix]sha256-[TargetImageDigest].[AttachmentName] --base-image-only only verify the base image (the last FROM image in the Dockerfile) --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. + --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. Conflicts with --certificate-bundle. --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 diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index 39768ed1973..872ca281ab5 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -50,7 +50,8 @@ cosign manifest verify [flags] --attachment string DEPRECATED, related image attachment to verify (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] --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. + --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. Conflicts with --certificate-bundle. --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 diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index e7c26e17f7a..72da3e96e93 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -60,7 +60,8 @@ cosign verify-attestation [flags] --allow-insecure-registry whether to allow insecure connections to registries (e.g., with expired or self-signed TLS certificates). Don't use this for anything but testing --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] --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. + --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. Conflicts with --certificate-bundle. --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 diff --git a/doc/cosign_verify-blob-attestation.md b/doc/cosign_verify-blob-attestation.md index f688ee7e8fc..c383809e37d 100644 --- a/doc/cosign_verify-blob-attestation.md +++ b/doc/cosign_verify-blob-attestation.md @@ -30,7 +30,8 @@ cosign verify-blob-attestation [flags] ``` --bundle string path to bundle FILE --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. + --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. Conflicts with --certificate-bundle. --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 diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index 72089672249..a3fc7428f84 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -60,7 +60,8 @@ cosign verify-blob [flags] ``` --bundle string path to bundle FILE --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. + --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. Conflicts with --certificate-bundle. --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 diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index 5bf88f6de73..39876f24d59 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -73,7 +73,8 @@ cosign verify [flags] --attachment string DEPRECATED, related image attachment to verify (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] --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. + --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. Conflicts with --certificate-bundle. --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 From b6b9371e02cebf90bb9f313ed72fd4a3129909f6 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Tue, 9 Jan 2024 16:04:10 +0100 Subject: [PATCH 02/13] Add --ca-roots flag for 'cosign verify' Add --ca-roots command-line flag for 'cosign verify' to enable verifying cosign signatures using PEM bundles of CA roots. Whether to also add --ca-intermediates flag is TBD. Unit tests will be added in the next commit(s). Fixes #3462. Signed-off-by: Dmitry S --- cmd/cosign/cli/options/certificate.go | 10 +-- cmd/cosign/cli/verify.go | 2 +- cmd/cosign/cli/verify/verify.go | 87 +++++++++++++++++---------- doc/cosign_dockerfile_verify.md | 4 +- doc/cosign_manifest_verify.md | 4 +- doc/cosign_verify-attestation.md | 4 +- doc/cosign_verify-blob-attestation.md | 4 +- doc/cosign_verify-blob.md | 4 +- doc/cosign_verify.md | 4 +- pkg/cosign/verify.go | 10 +++ 10 files changed, 84 insertions(+), 49 deletions(-) diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go index b63fa68ea30..16d60605ed0 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -33,7 +33,7 @@ type CertVerifyOptions struct { CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string - CertBundle string + CARoots string CertChain string SCT string IgnoreSCT bool @@ -76,18 +76,18 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { 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.CertBundle, "certificate-bundle", "", + cmd.Flags().StringVar(&o.CARoots, "ca-roots", "", "path to a bundle file of CA certificates in PEM format which will be needed "+ "when building the certificate chains for the signing certificate. Conflicts with --certificate-chain.") - _ = cmd.Flags().SetAnnotation("certificate-bundle", cobra.BashCompFilenameExt, []string{"cert"}) + _ = cmd.Flags().SetAnnotation("ca-roots", cobra.BashCompFilenameExt, []string{"cert"}) 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. "+ "Must start with the parent intermediate CA certificate of the "+ - "signing certificate and end with the root certificate. Conflicts with --certificate-bundle.") + "signing certificate and end with the root certificate. Conflicts with --ca-roots.") _ = cmd.Flags().SetAnnotation("certificate-chain", cobra.BashCompFilenameExt, []string{"cert"}) - cmd.MarkFlagsMutuallyExclusive("certificate-bundle", "certificate-chain") + cmd.MarkFlagsMutuallyExclusive("ca-roots", "certificate-chain") cmd.Flags().StringVar(&o.SCT, "sct", "", "path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. "+ diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index af352e34ab1..536eb6818a6 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -115,7 +115,7 @@ against the transparency log.`, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, - CertBundle: o.CertVerify.CertBundle, + CARoots: o.CertVerify.CARoots, CertChain: o.CertVerify.CertChain, IgnoreSCT: o.CertVerify.IgnoreSCT, SCTRef: o.CertVerify.SCT, diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 9c48736730b..aa6146fbca3 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -59,7 +59,7 @@ type VerifyCommand struct { CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string - CertBundle string + CARoots string CertChain string CertOidcProvider string IgnoreSCT bool @@ -174,29 +174,47 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } } if keylessVerification(c.KeyRef, c.Sk) { - if c.CertChain != "" { - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - co.RootCerts = x509.NewCertPool() - co.RootCerts.AddCert(chain[len(chain)-1]) - if len(chain) > 1 { - co.IntermediateCerts = x509.NewCertPool() - for _, cert := range chain[:len(chain)-1] { - co.IntermediateCerts.AddCert(cert) + switch { + case c.CertChain != "": + { + chain, err := loadCertChainFromFileOrURL(c.CertChain) + if err != nil { + return err + } + co.RootCerts = x509.NewCertPool() + co.RootCerts.AddCert(chain[len(chain)-1]) + if len(chain) > 1 { + co.IntermediateCerts = x509.NewCertPool() + for _, cert := range chain[:len(chain)-1] { + co.IntermediateCerts.AddCert(cert) + } } } - } else { - // This performs an online fetch of the Fulcio roots. This is needed - // for verifying keyless certificates (both online and offline). - co.RootCerts, err = fulcio.GetRoots() - if err != nil { - return fmt.Errorf("getting Fulcio roots: %w", err) + case c.CARoots != "": + { + caRoots, err := loadCertChainFromFileOrURL(c.CARoots) + if err != nil { + return err + } + co.RootCerts = x509.NewCertPool() + if len(caRoots) > 0 { + for _, cert := range caRoots { + co.RootCerts.AddCert(cert) + } + } } - co.IntermediateCerts, err = fulcio.GetIntermediates() - if err != nil { - return fmt.Errorf("getting Fulcio intermediates: %w", err) + default: + { + // This performs an online fetch of the Fulcio roots. This is needed + // for verifying keyless certificates (both online and offline). + co.RootCerts, err = fulcio.GetRoots() + if err != nil { + return fmt.Errorf("getting Fulcio roots: %w", err) + } + co.IntermediateCerts, err = fulcio.GetIntermediates() + if err != nil { + return fmt.Errorf("getting Fulcio intermediates: %w", err) + } } } } @@ -238,8 +256,8 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { if err != nil { return err } - if c.CertChain == "" { - // If no certChain is passed, the Fulcio root certificate will be used + if c.CertChain == "" && c.CARoots == "" { + // If no certChain and no CARoots are passed, the Fulcio root certificate will be used co.RootCerts, err = fulcio.GetRoots() if err != nil { return fmt.Errorf("getting Fulcio roots: %w", err) @@ -253,14 +271,21 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return err } } else { - // Verify certificate with chain - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - pubKey, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) - if err != nil { - return err + if c.CARoots == "" { + // Verify certificate with chain + chain, err := loadCertChainFromFileOrURL(c.CertChain) + if err != nil { + return err + } + pubKey, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) + if err != nil { + return err + } + } else { + pubKey, err = cosign.ValidateAndUnpackCertWithCertPools(cert, co) + if err != nil { + return err + } } } if c.SCTRef != "" { diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index 4cf576fadc7..af9bb726a96 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -55,9 +55,9 @@ cosign dockerfile verify [flags] --attachment string DEPRECATED, related image attachment to verify (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) + --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --certificate-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. - --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. Conflicts with --certificate-bundle. + --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. Conflicts with --ca-roots. --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 diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index 872ca281ab5..596095a2641 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -49,9 +49,9 @@ cosign manifest verify [flags] -a, --annotations strings extra key=value pairs to sign --attachment string DEPRECATED, related image attachment to verify (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] + --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --certificate-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. - --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. Conflicts with --certificate-bundle. + --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. Conflicts with --ca-roots. --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 diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index 72da3e96e93..a67b2c4c255 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -59,9 +59,9 @@ cosign verify-attestation [flags] --allow-http-registry whether to allow using HTTP protocol while connecting to registries. Don't use this for anything but testing --allow-insecure-registry whether to allow insecure connections to registries (e.g., with expired or self-signed TLS certificates). Don't use this for anything but testing --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] + --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --certificate-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. - --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. Conflicts with --certificate-bundle. + --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. Conflicts with --ca-roots. --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 diff --git a/doc/cosign_verify-blob-attestation.md b/doc/cosign_verify-blob-attestation.md index c383809e37d..33a834c6a0a 100644 --- a/doc/cosign_verify-blob-attestation.md +++ b/doc/cosign_verify-blob-attestation.md @@ -29,9 +29,9 @@ cosign verify-blob-attestation [flags] ``` --bundle string path to bundle FILE + --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --certificate-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. - --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. Conflicts with --certificate-bundle. + --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. Conflicts with --ca-roots. --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 diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index a3fc7428f84..85a6f46fe5a 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -59,9 +59,9 @@ cosign verify-blob [flags] ``` --bundle string path to bundle FILE + --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --certificate-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. - --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. Conflicts with --certificate-bundle. + --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. Conflicts with --ca-roots. --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 diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index 39876f24d59..b6991aaa7a5 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -72,9 +72,9 @@ cosign verify [flags] -a, --annotations strings extra key=value pairs to sign --attachment string DEPRECATED, related image attachment to verify (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] + --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --certificate-bundle string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. - --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. Conflicts with --certificate-bundle. + --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. Conflicts with --ca-roots. --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 diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index abd551d8028..3a41ad59536 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -433,6 +433,16 @@ func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certif return ValidateAndUnpackCert(cert, co) } +// ValidateAndUnpackCertWithCertPools creates a Verifier from a certificate. Verifies that the certificate +// chains up to the provided root. CheckOpts should contain a pool of CA Roots and optionally the Intermediates +// Optionally verifies the subject and issuer of the certificate. +func ValidateAndUnpackCertWithCertPools(cert *x509.Certificate, co *CheckOpts) (signature.Verifier, error) { + if co.RootCerts == nil { + return nil, errors.New("no CA roots provided to validate certificate") + } + return ValidateAndUnpackCert(cert, co) +} + func tlogValidateEntry(ctx context.Context, client *client.Rekor, rekorPubKeys *TrustedTransparencyLogPubKeys, sig oci.Signature, pem []byte) (*models.LogEntryAnon, error) { b64sig, err := sig.Base64Signature() From e333eb849926492bd855c76a000763cbc64029fa Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Mon, 29 Jan 2024 07:23:10 +0100 Subject: [PATCH 03/13] add functional tests for --ca-roots flag Signed-off-by: Dmitry S --- .github/workflows/e2e-tests.yml | 8 + cmd/cosign/cli/options/certificate.go | 10 +- cmd/cosign/cli/verify.go | 5 + cmd/cosign/cli/verify/verify.go | 105 +++--- doc/cosign_dockerfile_verify.md | 3 +- doc/cosign_manifest_verify.md | 3 +- doc/cosign_verify-attestation.md | 3 +- doc/cosign_verify-blob-attestation.md | 3 +- doc/cosign_verify-blob.md | 3 +- doc/cosign_verify.md | 7 +- pkg/cosign/verify.go | 10 - test/e2e_test.go | 446 ++++++++++++++++++++++++++ test/e2e_tsa_certbundle.sh | 86 +++++ test/e2e_tsa_mtls.sh | 102 ++++++ test/gencert/main.go | 228 +++++++++++++ test/helpers.go | 27 ++ 16 files changed, 986 insertions(+), 63 deletions(-) create mode 100755 test/e2e_tsa_certbundle.sh create mode 100755 test/e2e_tsa_mtls.sh create mode 100644 test/gencert/main.go diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index e3479e6847d..d1d640021b7 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -61,6 +61,7 @@ jobs: - name: Run pkcs11 end-to-end tests shell: bash run: ./test/e2e_test_pkcs11.sh +<<<<<<< HEAD e2e-kms: runs-on: ubuntu-latest @@ -213,3 +214,10 @@ jobs: - name: Collect diagnostics if: ${{ failure() }} uses: chainguard-dev/actions/kind-diag@84c993eaf02da1c325854fb272a4df9184bd80fc # main +||||||| parent of 83ab01aa (add functional tests for --ca-roots flag) +======= + + - name: Run e2e_tsa_certbundle.sh + shell: bash + run: make && PATH="$PWD:$PATH" ./test/e2e_tsa_certbundle.sh +>>>>>>> 83ab01aa (add functional tests for --ca-roots flag) diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go index 16d60605ed0..3df7b4b962e 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -33,6 +33,7 @@ type CertVerifyOptions struct { CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string + CAIntermediates string CARoots string CertChain string SCT string @@ -76,6 +77,12 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { 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.CAIntermediates, "ca-intermediates", "", + "path to a file of intermediate CA certificates in PEM format which will be needed "+ + "when building the certificate chains for the signing certificate. "+ + "The flag is optional and must be used together with --ca-roots, conflicts with "+ + "--certificate-chain.") + _ = cmd.Flags().SetAnnotation("ca-intermediates", cobra.BashCompFilenameExt, []string{"cert"}) cmd.Flags().StringVar(&o.CARoots, "ca-roots", "", "path to a bundle file of CA certificates in PEM format which will be needed "+ "when building the certificate chains for the signing certificate. Conflicts with --certificate-chain.") @@ -85,9 +92,10 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { "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. Conflicts with --ca-roots.") + "signing certificate and end with the root certificate. Conflicts with --ca-roots and --ca-intermediates.") _ = cmd.Flags().SetAnnotation("certificate-chain", cobra.BashCompFilenameExt, []string{"cert"}) cmd.MarkFlagsMutuallyExclusive("ca-roots", "certificate-chain") + cmd.MarkFlagsMutuallyExclusive("ca-intermediates", "certificate-chain") cmd.Flags().StringVar(&o.SCT, "sct", "", "path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. "+ diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 536eb6818a6..a2540923082 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -62,6 +62,10 @@ against the transparency log.`, # verify image with local certificate and certificate chain cosign verify --cert cosign.crt --cert-chain chain.crt + # verify image with local certificate and certificate bundles of CA roots + # and (optionally) CA intermediates + cosign verify --cert cosign.crt --ca-roots ca-roots.pem --ca-intermediates ca-intermediates.pem + # verify image using keyless verification with the given certificate # chain and identity parameters, without Fulcio roots (for BYO PKI): cosign verify --cert-chain chain.crt --certificate-oidc-issuer https://issuer.example.com --certificate-identity foo@example.com @@ -115,6 +119,7 @@ against the transparency log.`, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, + CAIntermediates: o.CertVerify.CAIntermediates, CARoots: o.CertVerify.CARoots, CertChain: o.CertVerify.CertChain, IgnoreSCT: o.CertVerify.IgnoreSCT, diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index aa6146fbca3..010cef4afb4 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -59,6 +59,7 @@ type VerifyCommand struct { CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string + CAIntermediates string CARoots string CertChain string CertOidcProvider string @@ -176,45 +177,51 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { if keylessVerification(c.KeyRef, c.Sk) { switch { case c.CertChain != "": - { - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - co.RootCerts = x509.NewCertPool() - co.RootCerts.AddCert(chain[len(chain)-1]) - if len(chain) > 1 { - co.IntermediateCerts = x509.NewCertPool() - for _, cert := range chain[:len(chain)-1] { - co.IntermediateCerts.AddCert(cert) - } + chain, err := loadCertChainFromFileOrURL(c.CertChain) + if err != nil { + return err + } + co.RootCerts = x509.NewCertPool() + co.RootCerts.AddCert(chain[len(chain)-1]) + if len(chain) > 1 { + co.IntermediateCerts = x509.NewCertPool() + for _, cert := range chain[:len(chain)-1] { + co.IntermediateCerts.AddCert(cert) } } case c.CARoots != "": - { - caRoots, err := loadCertChainFromFileOrURL(c.CARoots) + caRoots, err := loadCertChainFromFileOrURL(c.CARoots) + if err != nil { + return err + } + co.RootCerts = x509.NewCertPool() + if len(caRoots) > 0 { + for _, cert := range caRoots { + co.RootCerts.AddCert(cert) + } + } + if c.CAIntermediates != "" { + caIntermediates, err := loadCertChainFromFileOrURL(c.CAIntermediates) if err != nil { return err } - co.RootCerts = x509.NewCertPool() - if len(caRoots) > 0 { - for _, cert := range caRoots { - co.RootCerts.AddCert(cert) + if len(caIntermediates) > 0 { + co.IntermediateCerts = x509.NewCertPool() + for _, cert := range caIntermediates { + co.IntermediateCerts.AddCert(cert) } } } default: - { - // This performs an online fetch of the Fulcio roots. This is needed - // for verifying keyless certificates (both online and offline). - co.RootCerts, err = fulcio.GetRoots() - if err != nil { - return fmt.Errorf("getting Fulcio roots: %w", err) - } - co.IntermediateCerts, err = fulcio.GetIntermediates() - if err != nil { - return fmt.Errorf("getting Fulcio intermediates: %w", err) - } + // This performs an online fetch of the Fulcio roots from a TUF repository. + // This is needed for verifying keyless certificates (both online and offline). + co.RootCerts, err = fulcio.GetRoots() + if err != nil { + return fmt.Errorf("getting Fulcio roots: %w", err) + } + co.IntermediateCerts, err = fulcio.GetIntermediates() + if err != nil { + return fmt.Errorf("getting Fulcio intermediates: %w", err) } } } @@ -256,7 +263,8 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { if err != nil { return err } - if c.CertChain == "" && c.CARoots == "" { + switch { + case c.CertChain == "" && co.RootCerts == nil: // If no certChain and no CARoots are passed, the Fulcio root certificate will be used co.RootCerts, err = fulcio.GetRoots() if err != nil { @@ -270,24 +278,26 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { if err != nil { return err } - } else { - if c.CARoots == "" { - // Verify certificate with chain - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - pubKey, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) - if err != nil { - return err - } - } else { - pubKey, err = cosign.ValidateAndUnpackCertWithCertPools(cert, co) - if err != nil { - return err - } + case c.CertChain != "": + // Verify certificate with chain + chain, err := loadCertChainFromFileOrURL(c.CertChain) + if err != nil { + return err + } + pubKey, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) + if err != nil { + return err + } + case co.RootCerts != nil: + // Verify certificate with root (and if given, intermediate) certificate + pubKey, err = cosign.ValidateAndUnpackCert(cert, co) + if err != nil { + return err } + default: + return errors.New("internal error in handling CertChain and RootCerts - default case should never happen") } + if c.SCTRef != "" { sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) if err != nil { @@ -295,6 +305,9 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } co.SCT = sct } + default: + // Do nothing. Neither keyRef, c.Sk, nor certRef were set - can happen for example when using Fulcio and TSA. + // For an example see the TestAttachWithRFC3161Timestamp test in test/e2e_test.go. } co.SigVerifier = pubKey diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index af9bb726a96..aea4d4aadde 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -55,9 +55,10 @@ cosign dockerfile verify [flags] --attachment string DEPRECATED, related image attachment to verify (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) + --ca-intermediates string path to a file of intermediate CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. The flag is optional and must be used together with --ca-roots, conflicts with --certificate-chain. --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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. Conflicts with --ca-roots. + --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. Conflicts with --ca-roots and --ca-intermediates. --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 diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index 596095a2641..dc1af148c48 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -49,9 +49,10 @@ cosign manifest verify [flags] -a, --annotations strings extra key=value pairs to sign --attachment string DEPRECATED, related image attachment to verify (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] + --ca-intermediates string path to a file of intermediate CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. The flag is optional and must be used together with --ca-roots, conflicts with --certificate-chain. --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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. Conflicts with --ca-roots. + --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. Conflicts with --ca-roots and --ca-intermediates. --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 diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index a67b2c4c255..9b747ab9ef0 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -59,9 +59,10 @@ cosign verify-attestation [flags] --allow-http-registry whether to allow using HTTP protocol while connecting to registries. Don't use this for anything but testing --allow-insecure-registry whether to allow insecure connections to registries (e.g., with expired or self-signed TLS certificates). Don't use this for anything but testing --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] + --ca-intermediates string path to a file of intermediate CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. The flag is optional and must be used together with --ca-roots, conflicts with --certificate-chain. --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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. Conflicts with --ca-roots. + --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. Conflicts with --ca-roots and --ca-intermediates. --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 diff --git a/doc/cosign_verify-blob-attestation.md b/doc/cosign_verify-blob-attestation.md index 33a834c6a0a..f9d199ec45d 100644 --- a/doc/cosign_verify-blob-attestation.md +++ b/doc/cosign_verify-blob-attestation.md @@ -29,9 +29,10 @@ cosign verify-blob-attestation [flags] ``` --bundle string path to bundle FILE + --ca-intermediates string path to a file of intermediate CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. The flag is optional and must be used together with --ca-roots, conflicts with --certificate-chain. --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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. Conflicts with --ca-roots. + --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. Conflicts with --ca-roots and --ca-intermediates. --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 diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index 85a6f46fe5a..e4fb5c16a22 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -59,9 +59,10 @@ cosign verify-blob [flags] ``` --bundle string path to bundle FILE + --ca-intermediates string path to a file of intermediate CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. The flag is optional and must be used together with --ca-roots, conflicts with --certificate-chain. --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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. Conflicts with --ca-roots. + --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. Conflicts with --ca-roots and --ca-intermediates. --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 diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index b6991aaa7a5..0852f21fc81 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -38,6 +38,10 @@ cosign verify [flags] # verify image with local certificate and certificate chain cosign verify --cert cosign.crt --cert-chain chain.crt + # verify image with local certificate and certificate bundles of CA roots + # and (optionally) CA intermediates + cosign verify --cert cosign.crt --ca-roots ca-roots.pem --ca-intermediates ca-intermediates.pem + # verify image using keyless verification with the given certificate # chain and identity parameters, without Fulcio roots (for BYO PKI): cosign verify --cert-chain chain.crt --certificate-oidc-issuer https://issuer.example.com --certificate-identity foo@example.com @@ -72,9 +76,10 @@ cosign verify [flags] -a, --annotations strings extra key=value pairs to sign --attachment string DEPRECATED, related image attachment to verify (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] + --ca-intermediates string path to a file of intermediate CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. The flag is optional and must be used together with --ca-roots, conflicts with --certificate-chain. --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --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. Conflicts with --ca-roots. + --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. Conflicts with --ca-roots and --ca-intermediates. --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 diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 3a41ad59536..abd551d8028 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -433,16 +433,6 @@ func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certif return ValidateAndUnpackCert(cert, co) } -// ValidateAndUnpackCertWithCertPools creates a Verifier from a certificate. Verifies that the certificate -// chains up to the provided root. CheckOpts should contain a pool of CA Roots and optionally the Intermediates -// Optionally verifies the subject and issuer of the certificate. -func ValidateAndUnpackCertWithCertPools(cert *x509.Certificate, co *CheckOpts) (signature.Verifier, error) { - if co.RootCerts == nil { - return nil, errors.New("no CA roots provided to validate certificate") - } - return ValidateAndUnpackCert(cert, co) -} - func tlogValidateEntry(ctx context.Context, client *client.Rekor, rekorPubKeys *TrustedTransparencyLogPubKeys, sig oci.Signature, pem []byte) (*models.LogEntryAnon, error) { b64sig, err := sig.Base64Signature() diff --git a/test/e2e_test.go b/test/e2e_test.go index da874ae329f..d6205305b2d 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -21,7 +21,12 @@ import ( "bytes" "context" "crypto" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/base64" "encoding/json" + "encoding/pem" "fmt" "io" "net/http" @@ -33,6 +38,7 @@ import ( "testing" "time" + "github.com/go-openapi/strfmt" "github.com/google/go-cmp/cmp" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" @@ -55,6 +61,8 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" cliverify "github.com/sigstore/cosign/v2/cmd/cosign/cli/verify" "github.com/sigstore/cosign/v2/internal/pkg/cosign/fulcio/fulcioroots" + "github.com/sigstore/cosign/v2/internal/pkg/cosign/tsa" + "github.com/sigstore/cosign/v2/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/bundle" "github.com/sigstore/cosign/v2/pkg/cosign/env" @@ -791,6 +799,348 @@ func TestAttestationRFC3161Timestamp(t *testing.T) { must(verifyAttestation.Exec(ctx, []string{imgName}), t) } +func TestVerifyWithCARoots(t *testing.T) { + ctx := context.Background() + // TSA server needed to create timestamp + viper.Set("timestamp-signer", "memory") + viper.Set("timestamp-signer-hash", "sha256") + apiServer := server.NewRestAPIServer("localhost", 0, []string{"http"}, false, 10*time.Second, 10*time.Second) + server := httptest.NewServer(apiServer.GetHandler()) + t.Cleanup(server.Close) + + repo, stop := reg(t) + defer stop() + td := t.TempDir() + + imgName := path.Join(repo, "cosign-verify-caroots-e2e") + + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + b := bytes.Buffer{} + must(generate.GenerateCmd(context.Background(), options.RegistryOptions{}, imgName, nil, &b), t) + + rootCert, rootKey, _ := GenerateRootCa() + subCert, subKey, _ := GenerateSubordinateCa(rootCert, rootKey) + leafCert, privKey, _ := GenerateLeafCert("subject@mail.com", "oidc-issuer", subCert, subKey) + + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + rootCert02, rootKey02, _ := GenerateRootCa() + subCert02, subKey02, _ := GenerateSubordinateCa(rootCert02, rootKey02) + leafCert02, _, _ := GenerateLeafCert("subject02@mail.com", "oidc-issuer02", subCert02, subKey02) + pemRoot02 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert02.Raw}) + pemSub02 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert02.Raw}) + pemLeaf02 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert02.Raw}) + pemsubRef02 := mkfile(string(pemSub02), td, t) + pemrootRef02 := mkfile(string(pemRoot02), td, t) + pemleafRef02 := mkfile(string(pemLeaf02), td, t) + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootCert) + + payloadref := mkfile(b.String(), td, t) + + h := sha256.Sum256(b.Bytes()) + signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) + b64signature := base64.StdEncoding.EncodeToString([]byte(signature)) + sigRef := mkfile(b64signature, td, t) + pemsubRef := mkfile(string(pemSub), td, t) + pemrootRef := mkfile(string(pemRoot), td, t) + pemleafRef := mkfile(string(pemLeaf), td, t) + certchainRef := mkfile(string(append(pemSub[:], pemRoot[:]...)), td, t) + + pemrootBundleRef := mkfile(string(append(pemRoot[:], pemRoot02[:]...)), td, t) + pemsubBundleRef := mkfile(string(append(pemSub[:], pemSub02[:]...)), td, t) + + tsclient, err := tsaclient.GetTimestampClient(server.URL) + if err != nil { + t.Error(err) + } + + chain, err := tsclient.Timestamp.GetTimestampCertChain(nil) + if err != nil { + t.Fatalf("unexpected error getting timestamp chain: %v", err) + } + + tsaChainRef, err := os.CreateTemp(os.TempDir(), "tempfile") + if err != nil { + t.Fatalf("error creating temp file: %v", err) + } + defer os.Remove(tsaChainRef.Name()) + _, err = tsaChainRef.WriteString(chain.Payload) + if err != nil { + t.Fatalf("error writing chain payload to temp file: %v", err) + } + + tsBytes, err := tsa.GetTimestampedSignature(signature, client.NewTSAClient(server.URL+"/api/v1/timestamp")) + if err != nil { + t.Fatalf("unexpected error creating timestamp: %v", err) + } + rfc3161TSRef := mkfile(string(tsBytes), td, t) + + // Upload it! + err = attach.SignatureCmd(ctx, options.RegistryOptions{}, sigRef, payloadref, pemleafRef, certchainRef, rfc3161TSRef, "", imgName) + if err != nil { + t.Fatal(err) + } + + // the following fields with non-changing values are logically "factored out" for brevity + // and passed to verifyKeylessTSAWithCARoots in the testing loop: + // imageName string + // tsaCertChainRef string + // skipSCT bool + // skipTlogVerify bool + tests := []struct { + name string + rootRef string + subRef string + leafRef string + wantError bool + }{ + { + "verify with root, intermediate and leaf certificates", + pemrootRef, + pemsubRef, + pemleafRef, + false, + }, + // NB - "confusely" switching the root and intermediate PEM files does _NOT_ (currently) produce an error + // - the Go crypto/x509 package doesn't strictly verify that the certificate chain is anchored + // in a self-signed root certificate. In this case, only the chain up to the intermediate + // certificate is verified, and the root certificate is ignored. + // See also https://gist.github.com/dmitris/15160f703b3038b1b00d03d3c7b66ce0 and in particular + // https://gist.github.com/dmitris/15160f703b3038b1b00d03d3c7b66ce0#file-main-go-L133-L135 as an example. + { + "switch root and intermediate no error", + pemsubRef, + pemrootRef, + pemleafRef, + false, + }, + { + "leave out the root certificate", + "", + pemsubRef, + pemleafRef, + true, + }, + { + "leave out the intermediate certificate", + pemrootRef, + "", + pemleafRef, + true, + }, + { + "leave out the codesigning leaf certificate which is extracted from the image", + pemrootRef, + pemsubRef, + "", + false, + }, + { + "wrong leaf certificate", + pemrootRef, + pemsubRef, + pemleafRef02, + true, + }, + { + "root and intermediates bundles", + pemrootBundleRef, + pemsubBundleRef, + pemleafRef, + false, + }, + { + "wrong root and intermediates bundles", + pemrootRef02, + pemsubRef02, + pemleafRef, + true, + }, + { + "wrong root undle", + pemrootRef02, + pemsubBundleRef, + pemleafRef, + true, + }, + { + "wrong intermediates bundle", + pemrootRef, + pemsubRef02, + pemleafRef, + true, + }, + } + for _, tt := range tests { + err := verifyKeylessTSAWithCARoots(imgName, + tt.rootRef, + tt.subRef, + tt.leafRef, + tsaChainRef.Name(), + true, + true) + hasErr := (err != nil) + if hasErr != tt.wantError { + if tt.wantError { + t.Errorf("%s - no expected error", tt.name) + } else { + t.Errorf("%s - unexpected error: %v", tt.name, err) + } + } + } +} + +func TestAttachWithRFC3161Timestamp(t *testing.T) { + ctx := context.Background() + // TSA server needed to create timestamp + viper.Set("timestamp-signer", "memory") + viper.Set("timestamp-signer-hash", "sha256") + apiServer := server.NewRestAPIServer("localhost", 0, []string{"http"}, false, 10*time.Second, 10*time.Second) + server := httptest.NewServer(apiServer.GetHandler()) + t.Cleanup(server.Close) + + repo, stop := reg(t) + defer stop() + td := t.TempDir() + + imgName := path.Join(repo, "cosign-attach-timestamp-e2e") + + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + b := bytes.Buffer{} + must(generate.GenerateCmd(context.Background(), options.RegistryOptions{}, imgName, nil, &b), t) + + rootCert, rootKey, _ := GenerateRootCa() + subCert, subKey, _ := GenerateSubordinateCa(rootCert, rootKey) + leafCert, privKey, _ := GenerateLeafCert("subject@mail.com", "oidc-issuer", subCert, subKey) + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + payloadref := mkfile(b.String(), td, t) + + h := sha256.Sum256(b.Bytes()) + signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) + b64signature := base64.StdEncoding.EncodeToString([]byte(signature)) + sigRef := mkfile(b64signature, td, t) + pemleafRef := mkfile(string(pemLeaf), td, t) + pemrootRef := mkfile(string(pemRoot), td, t) + + certchainRef := mkfile(string(append(pemSub[:], pemRoot[:]...)), td, t) + + t.Setenv("SIGSTORE_ROOT_FILE", pemrootRef) + // reset the roots to use the root pointed by the environment variable SIGSTORE_ROOT_FILE + if err := fulcioroots.ReInit(); err != nil { + t.Fatal(err) + } + + tsclient, err := tsaclient.GetTimestampClient(server.URL) + if err != nil { + t.Error(err) + } + + chain, err := tsclient.Timestamp.GetTimestampCertChain(nil) + if err != nil { + t.Fatalf("unexpected error getting timestamp chain: %v", err) + } + + file, err := os.CreateTemp(os.TempDir(), "tempfile") + if err != nil { + t.Fatalf("error creating temp file: %v", err) + } + defer os.Remove(file.Name()) + _, err = file.WriteString(chain.Payload) + if err != nil { + t.Fatalf("error writing chain payload to temp file: %v", err) + } + + tsBytes, err := tsa.GetTimestampedSignature(signature, client.NewTSAClient(server.URL+"/api/v1/timestamp")) + if err != nil { + t.Fatalf("unexpected error creating timestamp: %v", err) + } + rfc3161TSRef := mkfile(string(tsBytes), td, t) + + // Upload it! + err = attach.SignatureCmd(ctx, options.RegistryOptions{}, sigRef, payloadref, pemleafRef, certchainRef, rfc3161TSRef, "", imgName) + if err != nil { + t.Fatal(err) + } + + must(verifyKeylessTSA(imgName, file.Name(), true, true), t) +} + +func TestAttachWithRekorBundle(t *testing.T) { + ctx := context.Background() + + repo, stop := reg(t) + defer stop() + td := t.TempDir() + + imgName := path.Join(repo, "cosign-attach-timestamp-e2e") + + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + b := bytes.Buffer{} + must(generate.GenerateCmd(context.Background(), options.RegistryOptions{}, imgName, nil, &b), t) + + rootCert, rootKey, _ := GenerateRootCa() + subCert, subKey, _ := GenerateSubordinateCa(rootCert, rootKey) + leafCert, privKey, _ := GenerateLeafCert("subject@mail.com", "oidc-issuer", subCert, subKey) + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + payloadref := mkfile(b.String(), td, t) + + h := sha256.Sum256(b.Bytes()) + signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) + b64signature := base64.StdEncoding.EncodeToString([]byte(signature)) + sigRef := mkfile(b64signature, td, t) + pemleafRef := mkfile(string(pemLeaf), td, t) + pemrootRef := mkfile(string(pemRoot), td, t) + + t.Setenv("SIGSTORE_ROOT_FILE", pemrootRef) + + certchainRef := mkfile(string(append(pemSub[:], pemRoot[:]...)), td, t) + + localPayload := cosign.LocalSignedPayload{ + Base64Signature: b64signature, + Cert: string(pemLeaf), + Bundle: &bundle.RekorBundle{ + SignedEntryTimestamp: strfmt.Base64("MEUCIEDcarEwRYkrxE9ne+kzEVvUhnWaauYzxhUyXOLy1hwAAiEA4VdVCvNRs+D/5o33C2KBy+q2YX3lP4Y7nqRFU+K3hi0="), + Payload: bundle.RekorPayload{ + Body: "REMOVED", + IntegratedTime: 1631646761, + LogIndex: 693591, + LogID: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", + }, + }, + } + + jsonBundle, err := json.Marshal(localPayload) + if err != nil { + t.Fatal(err) + } + bundlePath := filepath.Join(td, "bundle.json") + if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil { + t.Fatal(err) + } + + // Upload it! + err = attach.SignatureCmd(ctx, options.RegistryOptions{}, sigRef, payloadref, pemleafRef, certchainRef, "", bundlePath, imgName) + if err != nil { + t.Fatal(err) + } +} + func TestRekorBundle(t *testing.T) { td := t.TempDir() err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td) @@ -1422,6 +1772,102 @@ func TestGenerate(t *testing.T) { equals(ss.Optional["foo"], "bar", t) } +func TestUploadDownload(t *testing.T) { + repo, stop := reg(t) + defer stop() + td := t.TempDir() + ctx := context.Background() + + testCases := map[string]struct { + signature string + signatureType attach.SignatureArgType + expectedErr bool + }{ + "stdin containing signature": { + signature: "testsignatureraw", + signatureType: attach.StdinSignature, + expectedErr: false, + }, + "file containing signature": { + signature: "testsignaturefile", + signatureType: attach.FileSignature, + expectedErr: false, + }, + "raw signature as argument": { + signature: "testsignatureraw", + signatureType: attach.RawSignature, + expectedErr: true, + }, + "empty signature as argument": { + signature: "", + signatureType: attach.RawSignature, + expectedErr: true, + }, + } + + imgName := path.Join(repo, "cosign-e2e") + for testName, testCase := range testCases { + t.Run(testName, func(t *testing.T) { + ref, _, cleanup := mkimage(t, imgName) + payload := "testpayload" + payloadPath := mkfile(payload, td, t) + signature := base64.StdEncoding.EncodeToString([]byte(testCase.signature)) + restoreStdin := func() {} + + var sigRef string + if testCase.signatureType == attach.FileSignature { + sigRef = mkfile(signature, td, t) + } else if testCase.signatureType == attach.StdinSignature { + sigRef = "-" + restoreStdin = mockStdin(signature, td, t) + } else { + sigRef = signature + } + // Upload it! + err := attach.SignatureCmd(ctx, options.RegistryOptions{}, sigRef, payloadPath, "", "", "", "", imgName) + if testCase.expectedErr { + mustErr(err, t) + } else { + must(err, t) + } + restoreStdin() + + // Now download it! + se, err := ociremote.SignedEntity(ref, ociremote.WithRemoteOptions(registryClientOpts(ctx)...)) + must(err, t) + sigs, err := se.Signatures() + must(err, t) + signatures, err := sigs.Get() + must(err, t) + + if testCase.expectedErr { + if len(signatures) != 0 { + t.Fatalf("unexpected signatures %d, wanted 0", len(signatures)) + } + } else { + if len(signatures) != 1 { + t.Fatalf("unexpected signatures %d, wanted 1", len(signatures)) + } + + if b64sig, err := signatures[0].Base64Signature(); err != nil { + t.Fatalf("Base64Signature() = %v", err) + } else if diff := cmp.Diff(b64sig, signature); diff != "" { + t.Error(diff) + } + + if p, err := signatures[0].Payload(); err != nil { + t.Fatalf("Payload() = %v", err) + } else if diff := cmp.Diff(p, []byte(payload)); diff != "" { + t.Error(diff) + } + } + + // Now delete it! + cleanup() + }) + } +} + func TestSaveLoad(t *testing.T) { td := t.TempDir() err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td) diff --git a/test/e2e_tsa_certbundle.sh b/test/e2e_tsa_certbundle.sh new file mode 100755 index 00000000000..cfe3d84b704 --- /dev/null +++ b/test/e2e_tsa_certbundle.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# +# Copyright 2024 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. + +# This test was done for https://github.com/sigstore/cosign/pull/3464 +# to verify that 'cosign verify' can work with the '--ca-roots' +# command-line option. It is a modified copy of e2e_tsa_mtls.sh. + +set -exuo pipefail + +## Requirements +# - cosign +# - crane +# - go + +CERT_BASE="test/testdata" + +export TIMESTAMP_SERVER_URL=https://freetsa.org/tsr +TIMESTAMP_CHAIN_FILE="timestamp-chain" +curl -s https://www.freetsa.org/files/cacert.pem > $TIMESTAMP_CHAIN_FILE +echo "TIMESTAMP_CHAIN_FILE: $(ls -l $TIMESTAMP_CHAIN_FILE)" +COSIGN_CLI=./cosign + +# unlike e2e_tsa_mtls.sh, there is no option to pass an image as a command-line parameter. + +# Upload an image to ttl.sh - commands from https://docs.sigstore.dev/cosign/keyless/ +SRC_IMAGE=busybox +SRC_DIGEST=$(crane digest busybox) +for i in 01 02; do + IMAGE_URI_TEMP=ttl.sh/$(uuidgen | head -c 8 | tr 'A-Z' 'a-z') + crane cp $SRC_IMAGE@$SRC_DIGEST "${IMAGE_URI_TEMP}:3h" + declare "IMG${i}=${IMAGE_URI_TEMP}@${SRC_DIGEST}" +done + + +echo "IMG01: $IMG01, IMG02: $IMG02, TIMESTAMP_SERVER_URL: $TIMESTAMP_SERVER_URL" + +# use gencert to generate two CAs (for testing certificate bundle feature), +# keys and certificates +echo "generate CAs, keys and certificates with gencert" +passwd=$(uuidgen | head -c 32 | tr 'A-Z' 'a-z') +rm -f *.pem import-cosign.* +for i in 01 02; do + go run test/gencert/main.go -output-suffix "$i" -intermediate + COSIGN_PASSWORD="$passwd" $COSIGN_CLI import-key-pair --key ca-intermediate-key${i}.pem --output-key-prefix import-cosign${i} + IMG="IMG${i}" + cat ca-intermediate${i}.pem ca-root${i}.pem > certchain${i}.pem + COSIGN_PASSWORD="$passwd" $COSIGN_CLI sign --timestamp-server-url "${TIMESTAMP_SERVER_URL}" \ + --upload=true --tlog-upload=false --key import-cosign${i}.key --certificate-chain certchain${i}.pem --cert cert${i}.pem "${!IMG}" + # key is now longer needed + rm -f key${i}.pem import-cosign${i}.* +done + +# create a certificate bundle - concatenate both generated CA certificates +ls -l *.pem +cat ca-root01.pem ca-root02.pem > ca-roots.pem +cat ca-intermediate01.pem ca-intermediate02.pem > ca-intermediates.pem + +echo "cosign verify:" +for i in 01 02; do + IMG="IMG${i}" + # first try with --certificate-chain parameter + $COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \ + --certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \ + --certificate-chain certchain${i}.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE "${!IMG}" + + # then do the same but now with --ca-roots and --ca-intermediates parameters + $COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \ + --certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \ + --ca-roots ca-roots.pem --ca-intermediates ca-intermediates.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE "${!IMG}" +done + +# cleanup +rm -fr *.pem timestamp-chain diff --git a/test/e2e_tsa_mtls.sh b/test/e2e_tsa_mtls.sh new file mode 100755 index 00000000000..cc06eb04e5b --- /dev/null +++ b/test/e2e_tsa_mtls.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# +# Copyright 2023 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. + +set -exuo pipefail + +## Requirements +# - cosign +# - crane +# - go + +CERT_BASE="test/testdata" + +# the certificates listed below are generated with the `gen-tsa-mtls-certs.sh` script. +TIMESTAMP_CACERT=$CERT_BASE/tsa-mtls-ca.crt +TIMESTAMP_CLIENT_CERT=$CERT_BASE/tsa-mtls-client.crt +TIMESTAMP_CLIENT_KEY=$CERT_BASE/tsa-mtls-client.key +TIMESTAMP_SERVER_CERT=$CERT_BASE/tsa-mtls-server.crt +TIMESTAMP_SERVER_KEY=$CERT_BASE/tsa-mtls-server.key +TIMESTAMP_SERVER_NAME="server.example.com" +TIMESTAMP_SERVER_URL=https://localhost:3000/api/v1/timestamp +TIMESTAMP_CHAIN_FILE="timestamp-chain" + +set +e +COSIGN_CLI=./cosign +command -v timestamp-server >& /dev/null +exit_code=$? +set -e +if [[ $exit_code != 0 ]]; then + rm -fr /tmp/timestamp-authority + git clone https://github.com/sigstore/timestamp-authority /tmp/timestamp-authority + pushd /tmp/timestamp-authority + make + export PATH="/tmp/timestamp-authority/bin:$PATH" + popd +fi + +timestamp-server serve --disable-ntp-monitoring --tls-host 0.0.0.0 --tls-port 3000 \ + --scheme https --tls-ca $TIMESTAMP_CACERT --tls-key $TIMESTAMP_SERVER_KEY \ + --tls-certificate $TIMESTAMP_SERVER_CERT & + +sleep 1 +curl -k -s --key test/testdata/tsa-mtls-client.key \ + --cert test/testdata/tsa-mtls-client.crt \ + --cacert test/testdata/tsa-mtls-ca.crt https://localhost:3000/api/v1/timestamp/certchain \ + > $TIMESTAMP_CHAIN_FILE +echo "DONE: $(ls -l $TIMESTAMP_CHAIN_FILE)" + +IMG=${IMAGE_URI_DIGEST:-} +if [[ "$#" -ge 1 ]]; then + IMG=$1 +elif [[ -z "${IMG}" ]]; then + # Upload an image to ttl.sh - commands from https://docs.sigstore.dev/cosign/keyless/ + SRC_IMAGE=busybox + SRC_DIGEST=$(crane digest busybox) + IMAGE_URI=ttl.sh/$(uuidgen | head -c 8 | tr 'A-Z' 'a-z') + crane cp $SRC_IMAGE@$SRC_DIGEST $IMAGE_URI:3h + IMG=$IMAGE_URI@$SRC_DIGEST +fi + +echo "IMG (IMAGE_URI_DIGEST): $IMG, TIMESTAMP_SERVER_URL: $TIMESTAMP_SERVER_URL" + +rm -f *.pem import-cosign.* key.pem + +# use gencert to generate CA, keys and certificates +echo "generate keys and certificates with gencert" + +passwd=$(uuidgen | head -c 32 | tr 'A-Z' 'a-z') +rm -f *.pem import-cosign.* && go run test/gencert/main.go && COSIGN_PASSWORD="$passwd" $COSIGN_CLI import-key-pair --key key.pem + +COSIGN_PASSWORD="$passwd" $COSIGN_CLI sign --timestamp-server-url "${TIMESTAMP_SERVER_URL}" \ + --timestamp-client-cacert ${TIMESTAMP_CACERT} --timestamp-client-cert ${TIMESTAMP_CLIENT_CERT} \ + --timestamp-client-key ${TIMESTAMP_CLIENT_KEY} --timestamp-server-name ${TIMESTAMP_SERVER_NAME}\ + --upload=true --tlog-upload=false --key import-cosign.key --certificate-chain ca-root.pem --cert cert.pem $IMG + +# key is now longer needed +rm -f key.pem import-cosign.* + +echo "cosign verify:" +$COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \ + --certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \ + --certificate-chain ca-root.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE $IMG +# alternative invocation of 'cosign verify' using '--ca-roots' instead of '--certificate-chain' +$COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \ + --certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \ + --ca-roots ca-root.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE $IMG + +# cleanup +rm -fr ca-key.pem ca-root.pem cert.pem timestamp-chain /tmp/timestamp-authority +pkill -f 'timestamp-server' diff --git a/test/gencert/main.go b/test/gencert/main.go new file mode 100644 index 00000000000..76730241e35 --- /dev/null +++ b/test/gencert/main.go @@ -0,0 +1,228 @@ +// Copyright 2023 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. + +// code is based on the snippet https://gist.github.com/shaneutt/5e1995295cff6721c89a71d13a71c251 +// with a permissive (Public Domain) License. + +// TODO(dmitris) - refactor to move the code generating certificates and writing to files +// to a separate function. + +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "flag" + "log" + "math/big" + "net" + "os" + "time" +) + +func main() { + var genIntermediate = flag.Bool("intermediate", false, "generate intermediate CA") + var outputSuffix = flag.String("output-suffix", "", "suffix to append to generated files before the extension") + flag.Parse() + + // set up our CA certificate + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"CA Company, INC."}, + OrganizationalUnit: []string{"CA Root Team"}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{"Golden Gate Bridge"}, + PostalCode: []string{"94016"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning /*, x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth */}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + EmailAddresses: []string{"ca@example.com"}, + } + + // create our private and public key + caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + log.Fatal(err) + } + + // create the CA + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + log.Fatal(err) + } + + // pem encode + fname := "ca-root" + *outputSuffix + ".pem" + caCertFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + log.Fatalf("unable to write to %s: %v", fname, err) + } + err = pem.Encode(caCertFile, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + if err != nil { + log.Fatalf("unable to write to %s: %v", fname, err) + } + fname = "ca-key" + *outputSuffix + ".pem" + caPrivKeyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400) + if err != nil { + log.Fatal(err) + } + defer caPrivKeyFile.Close() + err = pem.Encode(caPrivKeyFile, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), + }) + if err != nil { + log.Fatalf("unable to create to %s: %v", fname, err) //nolint:gocritic + } + + // generate intermediate CA if requested + var caIntermediate *x509.Certificate + var caIntermediatePrivKey *rsa.PrivateKey + if *genIntermediate { + caIntermediate = &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"CA Company, INC."}, + OrganizationalUnit: []string{"CA Intermediate Team"}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{"Golden Gate Bridge"}, + PostalCode: []string{"94016"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning /*, x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth */}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + EmailAddresses: []string{"ca@example.com"}, + } + // create our private and public key + caIntermediatePrivKey, err = rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + log.Fatal(err) + } + + // create the Intermediate CA + caIntermediateBytes, err := x509.CreateCertificate(rand.Reader, caIntermediate, ca, &caIntermediatePrivKey.PublicKey, caPrivKey) + if err != nil { + log.Fatal(err) + } + + // pem encode + fname = "ca-intermediate" + *outputSuffix + ".pem" + caIntermediateCertFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + log.Fatalf("unable to write to %s: %v", fname, err) + } + err = pem.Encode(caIntermediateCertFile, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caIntermediateBytes, + }) + if err != nil { + log.Fatalf("unable to write to %s: %v", fname, err) + } + fname = "ca-intermediate-key" + *outputSuffix + ".pem" + caIntermediatePrivKeyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400) + if err != nil { + log.Fatal(err) + } + defer caIntermediatePrivKeyFile.Close() + err = pem.Encode(caIntermediatePrivKeyFile, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caIntermediatePrivKey), + }) + if err != nil { + log.Fatalf("unable to create to %s: %v", fname, err) //nolint:gocritic + } + } + // set up our server certificate + cert := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"End User"}, + OrganizationalUnit: []string{"End Node Team"}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{"Golden Gate Bridge"}, + PostalCode: []string{"94016"}, + }, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning /* x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth */}, + KeyUsage: x509.KeyUsageDigitalSignature, + EmailAddresses: []string{"xyz@nosuchprovider.com"}, + DNSNames: []string{"next.hugeunicorn.xyz"}, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + log.Fatal(err) + } + + var certBytes []byte + if !*genIntermediate { + certBytes, err = x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey) + } else { + certBytes, err = x509.CreateCertificate(rand.Reader, cert, caIntermediate, &caIntermediatePrivKey.PublicKey, caIntermediatePrivKey) + } + if err != nil { + log.Fatal(err) + } + + fname = "cert" + *outputSuffix + ".pem" + certFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + log.Fatal(err) + } + defer certFile.Close() + err = pem.Encode(certFile, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + if err != nil { + log.Fatalf("failed to encode cert: %v", err) + } + + fname = "key" + *outputSuffix + ".pem" + keyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400) + if err != nil { + log.Fatal(err) + } + defer keyFile.Close() + err = pem.Encode(keyFile, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), + }) + if err != nil { + log.Fatalf("failed to encode private key to %s: %v", fname, err) + } +} diff --git a/test/helpers.go b/test/helpers.go index 960ca13810c..beea8d7dcd5 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -114,6 +114,33 @@ var verifyKeylessTSA = func(imageRef string, tsaCertChain string, skipSCT bool, return cmd.Exec(context.Background(), args) } +var verifyKeylessTSAWithCARoots = func(imageRef string, + caroots string, // filename of a PEM file with CA Roots certificates + intermediates string, // empty or filename of a PEM file with Intermediate certificates + certFile string, // filename of a PEM file with the codesigning certificate + tsaCertChain string, + skipSCT bool, + skipTlogVerify bool) error { + cmd := cliverify.VerifyCommand{ + CertVerifyOptions: options.CertVerifyOptions{ + CertOidcIssuerRegexp: ".*", + CertIdentityRegexp: ".*", + }, + CertRef: certFile, + CARoots: caroots, + CAIntermediates: intermediates, + RekorURL: rekorURL, + HashAlgorithm: crypto.SHA256, + TSACertChainPath: tsaCertChain, + IgnoreSCT: skipSCT, + IgnoreTlog: skipTlogVerify, + MaxWorkers: 10, + } + args := []string{imageRef} + + return cmd.Exec(context.Background(), args) +} + // Used to verify local images stored on disk var verifyLocal = func(keyRef, path string, checkClaims bool, annotations map[string]interface{}, attachment string) error { cmd := cliverify.VerifyCommand{ From 2ecf4c3b48349ff02e54a911c76e8245d12b8ba6 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Tue, 13 Feb 2024 22:54:30 +0100 Subject: [PATCH 04/13] setup-crane action for e2e_test_pkcs11.sh Signed-off-by: Dmitry S --- .github/workflows/e2e-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index d1d640021b7..263dc816c08 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -58,6 +58,8 @@ jobs: go-version: '1.22' check-latest: true + - uses: imjasonh/setup-crane@00c9e93efa4e1138c9a7a5c594acd6c75a2fbf0c # v0.3 + - name: Run pkcs11 end-to-end tests shell: bash run: ./test/e2e_test_pkcs11.sh From c5ca7b48a6f100f151c66465630553927ab061a4 Mon Sep 17 00:00:00 2001 From: Dmitry Savintsev Date: Tue, 18 Jun 2024 20:01:40 +0200 Subject: [PATCH 05/13] rebase on trunk Signed-off-by: Dmitry Savintsev --- .github/workflows/e2e-tests.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 263dc816c08..df26b06b437 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -63,7 +63,10 @@ jobs: - name: Run pkcs11 end-to-end tests shell: bash run: ./test/e2e_test_pkcs11.sh -<<<<<<< HEAD + + - name: Run e2e_tsa_certbundle.sh + shell: bash + run: make && PATH="$PWD:$PATH" ./test/e2e_tsa_certbundle.sh e2e-kms: runs-on: ubuntu-latest @@ -216,10 +219,3 @@ jobs: - name: Collect diagnostics if: ${{ failure() }} uses: chainguard-dev/actions/kind-diag@84c993eaf02da1c325854fb272a4df9184bd80fc # main -||||||| parent of 83ab01aa (add functional tests for --ca-roots flag) -======= - - - name: Run e2e_tsa_certbundle.sh - shell: bash - run: make && PATH="$PWD:$PATH" ./test/e2e_tsa_certbundle.sh ->>>>>>> 83ab01aa (add functional tests for --ca-roots flag) From 813130818e1a774d7b7cfdd1d5fd38a9eba89491 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Thu, 20 Jun 2024 17:12:51 +0200 Subject: [PATCH 06/13] transform gencert subpackage to helper function Signed-off-by: Dmitry S --- test/e2e_test.go | 58 ++++++++ test/helpers.go | 312 +++++++++++++++++++++++++++++++++++++++++++ test/helpers_test.go | 28 ++++ 3 files changed, 398 insertions(+) create mode 100644 test/helpers_test.go diff --git a/test/e2e_test.go b/test/e2e_test.go index d6205305b2d..77890a0efdf 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -131,6 +131,64 @@ func TestSignVerify(t *testing.T) { mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}, "", false), t) } +func TestSignVerifyCertBundle(t *testing.T) { + td := t.TempDir() + err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td) + if err != nil { + t.Fatal(err) + } + + repo, stop := reg(t) + defer stop() + + imgName := path.Join(repo, "cosign-e2e") + + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + caCertFile, _ /* caPrivKeyFile */, caIntermediateCertFile, _ /* caIntermediatePrivKeyFile */, certFile, privKeyFile, pubkeyFile, certChainFile, err := generateCertificateBundleFiles(td, true, "foobar") + + ctx := context.Background() + // Verify should fail at first + must(verifyCertBundle(pubkeyFile, caCertFile, caIntermediateCertFile, certFile, imgName, true, nil, "", false), t) + // So should download + mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + + // Now sign the image + ko := options.KeyOpts{ + KeyRef: privKeyFile, + PassFunc: passFunc, + RekorURL: rekorURL, + SkipConfirmation: true, + } + so := options.SignOptions{ + Upload: true, + TlogUpload: true, + } + must(sign.SignCmd(ro, ko, so, []string{imgName}), t) + + // Now verify and download should work! + must(verifyCertBundle(pubkeyFile, caCertFile, caIntermediateCertFile, certFile, imgName, true, nil, "", false), t) + // verification with certificate chain instead of root/intermediate files should work as well + must(verifyCertChain(pubkeyFile, certChainFile, certFile, imgName, true, nil, "", false), t) + must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + + // Look for a specific annotation + mustErr(verifyCertBundle(pubkeyFile, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", false), t) + + so.AnnotationOptions = options.AnnotationOptions{ + Annotations: []string{"foo=bar"}, + } + // Sign the image with an annotation + must(sign.SignCmd(ro, ko, so, []string{imgName}), t) + + // It should match this time. + must(verifyCertBundle(pubkeyFile, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", false), t) + + // But two doesn't work + mustErr(verifyCertBundle(pubkeyFile, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}, "", false), t) +} + func TestSignVerifyClean(t *testing.T) { td := t.TempDir() err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td) diff --git a/test/helpers.go b/test/helpers.go index beea8d7dcd5..38bffaa282c 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -18,10 +18,19 @@ package test import ( + "bytes" "context" "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" "io" + "log" + "math/big" + "net" "net/http" "net/http/httptest" "net/url" @@ -29,6 +38,7 @@ import ( "path" "path/filepath" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/google/go-containerregistry/pkg/authn" @@ -77,6 +87,49 @@ var verify = func(keyRef, imageRef string, checkClaims bool, annotations map[str return cmd.Exec(context.Background(), args) } +var verifyCertChain = func(keyRef, certChain, certFile, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment string, skipTlogVerify bool) error { + cmd := cliverify.VerifyCommand{ + KeyRef: keyRef, + RekorURL: rekorURL, + CheckClaims: checkClaims, + Annotations: sigs.AnnotationsMap{Annotations: annotations}, + Attachment: attachment, + HashAlgorithm: crypto.SHA256, + MaxWorkers: 10, + IgnoreTlog: skipTlogVerify, + CertVerifyOptions: options.CertVerifyOptions{ + Cert: certFile, + CertChain: certChain, + }, + } + + args := []string{imageRef} + + return cmd.Exec(context.Background(), args) +} + +var verifyCertBundle = func(keyRef, caCertFile, caIntermediates, certFile, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment string, skipTlogVerify bool) error { + cmd := cliverify.VerifyCommand{ + KeyRef: keyRef, + RekorURL: rekorURL, + CheckClaims: checkClaims, + Annotations: sigs.AnnotationsMap{Annotations: annotations}, + Attachment: attachment, + HashAlgorithm: crypto.SHA256, + MaxWorkers: 10, + IgnoreTlog: skipTlogVerify, + CertVerifyOptions: options.CertVerifyOptions{ + Cert: certFile, + CAIntermediates: caIntermediates, + CARoots: caCertFile, + }, + } + + args := []string{imageRef} + + return cmd.Exec(context.Background(), args) +} + var verifyTSA = func(keyRef, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment, tsaCertChain string, skipTlogVerify bool) error { cmd := cliverify.VerifyCommand{ KeyRef: keyRef, @@ -443,3 +496,262 @@ func downloadAndSetEnv(t *testing.T, url, envVar, dir string) error { t.Setenv(envVar, f.Name()) return nil } + +func generateCertificateBundleFiles(td string, genIntermediate bool, outputSuffix string) ( + caCertFile string, + caPrivKeyFile string, + caIntermediateCertFile string, + caIntermediatePrivKeyFile string, + certFile string, + keyFile string, + pubKeyFile string, + certChainFile string, + err error, +) { + caCertBuf, caPrivKeyBuf, caIntermediateCertBuf, caIntermediatePrivKeyBuf, certBuf, keyBuf, pubkey, certChainBuf, err := generateCertificateBundle(genIntermediate) + if err != nil { + err = fmt.Errorf("error generating certificate bundle: %w", err) + return + } + err = os.WriteFile(filepath.Join(td, fmt.Sprintf("caCert%s.pem", outputSuffix)), caCertBuf.Bytes(), 0600) + if err != nil { + err = fmt.Errorf("error writing caCert to file: %w", err) + return + } + err = os.WriteFile(filepath.Join(td, fmt.Sprintf("caPrivKey%s.pem", outputSuffix)), caPrivKeyBuf.Bytes(), 0600) + if err != nil { + err = fmt.Errorf("error writing caPrivKey to file: %w", err) + return + } + if genIntermediate { + err = os.WriteFile(filepath.Join(td, fmt.Sprintf("caIntermediateCert%s.pem", outputSuffix)), caIntermediateCertBuf.Bytes(), 0600) + if err != nil { + err = fmt.Errorf("error writing caIntermediateCert to file: %w", err) + return + } + err = os.WriteFile(filepath.Join(td, fmt.Sprintf("caIntermediatePrivKey%s.pem", outputSuffix)), caIntermediatePrivKeyBuf.Bytes(), 0600) + if err != nil { + err = fmt.Errorf("error writing caIntermediatePrivKey to file: %w", err) + return + } + } + err = os.WriteFile(filepath.Join(td, fmt.Sprintf("cert%s.pem", outputSuffix)), certBuf.Bytes(), 0600) + if err != nil { + err = fmt.Errorf("error writing cert to file: %w", err) + return + } + err = os.WriteFile(filepath.Join(td, fmt.Sprintf("key%s.pem", outputSuffix)), keyBuf.Bytes(), 0600) + if err != nil { + err = fmt.Errorf("error writing key to file: %w", err) + return + } + // write the contents of certChainBuf to a file + certChainFile = filepath.Join(td, fmt.Sprintf("certchain%s.pem", outputSuffix)) + err = os.WriteFile(certChainFile, certChainBuf.Bytes(), 0600) + if err != nil { + err = fmt.Errorf("error writing certificate chain to file: %w", err) + return + } + // write the public key to a file + pubKeyFile = filepath.Join(td, fmt.Sprintf("pubkey%s.pem", outputSuffix)) + pubKeyBuf := &bytes.Buffer{} + pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubkey) + if err != nil { + err = fmt.Errorf("error marshalling public key: %w", err) + return + } + err = pem.Encode(pubKeyBuf, &pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubKeyBytes, + }) + return +} + +func generateCertificateBundle(genIntermediate bool) ( + caCertBuf *bytes.Buffer, + caPrivKeyBuf *bytes.Buffer, + caIntermediateCertBuf *bytes.Buffer, + caIntermediatePrivKeyBuf *bytes.Buffer, + certBuf *bytes.Buffer, + keyBuf *bytes.Buffer, + pubkeyBuf *bytes.Buffer, + certBundleBuf *bytes.Buffer, + err error, +) { + // set up our CA certificate + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"CA Company, INC."}, + OrganizationalUnit: []string{"CA Root Team"}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{"Golden Gate Bridge"}, + PostalCode: []string{"94016"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning /*, x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth */}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + EmailAddresses: []string{"ca@example.com"}, + } + + // create our private and public key + caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + log.Fatal(err) + } + pubkey := &caPrivKey.PublicKey + // create the CA + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + log.Fatal(err) + } + + caCertBuf = &bytes.Buffer{} + err = pem.Encode(caCertBuf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + if err != nil { + log.Fatalf("unable to write PEM encode: %v", err) + } + + caPrivKeyBuf = &bytes.Buffer{} + err = pem.Encode(caPrivKeyBuf, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), + }) + if err != nil { + log.Fatalf("unable to PEM encode private key to buffer: %v", err) //nolint:gocritic + } + pubkeyBuf = &bytes.Buffer{} + // PEM encode to pubkeyBuf the public key of caPrivKey + pubkeyBytes, err := x509.MarshalPKIXPublicKey(pubkey) + if err != nil { + log.Fatalf("failed to marshal public key: %v", err) + } + err = pem.Encode(pubkeyBuf, &pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubkeyBytes, + }) + if err != nil { + log.Fatalf("failed to PME-encode public key to buffer: %v", err) + } + + // generate intermediate CA if requested + var caIntermediate *x509.Certificate + var caIntermediatePrivKey *rsa.PrivateKey + if genIntermediate { + caIntermediate = &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"CA Company, INC."}, + OrganizationalUnit: []string{"CA Intermediate Team"}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{"Golden Gate Bridge"}, + PostalCode: []string{"94016"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning /*, x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth */}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + EmailAddresses: []string{"ca@example.com"}, + } + // create our private and public key + caIntermediatePrivKey, err = rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + log.Fatal(err) + } + + // create the Intermediate CA + caIntermediateBytes, err := x509.CreateCertificate(rand.Reader, caIntermediate, ca, &caIntermediatePrivKey.PublicKey, caPrivKey) + if err != nil { + log.Fatal(err) + } + + caIntermediateCertBuf = &bytes.Buffer{} + err = pem.Encode(caIntermediateCertBuf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caIntermediateBytes, + }) + if err != nil { + log.Fatalf("unable to write to buffer: %v", err) + } + caIntermediatePrivKeyBuf = &bytes.Buffer{} + err = pem.Encode(caIntermediatePrivKeyBuf, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caIntermediatePrivKey), + }) + if err != nil { + log.Fatalf("unable to PEM encode caIntermediatePrivKey: %v", err) + } + } + // set up our server certificate + cert := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"End User"}, + OrganizationalUnit: []string{"End Node Team"}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{"Golden Gate Bridge"}, + PostalCode: []string{"94016"}, + }, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning /* x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth */}, + KeyUsage: x509.KeyUsageDigitalSignature, + EmailAddresses: []string{"xyz@nosuchprovider.com"}, + DNSNames: []string{"next.hugeunicorn.xyz"}, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + log.Fatal(err) + } + + var certBytes []byte + if !genIntermediate { + certBytes, err = x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey) + } else { + certBytes, err = x509.CreateCertificate(rand.Reader, cert, caIntermediate, &caIntermediatePrivKey.PublicKey, caIntermediatePrivKey) + } + if err != nil { + log.Fatal(err) + } + + certBuf = &bytes.Buffer{} + err = pem.Encode(certBuf, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + if err != nil { + log.Fatalf("failed to encode cert: %v", err) + } + + // concatenate into certChainBuf the contents of caIntermediateCertBuf and caCertBuf + certBundleBuf = &bytes.Buffer{} + if genIntermediate { + _, err = certBundleBuf.Write(caIntermediateCertBuf.Bytes()) + if err != nil { + log.Fatalf("failed to write caIntermediateCertBuf to certChainBuf: %v", err) + } + } + _, err = certBundleBuf.Write(caCertBuf.Bytes()) + if err != nil { + log.Fatalf("failed to write caCertBuf to certChainBuf: %v", err) + } + + return caCertBuf, caPrivKeyBuf, caIntermediateCertBuf, caIntermediatePrivKeyBuf, certBuf, keyBuf, pubkeyBuf, certBundleBuf, nil +} diff --git a/test/helpers_test.go b/test/helpers_test.go new file mode 100644 index 00000000000..9c43468d966 --- /dev/null +++ b/test/helpers_test.go @@ -0,0 +1,28 @@ +//go:build e2e + +package test + +import "testing" + +func TestGenerateCertificateBundle(t *testing.T) { + for _, test := range []struct { + name string + genIntermediate bool + }{ + { + name: "without intermediate", + genIntermediate: false, + }, + { + name: "with intermediate", + genIntermediate: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + _, _, _, _, _, _, _, _, err := generateCertificateBundle(true) + if err != nil { + t.Fatalf("Error generating certificate bundle: %v", err) + } + }) + } +} From 701bad7769c0ac2bb97a6ed2f98060b59f0ce694 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Thu, 20 Jun 2024 18:36:07 +0200 Subject: [PATCH 07/13] use the trunk version of workflows/e2e-tests.yml Signed-off-by: Dmitry S --- .github/workflows/e2e-tests.yml | 6 - test/gencert/main.go | 228 -------------------------------- 2 files changed, 234 deletions(-) delete mode 100644 test/gencert/main.go diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index df26b06b437..e3479e6847d 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -58,16 +58,10 @@ jobs: go-version: '1.22' check-latest: true - - uses: imjasonh/setup-crane@00c9e93efa4e1138c9a7a5c594acd6c75a2fbf0c # v0.3 - - name: Run pkcs11 end-to-end tests shell: bash run: ./test/e2e_test_pkcs11.sh - - name: Run e2e_tsa_certbundle.sh - shell: bash - run: make && PATH="$PWD:$PATH" ./test/e2e_tsa_certbundle.sh - e2e-kms: runs-on: ubuntu-latest services: diff --git a/test/gencert/main.go b/test/gencert/main.go deleted file mode 100644 index 76730241e35..00000000000 --- a/test/gencert/main.go +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright 2023 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. - -// code is based on the snippet https://gist.github.com/shaneutt/5e1995295cff6721c89a71d13a71c251 -// with a permissive (Public Domain) License. - -// TODO(dmitris) - refactor to move the code generating certificates and writing to files -// to a separate function. - -package main - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "flag" - "log" - "math/big" - "net" - "os" - "time" -) - -func main() { - var genIntermediate = flag.Bool("intermediate", false, "generate intermediate CA") - var outputSuffix = flag.String("output-suffix", "", "suffix to append to generated files before the extension") - flag.Parse() - - // set up our CA certificate - ca := &x509.Certificate{ - SerialNumber: big.NewInt(2019), - Subject: pkix.Name{ - Organization: []string{"CA Company, INC."}, - OrganizationalUnit: []string{"CA Root Team"}, - Country: []string{"US"}, - Province: []string{""}, - Locality: []string{"San Francisco"}, - StreetAddress: []string{"Golden Gate Bridge"}, - PostalCode: []string{"94016"}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - IsCA: true, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning /*, x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth */}, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - BasicConstraintsValid: true, - EmailAddresses: []string{"ca@example.com"}, - } - - // create our private and public key - caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - log.Fatal(err) - } - - // create the CA - caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) - if err != nil { - log.Fatal(err) - } - - // pem encode - fname := "ca-root" + *outputSuffix + ".pem" - caCertFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - log.Fatalf("unable to write to %s: %v", fname, err) - } - err = pem.Encode(caCertFile, &pem.Block{ - Type: "CERTIFICATE", - Bytes: caBytes, - }) - if err != nil { - log.Fatalf("unable to write to %s: %v", fname, err) - } - fname = "ca-key" + *outputSuffix + ".pem" - caPrivKeyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400) - if err != nil { - log.Fatal(err) - } - defer caPrivKeyFile.Close() - err = pem.Encode(caPrivKeyFile, &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), - }) - if err != nil { - log.Fatalf("unable to create to %s: %v", fname, err) //nolint:gocritic - } - - // generate intermediate CA if requested - var caIntermediate *x509.Certificate - var caIntermediatePrivKey *rsa.PrivateKey - if *genIntermediate { - caIntermediate = &x509.Certificate{ - SerialNumber: big.NewInt(2019), - Subject: pkix.Name{ - Organization: []string{"CA Company, INC."}, - OrganizationalUnit: []string{"CA Intermediate Team"}, - Country: []string{"US"}, - Province: []string{""}, - Locality: []string{"San Francisco"}, - StreetAddress: []string{"Golden Gate Bridge"}, - PostalCode: []string{"94016"}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - IsCA: true, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning /*, x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth */}, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - BasicConstraintsValid: true, - EmailAddresses: []string{"ca@example.com"}, - } - // create our private and public key - caIntermediatePrivKey, err = rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - log.Fatal(err) - } - - // create the Intermediate CA - caIntermediateBytes, err := x509.CreateCertificate(rand.Reader, caIntermediate, ca, &caIntermediatePrivKey.PublicKey, caPrivKey) - if err != nil { - log.Fatal(err) - } - - // pem encode - fname = "ca-intermediate" + *outputSuffix + ".pem" - caIntermediateCertFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - log.Fatalf("unable to write to %s: %v", fname, err) - } - err = pem.Encode(caIntermediateCertFile, &pem.Block{ - Type: "CERTIFICATE", - Bytes: caIntermediateBytes, - }) - if err != nil { - log.Fatalf("unable to write to %s: %v", fname, err) - } - fname = "ca-intermediate-key" + *outputSuffix + ".pem" - caIntermediatePrivKeyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400) - if err != nil { - log.Fatal(err) - } - defer caIntermediatePrivKeyFile.Close() - err = pem.Encode(caIntermediatePrivKeyFile, &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(caIntermediatePrivKey), - }) - if err != nil { - log.Fatalf("unable to create to %s: %v", fname, err) //nolint:gocritic - } - } - // set up our server certificate - cert := &x509.Certificate{ - SerialNumber: big.NewInt(2019), - Subject: pkix.Name{ - Organization: []string{"End User"}, - OrganizationalUnit: []string{"End Node Team"}, - Country: []string{"US"}, - Province: []string{""}, - Locality: []string{"San Francisco"}, - StreetAddress: []string{"Golden Gate Bridge"}, - PostalCode: []string{"94016"}, - }, - IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(10, 0, 0), - SubjectKeyId: []byte{1, 2, 3, 4, 6}, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning /* x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth */}, - KeyUsage: x509.KeyUsageDigitalSignature, - EmailAddresses: []string{"xyz@nosuchprovider.com"}, - DNSNames: []string{"next.hugeunicorn.xyz"}, - } - - certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - log.Fatal(err) - } - - var certBytes []byte - if !*genIntermediate { - certBytes, err = x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey) - } else { - certBytes, err = x509.CreateCertificate(rand.Reader, cert, caIntermediate, &caIntermediatePrivKey.PublicKey, caIntermediatePrivKey) - } - if err != nil { - log.Fatal(err) - } - - fname = "cert" + *outputSuffix + ".pem" - certFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - log.Fatal(err) - } - defer certFile.Close() - err = pem.Encode(certFile, &pem.Block{ - Type: "CERTIFICATE", - Bytes: certBytes, - }) - if err != nil { - log.Fatalf("failed to encode cert: %v", err) - } - - fname = "key" + *outputSuffix + ".pem" - keyFile, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400) - if err != nil { - log.Fatal(err) - } - defer keyFile.Close() - err = pem.Encode(keyFile, &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), - }) - if err != nil { - log.Fatalf("failed to encode private key to %s: %v", fname, err) - } -} From da3779d3a995f2e3fe779f9c9fa4d85f44c1ca1b Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Thu, 20 Jun 2024 20:31:04 +0200 Subject: [PATCH 08/13] correct certificate generation for e2e tests Signed-off-by: Dmitry S --- test/e2e_test.go | 18 +++++++++-------- test/helpers.go | 47 +++++++------------------------------------- test/helpers_test.go | 2 +- 3 files changed, 18 insertions(+), 49 deletions(-) diff --git a/test/e2e_test.go b/test/e2e_test.go index 77890a0efdf..f6ead509f97 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -146,17 +146,18 @@ func TestSignVerifyCertBundle(t *testing.T) { _, _, cleanup := mkimage(t, imgName) defer cleanup() - caCertFile, _ /* caPrivKeyFile */, caIntermediateCertFile, _ /* caIntermediatePrivKeyFile */, certFile, privKeyFile, pubkeyFile, certChainFile, err := generateCertificateBundleFiles(td, true, "foobar") + _, privKeyPath, pubKeyPath := keypair(t, td) + caCertFile, _ /* caPrivKeyFile */, caIntermediateCertFile, _ /* caIntermediatePrivKeyFile */, certFile, certChainFile, err := generateCertificateBundleFiles(td, true, "foobar") ctx := context.Background() // Verify should fail at first - must(verifyCertBundle(pubkeyFile, caCertFile, caIntermediateCertFile, certFile, imgName, true, nil, "", false), t) + mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, certFile, imgName, true, nil, "", true), t) // So should download mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) // Now sign the image ko := options.KeyOpts{ - KeyRef: privKeyFile, + KeyRef: privKeyPath, PassFunc: passFunc, RekorURL: rekorURL, SkipConfirmation: true, @@ -168,13 +169,14 @@ func TestSignVerifyCertBundle(t *testing.T) { must(sign.SignCmd(ro, ko, so, []string{imgName}), t) // Now verify and download should work! - must(verifyCertBundle(pubkeyFile, caCertFile, caIntermediateCertFile, certFile, imgName, true, nil, "", false), t) + ignoreTlog := true + must(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, certFile, imgName, true, nil, "", ignoreTlog), t) // verification with certificate chain instead of root/intermediate files should work as well - must(verifyCertChain(pubkeyFile, certChainFile, certFile, imgName, true, nil, "", false), t) + must(verifyCertChain(pubKeyPath, certChainFile, certFile, imgName, true, nil, "", ignoreTlog), t) must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) // Look for a specific annotation - mustErr(verifyCertBundle(pubkeyFile, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", false), t) + mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", ignoreTlog), t) so.AnnotationOptions = options.AnnotationOptions{ Annotations: []string{"foo=bar"}, @@ -183,10 +185,10 @@ func TestSignVerifyCertBundle(t *testing.T) { must(sign.SignCmd(ro, ko, so, []string{imgName}), t) // It should match this time. - must(verifyCertBundle(pubkeyFile, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", false), t) + must(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", ignoreTlog), t) // But two doesn't work - mustErr(verifyCertBundle(pubkeyFile, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}, "", false), t) + mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}, "", ignoreTlog), t) } func TestSignVerifyClean(t *testing.T) { diff --git a/test/helpers.go b/test/helpers.go index 38bffaa282c..0967e6ddaf9 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -119,9 +119,10 @@ var verifyCertBundle = func(keyRef, caCertFile, caIntermediates, certFile, image MaxWorkers: 10, IgnoreTlog: skipTlogVerify, CertVerifyOptions: options.CertVerifyOptions{ - Cert: certFile, - CAIntermediates: caIntermediates, - CARoots: caCertFile, + CAIntermediates: caIntermediates, + CARoots: caCertFile, + CertOidcIssuerRegexp: ".*", + CertIdentityRegexp: ".*", }, } @@ -503,12 +504,10 @@ func generateCertificateBundleFiles(td string, genIntermediate bool, outputSuffi caIntermediateCertFile string, caIntermediatePrivKeyFile string, certFile string, - keyFile string, - pubKeyFile string, certChainFile string, err error, ) { - caCertBuf, caPrivKeyBuf, caIntermediateCertBuf, caIntermediatePrivKeyBuf, certBuf, keyBuf, pubkey, certChainBuf, err := generateCertificateBundle(genIntermediate) + caCertBuf, caPrivKeyBuf, caIntermediateCertBuf, caIntermediatePrivKeyBuf, certBuf, certChainBuf, err := generateCertificateBundle(genIntermediate) if err != nil { err = fmt.Errorf("error generating certificate bundle: %w", err) return @@ -540,11 +539,7 @@ func generateCertificateBundleFiles(td string, genIntermediate bool, outputSuffi err = fmt.Errorf("error writing cert to file: %w", err) return } - err = os.WriteFile(filepath.Join(td, fmt.Sprintf("key%s.pem", outputSuffix)), keyBuf.Bytes(), 0600) - if err != nil { - err = fmt.Errorf("error writing key to file: %w", err) - return - } + // write the contents of certChainBuf to a file certChainFile = filepath.Join(td, fmt.Sprintf("certchain%s.pem", outputSuffix)) err = os.WriteFile(certChainFile, certChainBuf.Bytes(), 0600) @@ -552,18 +547,6 @@ func generateCertificateBundleFiles(td string, genIntermediate bool, outputSuffi err = fmt.Errorf("error writing certificate chain to file: %w", err) return } - // write the public key to a file - pubKeyFile = filepath.Join(td, fmt.Sprintf("pubkey%s.pem", outputSuffix)) - pubKeyBuf := &bytes.Buffer{} - pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubkey) - if err != nil { - err = fmt.Errorf("error marshalling public key: %w", err) - return - } - err = pem.Encode(pubKeyBuf, &pem.Block{ - Type: "PUBLIC KEY", - Bytes: pubKeyBytes, - }) return } @@ -573,8 +556,6 @@ func generateCertificateBundle(genIntermediate bool) ( caIntermediateCertBuf *bytes.Buffer, caIntermediatePrivKeyBuf *bytes.Buffer, certBuf *bytes.Buffer, - keyBuf *bytes.Buffer, - pubkeyBuf *bytes.Buffer, certBundleBuf *bytes.Buffer, err error, ) { @@ -604,7 +585,6 @@ func generateCertificateBundle(genIntermediate bool) ( if err != nil { log.Fatal(err) } - pubkey := &caPrivKey.PublicKey // create the CA caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) if err != nil { @@ -628,19 +608,6 @@ func generateCertificateBundle(genIntermediate bool) ( if err != nil { log.Fatalf("unable to PEM encode private key to buffer: %v", err) //nolint:gocritic } - pubkeyBuf = &bytes.Buffer{} - // PEM encode to pubkeyBuf the public key of caPrivKey - pubkeyBytes, err := x509.MarshalPKIXPublicKey(pubkey) - if err != nil { - log.Fatalf("failed to marshal public key: %v", err) - } - err = pem.Encode(pubkeyBuf, &pem.Block{ - Type: "PUBLIC KEY", - Bytes: pubkeyBytes, - }) - if err != nil { - log.Fatalf("failed to PME-encode public key to buffer: %v", err) - } // generate intermediate CA if requested var caIntermediate *x509.Certificate @@ -753,5 +720,5 @@ func generateCertificateBundle(genIntermediate bool) ( log.Fatalf("failed to write caCertBuf to certChainBuf: %v", err) } - return caCertBuf, caPrivKeyBuf, caIntermediateCertBuf, caIntermediatePrivKeyBuf, certBuf, keyBuf, pubkeyBuf, certBundleBuf, nil + return caCertBuf, caPrivKeyBuf, caIntermediateCertBuf, caIntermediatePrivKeyBuf, certBuf, certBundleBuf, nil } diff --git a/test/helpers_test.go b/test/helpers_test.go index 9c43468d966..95b8872ddd8 100644 --- a/test/helpers_test.go +++ b/test/helpers_test.go @@ -19,7 +19,7 @@ func TestGenerateCertificateBundle(t *testing.T) { }, } { t.Run(test.name, func(t *testing.T) { - _, _, _, _, _, _, _, _, err := generateCertificateBundle(true) + _, _, _, _, _, _, err := generateCertificateBundle(true) if err != nil { t.Fatalf("Error generating certificate bundle: %v", err) } From ee0ece0e634c88da33f0c5efcbdeb78165d47cfa Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Thu, 20 Jun 2024 21:23:16 +0200 Subject: [PATCH 09/13] refactor test cert/keys generation and corresponding test Signed-off-by: Dmitry S --- test/helpers.go | 27 ++++---- test/helpers_test.go | 149 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 160 insertions(+), 16 deletions(-) diff --git a/test/helpers.go b/test/helpers.go index 0967e6ddaf9..7747bebdd90 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -512,31 +512,36 @@ func generateCertificateBundleFiles(td string, genIntermediate bool, outputSuffi err = fmt.Errorf("error generating certificate bundle: %w", err) return } - err = os.WriteFile(filepath.Join(td, fmt.Sprintf("caCert%s.pem", outputSuffix)), caCertBuf.Bytes(), 0600) + caCertFile = filepath.Join(td, fmt.Sprintf("caCert%s.pem", outputSuffix)) + err = os.WriteFile(caCertFile, caCertBuf.Bytes(), 0600) if err != nil { - err = fmt.Errorf("error writing caCert to file: %w", err) + err = fmt.Errorf("error writing caCert to file %s: %w", caCertFile, err) return } - err = os.WriteFile(filepath.Join(td, fmt.Sprintf("caPrivKey%s.pem", outputSuffix)), caPrivKeyBuf.Bytes(), 0600) + caPrivKeyFile = filepath.Join(td, fmt.Sprintf("caPrivKey%s.pem", outputSuffix)) + err = os.WriteFile(caPrivKeyFile, caPrivKeyBuf.Bytes(), 0600) if err != nil { - err = fmt.Errorf("error writing caPrivKey to file: %w", err) + err = fmt.Errorf("error writing caPrivKey to file %s: %w", caPrivKeyFile, err) return } if genIntermediate { - err = os.WriteFile(filepath.Join(td, fmt.Sprintf("caIntermediateCert%s.pem", outputSuffix)), caIntermediateCertBuf.Bytes(), 0600) + caIntermediateCertFile = filepath.Join(td, fmt.Sprintf("caIntermediateCert%s.pem", outputSuffix)) + err = os.WriteFile(caIntermediateCertFile, caIntermediateCertBuf.Bytes(), 0600) if err != nil { - err = fmt.Errorf("error writing caIntermediateCert to file: %w", err) + err = fmt.Errorf("error writing caIntermediateCert to file %s: %w", caIntermediateCertFile, err) return } - err = os.WriteFile(filepath.Join(td, fmt.Sprintf("caIntermediatePrivKey%s.pem", outputSuffix)), caIntermediatePrivKeyBuf.Bytes(), 0600) + caIntermediatePrivKeyFile = filepath.Join(td, fmt.Sprintf("caIntermediatePrivKey%s.pem", outputSuffix)) + err = os.WriteFile(caIntermediatePrivKeyFile, caIntermediatePrivKeyBuf.Bytes(), 0600) if err != nil { - err = fmt.Errorf("error writing caIntermediatePrivKey to file: %w", err) + err = fmt.Errorf("error writing caIntermediatePrivKey to file %s: %w", caIntermediatePrivKeyFile, err) return } } - err = os.WriteFile(filepath.Join(td, fmt.Sprintf("cert%s.pem", outputSuffix)), certBuf.Bytes(), 0600) + certFile = filepath.Join(td, fmt.Sprintf("cert%s.pem", outputSuffix)) + err = os.WriteFile(certFile, certBuf.Bytes(), 0600) if err != nil { - err = fmt.Errorf("error writing cert to file: %w", err) + err = fmt.Errorf("error writing cert to file %s: %w", certFile, err) return } @@ -544,7 +549,7 @@ func generateCertificateBundleFiles(td string, genIntermediate bool, outputSuffi certChainFile = filepath.Join(td, fmt.Sprintf("certchain%s.pem", outputSuffix)) err = os.WriteFile(certChainFile, certChainBuf.Bytes(), 0600) if err != nil { - err = fmt.Errorf("error writing certificate chain to file: %w", err) + err = fmt.Errorf("error writing certificate chain to file %s: %w", certFile, err) return } return diff --git a/test/helpers_test.go b/test/helpers_test.go index 95b8872ddd8..ae04371422a 100644 --- a/test/helpers_test.go +++ b/test/helpers_test.go @@ -2,10 +2,16 @@ package test -import "testing" +import ( + "crypto/x509" + "encoding/pem" + "io/ioutil" + "log" + "testing" +) -func TestGenerateCertificateBundle(t *testing.T) { - for _, test := range []struct { +func TestGenerateCertificateBundleFiles(t *testing.T) { + for _, tt := range []struct { name string genIntermediate bool }{ @@ -18,11 +24,144 @@ func TestGenerateCertificateBundle(t *testing.T) { genIntermediate: true, }, } { - t.Run(test.name, func(t *testing.T) { - _, _, _, _, _, _, err := generateCertificateBundle(true) + t.Run(tt.name, func(t *testing.T) { + td := t.TempDir() + suffix := "foobar" + caCertFile, caPrivKeyFile, caIntermediateCertFile, caIntermediatePrivKeyFile, + certFile, certChainFile, err := generateCertificateBundleFiles(td, true, suffix) if err != nil { t.Fatalf("Error generating certificate bundle: %v", err) } + verifyCertificate(t, caCertFile) + if tt.genIntermediate { + verifyCertificate(t, caIntermediateCertFile) + } + verifyCertificate(t, certFile) + + verifyPrivateKey(t, caPrivKeyFile) + if tt.genIntermediate { + verifyPrivateKey(t, caIntermediatePrivKeyFile) + verifyCertificateChain(t, certChainFile) + } }) } } + +func verifyCertificate(t *testing.T, certFile string) { + t.Helper() + // open and parse certFile, ensure it is a TLS certificate + data, err := ioutil.ReadFile(certFile) + if err != nil { + t.Fatalf("Error reading certificate file %s: %v\n", certFile, err) + return + } + + // Check if the file contents are a PEM-encoded TLS certificate + if !isPEMEncodedCert(data) { + t.Fatalf("file %s doesn't contain a valid PEM-encoded TLS certificate", certFile) + } +} + +func verifyCertificateChain(t *testing.T, certChainFile string) { + t.Helper() + // open and parse certChainFile, ensure it is a TLS certificate chain + data, err := ioutil.ReadFile(certChainFile) + if err != nil { + t.Fatalf("Error reading certificate file %s: %v\n", certChainFile, err) + } + + // Check if the file contents are a PEM-encoded TLS certificate + t.Logf("DMDEBUG 76 before isPEMEncodedCertChain") + if !isPEMEncodedCertChain(data) { + t.Fatalf("file %s doesn't contain a valid PEM-encoded TLS certificate chain", certChainFile) + } +} + +// isPEMEncodedCert checks if the provided data is a PEM-encoded certificate +func isPEMEncodedCert(data []byte) bool { + // Decode the PEM data + block, _ := pem.Decode(data) + if block == nil || block.Type != "CERTIFICATE" { + return false + } + + // Parse the certificate to ensure it is valid + _, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return false + } + + return true +} + +func verifyPrivateKey(t *testing.T, privKeyFile string) { + t.Helper() + // open and parse certFile, ensure it is a TLS certificate + data, err := ioutil.ReadFile(privKeyFile) + if err != nil { + t.Fatalf("Error reading private key file %s: %v\n", privKeyFile, err) + return + } + + // Check if the file contents are a PEM-encoded private key + if !isPEMEncodedPrivateKey(data) { + t.Fatalf("file %s doesn't contain a valid PEM-encoded private key", privKeyFile) + } +} + +// isPEMEncodedPrivateKey checks if the provided data is a PEM-encoded private key +func isPEMEncodedPrivateKey(data []byte) bool { + // Decode the PEM data + block, _ := pem.Decode(data) + if block == nil { + return false + } + var err error + + switch block.Type { + case "PRIVATE KEY": + _, err = x509.ParsePKCS8PrivateKey(block.Bytes) + case "RSA PRIVATE KEY": + _, err = x509.ParsePKCS1PrivateKey(block.Bytes) + case "EC PRIVATE KEY": + _, err = x509.ParseECPrivateKey(block.Bytes) + default: + return false + } + if err != nil { + log.Printf("isPEMEncodedPrivateKey: %v", err) + return false + } + + return true +} + +// isPEMEncodedCertChain checks if the provided data is a concatenation of a PEM-encoded +// intermediate certificate followed by a root certificate +func isPEMEncodedCertChain(data []byte) bool { + // Decode the PEM blocks one by one + blockCnt := 0 + for len(data) > 0 { + var block *pem.Block + block, data = pem.Decode(data) + if block == nil { + break + } + if block.Type != "CERTIFICATE" { + return false + } + + // Parse the certificate to ensure it is valid + _, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return false + } + + blockCnt++ + } + // we want exactly two blocks in the certificate chain - intermediate and root + if blockCnt != 2 { + return false + } + return true +} From 70ed9f9918e38a4a14d7fef4ab859880e70022e2 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Thu, 20 Jun 2024 23:53:14 +0200 Subject: [PATCH 10/13] add license header Signed-off-by: Dmitry S --- test/helpers_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/helpers_test.go b/test/helpers_test.go index ae04371422a..a1e9ae0f827 100644 --- a/test/helpers_test.go +++ b/test/helpers_test.go @@ -1,3 +1,18 @@ +// +// Copyright 2024 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. + //go:build e2e package test @@ -26,7 +41,7 @@ func TestGenerateCertificateBundleFiles(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { td := t.TempDir() - suffix := "foobar" + suffix := "foo" caCertFile, caPrivKeyFile, caIntermediateCertFile, caIntermediatePrivKeyFile, certFile, certChainFile, err := generateCertificateBundleFiles(td, true, suffix) if err != nil { From c437f5639917e3cc35387647fb45b2b5c2a6b021 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Fri, 21 Jun 2024 12:51:52 +0200 Subject: [PATCH 11/13] remove test shell scripts Signed-off-by: Dmitry S --- test/e2e_tsa_certbundle.sh | 86 ------------------------------- test/e2e_tsa_mtls.sh | 102 ------------------------------------- 2 files changed, 188 deletions(-) delete mode 100755 test/e2e_tsa_certbundle.sh delete mode 100755 test/e2e_tsa_mtls.sh diff --git a/test/e2e_tsa_certbundle.sh b/test/e2e_tsa_certbundle.sh deleted file mode 100755 index cfe3d84b704..00000000000 --- a/test/e2e_tsa_certbundle.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright 2024 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. - -# This test was done for https://github.com/sigstore/cosign/pull/3464 -# to verify that 'cosign verify' can work with the '--ca-roots' -# command-line option. It is a modified copy of e2e_tsa_mtls.sh. - -set -exuo pipefail - -## Requirements -# - cosign -# - crane -# - go - -CERT_BASE="test/testdata" - -export TIMESTAMP_SERVER_URL=https://freetsa.org/tsr -TIMESTAMP_CHAIN_FILE="timestamp-chain" -curl -s https://www.freetsa.org/files/cacert.pem > $TIMESTAMP_CHAIN_FILE -echo "TIMESTAMP_CHAIN_FILE: $(ls -l $TIMESTAMP_CHAIN_FILE)" -COSIGN_CLI=./cosign - -# unlike e2e_tsa_mtls.sh, there is no option to pass an image as a command-line parameter. - -# Upload an image to ttl.sh - commands from https://docs.sigstore.dev/cosign/keyless/ -SRC_IMAGE=busybox -SRC_DIGEST=$(crane digest busybox) -for i in 01 02; do - IMAGE_URI_TEMP=ttl.sh/$(uuidgen | head -c 8 | tr 'A-Z' 'a-z') - crane cp $SRC_IMAGE@$SRC_DIGEST "${IMAGE_URI_TEMP}:3h" - declare "IMG${i}=${IMAGE_URI_TEMP}@${SRC_DIGEST}" -done - - -echo "IMG01: $IMG01, IMG02: $IMG02, TIMESTAMP_SERVER_URL: $TIMESTAMP_SERVER_URL" - -# use gencert to generate two CAs (for testing certificate bundle feature), -# keys and certificates -echo "generate CAs, keys and certificates with gencert" -passwd=$(uuidgen | head -c 32 | tr 'A-Z' 'a-z') -rm -f *.pem import-cosign.* -for i in 01 02; do - go run test/gencert/main.go -output-suffix "$i" -intermediate - COSIGN_PASSWORD="$passwd" $COSIGN_CLI import-key-pair --key ca-intermediate-key${i}.pem --output-key-prefix import-cosign${i} - IMG="IMG${i}" - cat ca-intermediate${i}.pem ca-root${i}.pem > certchain${i}.pem - COSIGN_PASSWORD="$passwd" $COSIGN_CLI sign --timestamp-server-url "${TIMESTAMP_SERVER_URL}" \ - --upload=true --tlog-upload=false --key import-cosign${i}.key --certificate-chain certchain${i}.pem --cert cert${i}.pem "${!IMG}" - # key is now longer needed - rm -f key${i}.pem import-cosign${i}.* -done - -# create a certificate bundle - concatenate both generated CA certificates -ls -l *.pem -cat ca-root01.pem ca-root02.pem > ca-roots.pem -cat ca-intermediate01.pem ca-intermediate02.pem > ca-intermediates.pem - -echo "cosign verify:" -for i in 01 02; do - IMG="IMG${i}" - # first try with --certificate-chain parameter - $COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \ - --certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \ - --certificate-chain certchain${i}.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE "${!IMG}" - - # then do the same but now with --ca-roots and --ca-intermediates parameters - $COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \ - --certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \ - --ca-roots ca-roots.pem --ca-intermediates ca-intermediates.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE "${!IMG}" -done - -# cleanup -rm -fr *.pem timestamp-chain diff --git a/test/e2e_tsa_mtls.sh b/test/e2e_tsa_mtls.sh deleted file mode 100755 index cc06eb04e5b..00000000000 --- a/test/e2e_tsa_mtls.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright 2023 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. - -set -exuo pipefail - -## Requirements -# - cosign -# - crane -# - go - -CERT_BASE="test/testdata" - -# the certificates listed below are generated with the `gen-tsa-mtls-certs.sh` script. -TIMESTAMP_CACERT=$CERT_BASE/tsa-mtls-ca.crt -TIMESTAMP_CLIENT_CERT=$CERT_BASE/tsa-mtls-client.crt -TIMESTAMP_CLIENT_KEY=$CERT_BASE/tsa-mtls-client.key -TIMESTAMP_SERVER_CERT=$CERT_BASE/tsa-mtls-server.crt -TIMESTAMP_SERVER_KEY=$CERT_BASE/tsa-mtls-server.key -TIMESTAMP_SERVER_NAME="server.example.com" -TIMESTAMP_SERVER_URL=https://localhost:3000/api/v1/timestamp -TIMESTAMP_CHAIN_FILE="timestamp-chain" - -set +e -COSIGN_CLI=./cosign -command -v timestamp-server >& /dev/null -exit_code=$? -set -e -if [[ $exit_code != 0 ]]; then - rm -fr /tmp/timestamp-authority - git clone https://github.com/sigstore/timestamp-authority /tmp/timestamp-authority - pushd /tmp/timestamp-authority - make - export PATH="/tmp/timestamp-authority/bin:$PATH" - popd -fi - -timestamp-server serve --disable-ntp-monitoring --tls-host 0.0.0.0 --tls-port 3000 \ - --scheme https --tls-ca $TIMESTAMP_CACERT --tls-key $TIMESTAMP_SERVER_KEY \ - --tls-certificate $TIMESTAMP_SERVER_CERT & - -sleep 1 -curl -k -s --key test/testdata/tsa-mtls-client.key \ - --cert test/testdata/tsa-mtls-client.crt \ - --cacert test/testdata/tsa-mtls-ca.crt https://localhost:3000/api/v1/timestamp/certchain \ - > $TIMESTAMP_CHAIN_FILE -echo "DONE: $(ls -l $TIMESTAMP_CHAIN_FILE)" - -IMG=${IMAGE_URI_DIGEST:-} -if [[ "$#" -ge 1 ]]; then - IMG=$1 -elif [[ -z "${IMG}" ]]; then - # Upload an image to ttl.sh - commands from https://docs.sigstore.dev/cosign/keyless/ - SRC_IMAGE=busybox - SRC_DIGEST=$(crane digest busybox) - IMAGE_URI=ttl.sh/$(uuidgen | head -c 8 | tr 'A-Z' 'a-z') - crane cp $SRC_IMAGE@$SRC_DIGEST $IMAGE_URI:3h - IMG=$IMAGE_URI@$SRC_DIGEST -fi - -echo "IMG (IMAGE_URI_DIGEST): $IMG, TIMESTAMP_SERVER_URL: $TIMESTAMP_SERVER_URL" - -rm -f *.pem import-cosign.* key.pem - -# use gencert to generate CA, keys and certificates -echo "generate keys and certificates with gencert" - -passwd=$(uuidgen | head -c 32 | tr 'A-Z' 'a-z') -rm -f *.pem import-cosign.* && go run test/gencert/main.go && COSIGN_PASSWORD="$passwd" $COSIGN_CLI import-key-pair --key key.pem - -COSIGN_PASSWORD="$passwd" $COSIGN_CLI sign --timestamp-server-url "${TIMESTAMP_SERVER_URL}" \ - --timestamp-client-cacert ${TIMESTAMP_CACERT} --timestamp-client-cert ${TIMESTAMP_CLIENT_CERT} \ - --timestamp-client-key ${TIMESTAMP_CLIENT_KEY} --timestamp-server-name ${TIMESTAMP_SERVER_NAME}\ - --upload=true --tlog-upload=false --key import-cosign.key --certificate-chain ca-root.pem --cert cert.pem $IMG - -# key is now longer needed -rm -f key.pem import-cosign.* - -echo "cosign verify:" -$COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \ - --certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \ - --certificate-chain ca-root.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE $IMG -# alternative invocation of 'cosign verify' using '--ca-roots' instead of '--certificate-chain' -$COSIGN_CLI verify --insecure-ignore-tlog --insecure-ignore-sct --check-claims=true \ - --certificate-identity-regexp 'xyz@nosuchprovider.com' --certificate-oidc-issuer-regexp '.*' \ - --ca-roots ca-root.pem --timestamp-certificate-chain $TIMESTAMP_CHAIN_FILE $IMG - -# cleanup -rm -fr ca-key.pem ca-root.pem cert.pem timestamp-chain /tmp/timestamp-authority -pkill -f 'timestamp-server' From ec1abdfa1322a93ab31b6155483f6182d1b90936 Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Mon, 1 Jul 2024 14:29:32 +0200 Subject: [PATCH 12/13] remove unused certFile param to verifyCertBundle Signed-off-by: Dmitry S --- cmd/cosign/cli/verify/verify.go | 2 +- test/e2e_test.go | 10 +++++----- test/helpers.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 010cef4afb4..a4e657601e4 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -295,7 +295,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return err } default: - return errors.New("internal error in handling CertChain and RootCerts - default case should never happen") + return errors.New("no certificate chain provided to verify certificate") } if c.SCTRef != "" { diff --git a/test/e2e_test.go b/test/e2e_test.go index f6ead509f97..d7801e651db 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -151,7 +151,7 @@ func TestSignVerifyCertBundle(t *testing.T) { ctx := context.Background() // Verify should fail at first - mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, certFile, imgName, true, nil, "", true), t) + mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, nil, "", true), t) // So should download mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) @@ -170,13 +170,13 @@ func TestSignVerifyCertBundle(t *testing.T) { // Now verify and download should work! ignoreTlog := true - must(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, certFile, imgName, true, nil, "", ignoreTlog), t) + must(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, nil, "", ignoreTlog), t) // verification with certificate chain instead of root/intermediate files should work as well must(verifyCertChain(pubKeyPath, certChainFile, certFile, imgName, true, nil, "", ignoreTlog), t) must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) // Look for a specific annotation - mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", ignoreTlog), t) + mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", ignoreTlog), t) so.AnnotationOptions = options.AnnotationOptions{ Annotations: []string{"foo=bar"}, @@ -185,10 +185,10 @@ func TestSignVerifyCertBundle(t *testing.T) { must(sign.SignCmd(ro, ko, so, []string{imgName}), t) // It should match this time. - must(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", ignoreTlog), t) + must(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", ignoreTlog), t) // But two doesn't work - mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, certFile, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}, "", ignoreTlog), t) + mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}, "", ignoreTlog), t) } func TestSignVerifyClean(t *testing.T) { diff --git a/test/helpers.go b/test/helpers.go index 7747bebdd90..ca9d4f4377b 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -108,7 +108,7 @@ var verifyCertChain = func(keyRef, certChain, certFile, imageRef string, checkCl return cmd.Exec(context.Background(), args) } -var verifyCertBundle = func(keyRef, caCertFile, caIntermediates, certFile, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment string, skipTlogVerify bool) error { +var verifyCertBundle = func(keyRef, caCertFile, caIntermediateCertFile, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment string, skipTlogVerify bool) error { cmd := cliverify.VerifyCommand{ KeyRef: keyRef, RekorURL: rekorURL, @@ -119,7 +119,7 @@ var verifyCertBundle = func(keyRef, caCertFile, caIntermediates, certFile, image MaxWorkers: 10, IgnoreTlog: skipTlogVerify, CertVerifyOptions: options.CertVerifyOptions{ - CAIntermediates: caIntermediates, + CAIntermediates: caIntermediateCertFile, CARoots: caCertFile, CertOidcIssuerRegexp: ".*", CertIdentityRegexp: ".*", From f1be17db0fc1286eb7ac663745e43b0ec13a7def Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Mon, 1 Jul 2024 19:05:43 +0200 Subject: [PATCH 13/13] remove duplicate test functions Signed-off-by: Dmitry S --- test/e2e_test.go | 242 ----------------------------------------------- 1 file changed, 242 deletions(-) diff --git a/test/e2e_test.go b/test/e2e_test.go index d7801e651db..c12754e8b86 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -38,7 +38,6 @@ import ( "testing" "time" - "github.com/go-openapi/strfmt" "github.com/google/go-cmp/cmp" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" @@ -1056,151 +1055,6 @@ func TestVerifyWithCARoots(t *testing.T) { } } -func TestAttachWithRFC3161Timestamp(t *testing.T) { - ctx := context.Background() - // TSA server needed to create timestamp - viper.Set("timestamp-signer", "memory") - viper.Set("timestamp-signer-hash", "sha256") - apiServer := server.NewRestAPIServer("localhost", 0, []string{"http"}, false, 10*time.Second, 10*time.Second) - server := httptest.NewServer(apiServer.GetHandler()) - t.Cleanup(server.Close) - - repo, stop := reg(t) - defer stop() - td := t.TempDir() - - imgName := path.Join(repo, "cosign-attach-timestamp-e2e") - - _, _, cleanup := mkimage(t, imgName) - defer cleanup() - - b := bytes.Buffer{} - must(generate.GenerateCmd(context.Background(), options.RegistryOptions{}, imgName, nil, &b), t) - - rootCert, rootKey, _ := GenerateRootCa() - subCert, subKey, _ := GenerateSubordinateCa(rootCert, rootKey) - leafCert, privKey, _ := GenerateLeafCert("subject@mail.com", "oidc-issuer", subCert, subKey) - pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) - pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - payloadref := mkfile(b.String(), td, t) - - h := sha256.Sum256(b.Bytes()) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - b64signature := base64.StdEncoding.EncodeToString([]byte(signature)) - sigRef := mkfile(b64signature, td, t) - pemleafRef := mkfile(string(pemLeaf), td, t) - pemrootRef := mkfile(string(pemRoot), td, t) - - certchainRef := mkfile(string(append(pemSub[:], pemRoot[:]...)), td, t) - - t.Setenv("SIGSTORE_ROOT_FILE", pemrootRef) - // reset the roots to use the root pointed by the environment variable SIGSTORE_ROOT_FILE - if err := fulcioroots.ReInit(); err != nil { - t.Fatal(err) - } - - tsclient, err := tsaclient.GetTimestampClient(server.URL) - if err != nil { - t.Error(err) - } - - chain, err := tsclient.Timestamp.GetTimestampCertChain(nil) - if err != nil { - t.Fatalf("unexpected error getting timestamp chain: %v", err) - } - - file, err := os.CreateTemp(os.TempDir(), "tempfile") - if err != nil { - t.Fatalf("error creating temp file: %v", err) - } - defer os.Remove(file.Name()) - _, err = file.WriteString(chain.Payload) - if err != nil { - t.Fatalf("error writing chain payload to temp file: %v", err) - } - - tsBytes, err := tsa.GetTimestampedSignature(signature, client.NewTSAClient(server.URL+"/api/v1/timestamp")) - if err != nil { - t.Fatalf("unexpected error creating timestamp: %v", err) - } - rfc3161TSRef := mkfile(string(tsBytes), td, t) - - // Upload it! - err = attach.SignatureCmd(ctx, options.RegistryOptions{}, sigRef, payloadref, pemleafRef, certchainRef, rfc3161TSRef, "", imgName) - if err != nil { - t.Fatal(err) - } - - must(verifyKeylessTSA(imgName, file.Name(), true, true), t) -} - -func TestAttachWithRekorBundle(t *testing.T) { - ctx := context.Background() - - repo, stop := reg(t) - defer stop() - td := t.TempDir() - - imgName := path.Join(repo, "cosign-attach-timestamp-e2e") - - _, _, cleanup := mkimage(t, imgName) - defer cleanup() - - b := bytes.Buffer{} - must(generate.GenerateCmd(context.Background(), options.RegistryOptions{}, imgName, nil, &b), t) - - rootCert, rootKey, _ := GenerateRootCa() - subCert, subKey, _ := GenerateSubordinateCa(rootCert, rootKey) - leafCert, privKey, _ := GenerateLeafCert("subject@mail.com", "oidc-issuer", subCert, subKey) - pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) - pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - payloadref := mkfile(b.String(), td, t) - - h := sha256.Sum256(b.Bytes()) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - b64signature := base64.StdEncoding.EncodeToString([]byte(signature)) - sigRef := mkfile(b64signature, td, t) - pemleafRef := mkfile(string(pemLeaf), td, t) - pemrootRef := mkfile(string(pemRoot), td, t) - - t.Setenv("SIGSTORE_ROOT_FILE", pemrootRef) - - certchainRef := mkfile(string(append(pemSub[:], pemRoot[:]...)), td, t) - - localPayload := cosign.LocalSignedPayload{ - Base64Signature: b64signature, - Cert: string(pemLeaf), - Bundle: &bundle.RekorBundle{ - SignedEntryTimestamp: strfmt.Base64("MEUCIEDcarEwRYkrxE9ne+kzEVvUhnWaauYzxhUyXOLy1hwAAiEA4VdVCvNRs+D/5o33C2KBy+q2YX3lP4Y7nqRFU+K3hi0="), - Payload: bundle.RekorPayload{ - Body: "REMOVED", - IntegratedTime: 1631646761, - LogIndex: 693591, - LogID: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", - }, - }, - } - - jsonBundle, err := json.Marshal(localPayload) - if err != nil { - t.Fatal(err) - } - bundlePath := filepath.Join(td, "bundle.json") - if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil { - t.Fatal(err) - } - - // Upload it! - err = attach.SignatureCmd(ctx, options.RegistryOptions{}, sigRef, payloadref, pemleafRef, certchainRef, "", bundlePath, imgName) - if err != nil { - t.Fatal(err) - } -} - func TestRekorBundle(t *testing.T) { td := t.TempDir() err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td) @@ -1832,102 +1686,6 @@ func TestGenerate(t *testing.T) { equals(ss.Optional["foo"], "bar", t) } -func TestUploadDownload(t *testing.T) { - repo, stop := reg(t) - defer stop() - td := t.TempDir() - ctx := context.Background() - - testCases := map[string]struct { - signature string - signatureType attach.SignatureArgType - expectedErr bool - }{ - "stdin containing signature": { - signature: "testsignatureraw", - signatureType: attach.StdinSignature, - expectedErr: false, - }, - "file containing signature": { - signature: "testsignaturefile", - signatureType: attach.FileSignature, - expectedErr: false, - }, - "raw signature as argument": { - signature: "testsignatureraw", - signatureType: attach.RawSignature, - expectedErr: true, - }, - "empty signature as argument": { - signature: "", - signatureType: attach.RawSignature, - expectedErr: true, - }, - } - - imgName := path.Join(repo, "cosign-e2e") - for testName, testCase := range testCases { - t.Run(testName, func(t *testing.T) { - ref, _, cleanup := mkimage(t, imgName) - payload := "testpayload" - payloadPath := mkfile(payload, td, t) - signature := base64.StdEncoding.EncodeToString([]byte(testCase.signature)) - restoreStdin := func() {} - - var sigRef string - if testCase.signatureType == attach.FileSignature { - sigRef = mkfile(signature, td, t) - } else if testCase.signatureType == attach.StdinSignature { - sigRef = "-" - restoreStdin = mockStdin(signature, td, t) - } else { - sigRef = signature - } - // Upload it! - err := attach.SignatureCmd(ctx, options.RegistryOptions{}, sigRef, payloadPath, "", "", "", "", imgName) - if testCase.expectedErr { - mustErr(err, t) - } else { - must(err, t) - } - restoreStdin() - - // Now download it! - se, err := ociremote.SignedEntity(ref, ociremote.WithRemoteOptions(registryClientOpts(ctx)...)) - must(err, t) - sigs, err := se.Signatures() - must(err, t) - signatures, err := sigs.Get() - must(err, t) - - if testCase.expectedErr { - if len(signatures) != 0 { - t.Fatalf("unexpected signatures %d, wanted 0", len(signatures)) - } - } else { - if len(signatures) != 1 { - t.Fatalf("unexpected signatures %d, wanted 1", len(signatures)) - } - - if b64sig, err := signatures[0].Base64Signature(); err != nil { - t.Fatalf("Base64Signature() = %v", err) - } else if diff := cmp.Diff(b64sig, signature); diff != "" { - t.Error(diff) - } - - if p, err := signatures[0].Payload(); err != nil { - t.Fatalf("Payload() = %v", err) - } else if diff := cmp.Diff(p, []byte(payload)); diff != "" { - t.Error(diff) - } - } - - // Now delete it! - cleanup() - }) - } -} - func TestSaveLoad(t *testing.T) { td := t.TempDir() err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td)