From 40fc15f25abb8b95ee3668539ffccf8b0bddd7f1 Mon Sep 17 00:00:00 2001 From: Dmitry Savintsev Date: Mon, 1 Jul 2024 21:20:12 +0200 Subject: [PATCH] add --ca-roots and --ca-intermediates flags to 'cosign verify' (#3464) * 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 * 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 * add functional tests for --ca-roots flag Signed-off-by: Dmitry S * setup-crane action for e2e_test_pkcs11.sh Signed-off-by: Dmitry S * rebase on trunk Signed-off-by: Dmitry Savintsev * transform gencert subpackage to helper function Signed-off-by: Dmitry S * use the trunk version of workflows/e2e-tests.yml Signed-off-by: Dmitry S * correct certificate generation for e2e tests Signed-off-by: Dmitry S * refactor test cert/keys generation and corresponding test Signed-off-by: Dmitry S * add license header Signed-off-by: Dmitry S * remove test shell scripts Signed-off-by: Dmitry S * remove unused certFile param to verifyCertBundle Signed-off-by: Dmitry S * remove duplicate test functions Signed-off-by: Dmitry S --------- Signed-off-by: Dmitry S Signed-off-by: Dmitry Savintsev --- cmd/cosign/cli/options/certificate.go | 17 +- cmd/cosign/cli/verify.go | 6 + cmd/cosign/cli/verify/verify.go | 53 ++++- 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 | 8 +- test/e2e_test.go | 264 ++++++++++++++++++++++ test/helpers.go | 311 ++++++++++++++++++++++++++ test/helpers_test.go | 182 +++++++++++++++ 12 files changed, 847 insertions(+), 14 deletions(-) create mode 100644 test/helpers_test.go diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go index e89f257f9e3..3df7b4b962e 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -33,6 +33,8 @@ type CertVerifyOptions struct { CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string + CAIntermediates string + CARoots string CertChain string SCT string IgnoreSCT bool @@ -75,12 +77,25 @@ 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.") + _ = 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") + "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 d8c7ed9cd06..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,8 @@ 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, SCTRef: o.CertVerify.SCT, diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 31cb88c2641..a4e657601e4 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -59,6 +59,8 @@ type VerifyCommand struct { CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string + CAIntermediates string + CARoots string CertChain string CertOidcProvider string IgnoreSCT bool @@ -173,7 +175,8 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } } if keylessVerification(c.KeyRef, c.Sk) { - if c.CertChain != "" { + switch { + case c.CertChain != "": chain, err := loadCertChainFromFileOrURL(c.CertChain) if err != nil { return err @@ -186,9 +189,32 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { 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). + 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) + } + } + if c.CAIntermediates != "" { + caIntermediates, err := loadCertChainFromFileOrURL(c.CAIntermediates) + if err != nil { + return err + } + 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 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) @@ -237,8 +263,9 @@ 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 + 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 { return fmt.Errorf("getting Fulcio roots: %w", err) @@ -251,7 +278,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { if err != nil { return err } - } else { + case c.CertChain != "": // Verify certificate with chain chain, err := loadCertChainFromFileOrURL(c.CertChain) if err != nil { @@ -261,7 +288,16 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { 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("no certificate chain provided to verify certificate") } + if c.SCTRef != "" { sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) if err != nil { @@ -269,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 7817b32a67b..aea4d4aadde 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -55,8 +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 + --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 39768ed1973..dc1af148c48 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -49,8 +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 + --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 e7c26e17f7a..9b747ab9ef0 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -59,8 +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 + --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 f688ee7e8fc..f9d199ec45d 100644 --- a/doc/cosign_verify-blob-attestation.md +++ b/doc/cosign_verify-blob-attestation.md @@ -29,8 +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 + --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 72089672249..e4fb5c16a22 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -59,8 +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 + --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 5bf88f6de73..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,8 +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 + --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/test/e2e_test.go b/test/e2e_test.go index da874ae329f..c12754e8b86 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" @@ -55,6 +60,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" @@ -123,6 +130,66 @@ 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() + + _, privKeyPath, pubKeyPath := keypair(t, td) + caCertFile, _ /* caPrivKeyFile */, caIntermediateCertFile, _ /* caIntermediatePrivKeyFile */, certFile, certChainFile, err := generateCertificateBundleFiles(td, true, "foobar") + + ctx := context.Background() + // Verify should fail at first + mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, nil, "", true), t) + // So should download + mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) + + // Now sign the image + ko := options.KeyOpts{ + KeyRef: privKeyPath, + 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! + ignoreTlog := true + 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, imgName, true, map[string]interface{}{"foo": "bar"}, "", ignoreTlog), 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(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", ignoreTlog), t) + + // But two doesn't work + mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}, "", ignoreTlog), t) +} + func TestSignVerifyClean(t *testing.T) { td := t.TempDir() err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td) @@ -791,6 +858,203 @@ 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 TestRekorBundle(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 960ca13810c..ca9d4f4377b 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,50 @@ 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, caIntermediateCertFile, 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{ + CAIntermediates: caIntermediateCertFile, + CARoots: caCertFile, + CertOidcIssuerRegexp: ".*", + CertIdentityRegexp: ".*", + }, + } + + 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, @@ -114,6 +168,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{ @@ -416,3 +497,233 @@ 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, + certChainFile string, + err error, +) { + caCertBuf, caPrivKeyBuf, caIntermediateCertBuf, caIntermediatePrivKeyBuf, certBuf, certChainBuf, err := generateCertificateBundle(genIntermediate) + if err != nil { + err = fmt.Errorf("error generating certificate bundle: %w", err) + return + } + 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 %s: %w", caCertFile, err) + return + } + 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 %s: %w", caPrivKeyFile, err) + return + } + if genIntermediate { + 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 %s: %w", caIntermediateCertFile, err) + return + } + 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 %s: %w", caIntermediatePrivKeyFile, err) + return + } + } + 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 %s: %w", certFile, 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 %s: %w", certFile, err) + return + } + return +} + +func generateCertificateBundle(genIntermediate bool) ( + caCertBuf *bytes.Buffer, + caPrivKeyBuf *bytes.Buffer, + caIntermediateCertBuf *bytes.Buffer, + caIntermediatePrivKeyBuf *bytes.Buffer, + certBuf *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) + } + // 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 + } + + // 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, certBundleBuf, nil +} diff --git a/test/helpers_test.go b/test/helpers_test.go new file mode 100644 index 00000000000..a1e9ae0f827 --- /dev/null +++ b/test/helpers_test.go @@ -0,0 +1,182 @@ +// +// 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 + +import ( + "crypto/x509" + "encoding/pem" + "io/ioutil" + "log" + "testing" +) + +func TestGenerateCertificateBundleFiles(t *testing.T) { + for _, tt := range []struct { + name string + genIntermediate bool + }{ + { + name: "without intermediate", + genIntermediate: false, + }, + { + name: "with intermediate", + genIntermediate: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + td := t.TempDir() + suffix := "foo" + 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 +}