From 7b04f5e33dd99c0f2dfeb6ff4a9c7ef214cb4748 Mon Sep 17 00:00:00 2001 From: Prem Kumar Kalle Date: Tue, 25 Apr 2023 22:58:14 -0700 Subject: [PATCH] Add functionality to use custom certificate configuration while interacting with central repository - Added functionality to use the custom certificate configuration(configured by the user using tanzu config cert command) while interacting with central repository endpoint having self-signed certs/expired certs Signed-off-by: Prem Kumar Kalle --- go.mod | 2 +- go.sum | 4 +- pkg/carvelhelpers/fetcher.go | 46 ++-------- pkg/carvelhelpers/image_operations.go | 38 ++++++-- pkg/clientconfighelpers/helpers.go | 107 ++++++++++++++++------ pkg/clientconfighelpers/helpers_test.go | 117 ++++++++++++++++++++++++ pkg/constants/config_variables.go | 3 - pkg/cosignhelper/cosignverify.go | 67 +++++++++++++- pkg/discovery/oci_dbbacked.go | 44 ++++++++- pkg/discovery/oci_dbbacked_test.go | 90 +++++++++++++++++- 10 files changed, 429 insertions(+), 89 deletions(-) create mode 100644 pkg/clientconfighelpers/helpers_test.go diff --git a/go.mod b/go.mod index 636921d0d..2fb3cd4f1 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/vmware-tanzu/carvel-imgpkg v0.36.1 github.com/vmware-tanzu/carvel-ytt v0.40.0 github.com/vmware-tanzu/tanzu-framework/capabilities/client v0.0.0-20230415084831-9331f55d2999 - github.com/vmware-tanzu/tanzu-plugin-runtime v0.90.0-alpha.0.0.20230425191535-014e58e69078 + github.com/vmware-tanzu/tanzu-plugin-runtime v0.90.0-alpha.0.0.20230426214812-20c441a939f1 go.pinniped.dev v0.20.0 go.uber.org/multierr v1.8.0 golang.org/x/mod v0.9.0 diff --git a/go.sum b/go.sum index 42c6eb457..b6248f2bb 100644 --- a/go.sum +++ b/go.sum @@ -1289,8 +1289,8 @@ github.com/vmware-tanzu/tanzu-framework/apis/run v0.0.0-20221207131309-7323ca04b github.com/vmware-tanzu/tanzu-framework/apis/run v0.0.0-20221207131309-7323ca04b86c/go.mod h1:ukZpKQ0hf5bjWdJLjn2M6qXP+9giZWQPxt8nOfrCR+o= github.com/vmware-tanzu/tanzu-framework/capabilities/client v0.0.0-20230415084831-9331f55d2999 h1:WITDH+wpdl/clw1hwy+2jtq4Pt//i/Mq9lQXGwg3q4c= github.com/vmware-tanzu/tanzu-framework/capabilities/client v0.0.0-20230415084831-9331f55d2999/go.mod h1:umFZBUfJ8VI3p0VO/xocuE+4fO9s9QbytEiOqFcH/Tw= -github.com/vmware-tanzu/tanzu-plugin-runtime v0.90.0-alpha.0.0.20230425191535-014e58e69078 h1:FnqG7kCmltbUgnGJiDcokvQT1Bbvs38IrmVIUFj4P34= -github.com/vmware-tanzu/tanzu-plugin-runtime v0.90.0-alpha.0.0.20230425191535-014e58e69078/go.mod h1:FlvOcF26rX4EA+ADjYTJdFh6WVur6O4jh25FDP9Lp7E= +github.com/vmware-tanzu/tanzu-plugin-runtime v0.90.0-alpha.0.0.20230426214812-20c441a939f1 h1:k3PPUHUwLcc7dG5swAhA5a6UsgqJPAEkNDS/kvDyUzc= +github.com/vmware-tanzu/tanzu-plugin-runtime v0.90.0-alpha.0.0.20230426214812-20c441a939f1/go.mod h1:FlvOcF26rX4EA+ADjYTJdFh6WVur6O4jh25FDP9Lp7E= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/go-gitlab v0.73.1 h1:UMagqUZLJdjss1SovIC+kJCH4k2AZWXl58gJd38Y/hI= github.com/xanzy/go-gitlab v0.73.1/go.mod h1:d/a0vswScO7Agg1CZNz15Ic6SSvBG9vfw8egL99t4kA= diff --git a/pkg/carvelhelpers/fetcher.go b/pkg/carvelhelpers/fetcher.go index 25b1dee87..8745baeca 100644 --- a/pkg/carvelhelpers/fetcher.go +++ b/pkg/carvelhelpers/fetcher.go @@ -4,17 +4,11 @@ package carvelhelpers import ( - "os" - "runtime" - "strings" - "github.com/pkg/errors" ctlimg "github.com/vmware-tanzu/carvel-imgpkg/pkg/imgpkg/registry" "github.com/vmware-tanzu/tanzu-cli/pkg/clientconfighelpers" - "github.com/vmware-tanzu/tanzu-cli/pkg/configpaths" - "github.com/vmware-tanzu/tanzu-cli/pkg/constants" "github.com/vmware-tanzu/tanzu-cli/pkg/registry" ) @@ -36,40 +30,20 @@ func GetImageDigest(imageWithTag string) (string, string, error) { return NewImageOperationsImpl().GetImageDigest(imageWithTag) } -// newRegistry returns a new registry object by also -// taking into account for any custom registry or proxy -// environment variable provided by the user -func newRegistry() (registry.Registry, error) { - verifyCerts := true - skipVerifyCerts := os.Getenv(constants.ConfigVariableCustomImageRepositorySkipTLSVerify) - if strings.EqualFold(skipVerifyCerts, "true") { - verifyCerts = false - } - +// newRegistry returns a new registry object by also taking +// into account for any custom registry provided by the user +func newRegistry(registryName string) (registry.Registry, error) { registryOpts := &ctlimg.Opts{ - VerifyCerts: verifyCerts, + VerifyCerts: true, Anon: true, } - if runtime.GOOS == "windows" { - err := clientconfighelpers.AddRegistryTrustedRootCertsFileForWindows(registryOpts) - if err != nil { - return nil, err - } + regCertOptions, err := clientconfighelpers.GetRegistryCertOptions(registryName) + if err != nil { + return nil, errors.Wrapf(err, "unable to get the registry certificate configuration") } - - caCertBytes, err := clientconfighelpers.GetCustomRepositoryCaCertificateForClient() - if err == nil && len(caCertBytes) != 0 { - filePath, err := configpaths.GetRegistryCertFile() - if err != nil { - return nil, err - } - err = os.WriteFile(filePath, caCertBytes, 0o644) - if err != nil { - return nil, errors.Wrapf(err, "failed to write the custom image registry CA cert to file '%s'", filePath) - } - registryOpts.CACertPaths = append(registryOpts.CACertPaths, filePath) - } - + registryOpts.CACertPaths = regCertOptions.CACertPaths + registryOpts.VerifyCerts = !(regCertOptions.SkipCertVerify) + registryOpts.Insecure = regCertOptions.Insecure return registry.New(registryOpts) } diff --git a/pkg/carvelhelpers/image_operations.go b/pkg/carvelhelpers/image_operations.go index a7235898a..777f9c22e 100644 --- a/pkg/carvelhelpers/image_operations.go +++ b/pkg/carvelhelpers/image_operations.go @@ -5,6 +5,8 @@ package carvelhelpers import ( "github.com/pkg/errors" + + "github.com/vmware-tanzu/tanzu-cli/pkg/clientconfighelpers" ) // ImageOperationOptions implements the ImageOperationsImpl interface by using `imgpkg` library @@ -18,7 +20,11 @@ func NewImageOperationsImpl() ImageOperationsImpl { // CopyImageToTar downloads the image as tar file // This is equivalent to `imgpkg copy --image --to-tar ` command func (i *ImageOperationOptions) CopyImageToTar(sourceImageName, destTarFile string) error { - reg, err := newRegistry() + registryName, err := clientconfighelpers.GetRegistryName(sourceImageName) + if err != nil { + return err + } + reg, err := newRegistry(registryName) if err != nil { return errors.Wrapf(err, "unable to initialize registry") } @@ -28,7 +34,11 @@ func (i *ImageOperationOptions) CopyImageToTar(sourceImageName, destTarFile stri // CopyImageFromTar publishes the image to destination repository from specified tar file // This is equivalent to `imgpkg copy --tar --to-repo ` command func (i *ImageOperationOptions) CopyImageFromTar(sourceTarFile, destImageRepo string) error { - reg, err := newRegistry() + registryName, err := clientconfighelpers.GetRegistryName(destImageRepo) + if err != nil { + return err + } + reg, err := newRegistry(registryName) if err != nil { return errors.Wrapf(err, "unable to initialize registry") } @@ -38,7 +48,11 @@ func (i *ImageOperationOptions) CopyImageFromTar(sourceTarFile, destImageRepo st // DownloadImageAndSaveFilesToDir reads a plain OCI image and saves its // files to the specified location. func (i *ImageOperationOptions) DownloadImageAndSaveFilesToDir(imageWithTag, destinationDir string) error { - reg, err := newRegistry() + registryName, err := clientconfighelpers.GetRegistryName(imageWithTag) + if err != nil { + return err + } + reg, err := newRegistry(registryName) if err != nil { return errors.Wrapf(err, "unable to initialize registry") } @@ -53,7 +67,11 @@ func (i *ImageOperationOptions) DownloadImageAndSaveFilesToDir(imageWithTag, des // It takes os environment variables for custom repository and proxy // configuration into account while downloading image from repository func (i *ImageOperationOptions) GetFilesMapFromImage(imageWithTag string) (map[string][]byte, error) { - reg, err := newRegistry() + registryName, err := clientconfighelpers.GetRegistryName(imageWithTag) + if err != nil { + return nil, err + } + reg, err := newRegistry(registryName) if err != nil { return nil, errors.Wrapf(err, "unable to initialize registry") } @@ -62,7 +80,11 @@ func (i *ImageOperationOptions) GetFilesMapFromImage(imageWithTag string) (map[s // GetImageDigest gets digest of the image func (i *ImageOperationOptions) GetImageDigest(imageWithTag string) (string, string, error) { - reg, err := newRegistry() + registryName, err := clientconfighelpers.GetRegistryName(imageWithTag) + if err != nil { + return "", "", err + } + reg, err := newRegistry(registryName) if err != nil { return "", "", errors.Wrapf(err, "unable to initialize registry") } @@ -77,7 +99,11 @@ func (i *ImageOperationOptions) GetImageDigest(imageWithTag string) (string, str // PushImage publishes the image to the specified location func (i *ImageOperationOptions) PushImage(imageWithTag string, filePaths []string) error { - reg, err := newRegistry() + registryName, err := clientconfighelpers.GetRegistryName(imageWithTag) + if err != nil { + return err + } + reg, err := newRegistry(registryName) if err != nil { return errors.Wrapf(err, "unable to initialize registry") } diff --git a/pkg/clientconfighelpers/helpers.go b/pkg/clientconfighelpers/helpers.go index 179a2b3fe..8e07f2220 100644 --- a/pkg/clientconfighelpers/helpers.go +++ b/pkg/clientconfighelpers/helpers.go @@ -8,50 +8,87 @@ package clientconfighelpers import ( "encoding/base64" "os" + "runtime" + "strconv" - ctlimg "github.com/vmware-tanzu/carvel-imgpkg/pkg/imgpkg/registry" - + regname "github.com/google/go-containerregistry/pkg/name" "github.com/pkg/errors" "github.com/vmware-tanzu/tanzu-cli/pkg/configpaths" "github.com/vmware-tanzu/tanzu-cli/pkg/constants" + configlib "github.com/vmware-tanzu/tanzu-plugin-runtime/config" + configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" ) -// GetCustomRepositoryCaCertificateForClient returns CA certificate to use with cli client -// This function reads the CA certificate from following variables in decreasing order of precedence: -// 1. PROXY_CA_CERT -// 2. TKG_PROXY_CA_CERT -// 3. TKG_CUSTOM_IMAGE_REPOSITORY_CA_CERTIFICATE -func GetCustomRepositoryCaCertificateForClient() ([]byte, error) { - caCert := "" - var errProxyCACert, errTkgProxyCACertValue, errCustomImageRepoCACert error - var proxyCACertValue, tkgProxyCACertValue, customImageRepoCACert string +type RegistryCertOptions struct { + CACertPaths []string + SkipCertVerify bool + Insecure bool +} - // Get the proxy configuration from os environment variable - proxyCACertValue = os.Getenv(constants.ProxyCACert) - tkgProxyCACertValue = os.Getenv(constants.TKGProxyCACert) - customImageRepoCACert = os.Getenv(constants.ConfigVariableCustomImageRepositoryCaCertificate) +func GetRegistryCertOptions(registryName string) (*RegistryCertOptions, error) { + registryCertOpts := &RegistryCertOptions{ + SkipCertVerify: false, + Insecure: false, + } + + if runtime.GOOS == "windows" { + err := AddRegistryTrustedRootCertsFileForWindows(registryCertOpts) + if err != nil { + return nil, err + } + } - if errProxyCACert == nil && proxyCACertValue != "" { - caCert = proxyCACertValue - } else if errTkgProxyCACertValue == nil && tkgProxyCACertValue != "" { - caCert = tkgProxyCACertValue - } else if errCustomImageRepoCACert == nil && customImageRepoCACert != "" { - caCert = customImageRepoCACert - } else { - // return empty content when none is specified - return []byte{}, nil + // check if the custom cert data is configured for the registry + if exists, _ := configlib.CertExists(registryName); !exists { + return registryCertOpts, nil + } + cert, err := configlib.GetCert(registryName) + if err != nil { + return nil, errors.Wrapf(err, "failed to get the custom certificate configuration for host %q", registryName) } - decoded, err := base64.StdEncoding.DecodeString(caCert) + err = updateRegistryCertOptions(cert, registryCertOpts) if err != nil { - return nil, errors.Wrap(err, "unable to decode the base64-encoded custom registry CA certificate string") + return nil, errors.Wrapf(err, "failed to updated the registry cert options") } - return decoded, nil + + return registryCertOpts, nil } -// AddRegistryTrustedRootCertsFileForWindows adds CA certificate to registry options for windows environments -func AddRegistryTrustedRootCertsFileForWindows(registryOpts *ctlimg.Opts) error { +// updateRegistryCertOptions sets the registry options by taking the custom certificate data configured for registry as input +func updateRegistryCertOptions(cert *configtypes.Cert, registryCertOpts *RegistryCertOptions) error { + if cert.SkipCertVerify != "" { + skipVerifyCerts, _ := strconv.ParseBool(cert.SkipCertVerify) + registryCertOpts.SkipCertVerify = skipVerifyCerts + } + if cert.Insecure != "" { + insecure, _ := strconv.ParseBool(cert.Insecure) + registryCertOpts.Insecure = insecure + } + + if cert.CACertData != "" { + caCertBytes, err := base64.StdEncoding.DecodeString(cert.CACertData) + if err != nil { + return errors.Wrap(err, "unable to decode the base64-encoded custom registry CA certificate string") + } + if len(caCertBytes) != 0 { + filePath, err := configpaths.GetRegistryCertFile() + if err != nil { + return err + } + err = os.WriteFile(filePath, caCertBytes, 0o644) + if err != nil { + return errors.Wrapf(err, "failed to write the custom image registry CA cert to file '%s'", filePath) + } + registryCertOpts.CACertPaths = append(registryCertOpts.CACertPaths, filePath) + } + } + return nil +} + +// AddRegistryTrustedRootCertsFileForWindows adds CA certificate to registry options for Windows environments +func AddRegistryTrustedRootCertsFileForWindows(registryCertOpts *RegistryCertOptions) error { filePath, err := configpaths.GetRegistryTrustedCACertFileForWindows() if err != nil { return err @@ -60,6 +97,16 @@ func AddRegistryTrustedRootCertsFileForWindows(registryOpts *ctlimg.Opts) error if err != nil { return errors.Wrapf(err, "failed to write the registry trusted CA cert to file '%s'", filePath) } - registryOpts.CACertPaths = append(registryOpts.CACertPaths, filePath) + registryCertOpts.CACertPaths = append(registryCertOpts.CACertPaths, filePath) return nil } + +// GetRegistryName extracts the registry name from the image name with/without image tag +// (e.g. localhost:9876/tanzu-cli/plugins/central:small => localhost:9876) +func GetRegistryName(imageName string) (string, error) { + tag, err := regname.NewTag(imageName) + if err != nil { + return "", errors.Wrapf(err, "unable to fetch registry name from image %q", imageName) + } + return tag.Registry.Name(), nil +} diff --git a/pkg/clientconfighelpers/helpers_test.go b/pkg/clientconfighelpers/helpers_test.go new file mode 100644 index 000000000..c60b05ef8 --- /dev/null +++ b/pkg/clientconfighelpers/helpers_test.go @@ -0,0 +1,117 @@ +// Copyright 2023 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package clientconfighelpers + +import ( + "encoding/base64" + "os" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/vmware-tanzu/tanzu-cli/pkg/configpaths" + configlib "github.com/vmware-tanzu/tanzu-plugin-runtime/config" + configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" +) + +func TestClientConfigHelperSuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "pkg/clientconfighelpers suite") +} + +var _ = Describe("config cert command tests", func() { + + Describe("config cert add/list command tests", func() { + var ( + tanzuConfigFile *os.File + tanzuConfigFileNG *os.File + caCertDataOpt string + skipCertVerifyOpt string + insecureOpt string + err error + ) + const ( + fakeCACertData = "fake ca cert data" + testHostName = "test.vmware.com" + trueStr = "true" + ) + + BeforeEach(func() { + tanzuConfigFile, err = os.CreateTemp("", "config") + Expect(err).To(BeNil()) + os.Setenv("TANZU_CONFIG", tanzuConfigFile.Name()) + + tanzuConfigFileNG, err = os.CreateTemp("", "config_ng") + Expect(err).To(BeNil()) + os.Setenv("TANZU_CONFIG_NEXT_GEN", tanzuConfigFileNG.Name()) + + }) + AfterEach(func() { + os.Unsetenv("TANZU_CONFIG") + os.Unsetenv("TANZU_CONFIG_NEXT_GEN") + os.RemoveAll(tanzuConfigFile.Name()) + os.RemoveAll(tanzuConfigFileNG.Name()) + }) + JustBeforeEach(func() { + caCertDataOptB64 := "" + if len(caCertDataOpt) > 0 { + caCertDataOptB64 = base64.StdEncoding.EncodeToString([]byte(caCertDataOpt)) + } + cert := &configtypes.Cert{ + HostName: testHostName, + CACertData: caCertDataOptB64, + SkipCertVerify: skipCertVerifyOpt, + Insecure: insecureOpt, + } + err := configlib.SetCert(cert) + Expect(err).To(BeNil()) + }) + + Context("When only custom CA cert data is provided for the registry hostname in the config", func() { + BeforeEach(func() { + caCertDataOpt = fakeCACertData + }) + It("should return success and cert options should have registry cert path updated", func() { + certOptions, err := GetRegistryCertOptions(testHostName) + Expect(err).To(BeNil()) + Expect(certOptions).ToNot(BeNil()) + + regFilePath, err := configpaths.GetRegistryCertFile() + Expect(err).To(BeNil()) + Expect(certOptions.CACertPaths).To(ContainElement(regFilePath)) + Expect(certOptions.SkipCertVerify).To(Equal(false)) + Expect(certOptions.Insecure).To(Equal(false)) + }) + }) + + Context("When the custom CA cert data, skipCertVerify and Insecure options are provided for the registry hostname in the config", func() { + BeforeEach(func() { + caCertDataOpt = fakeCACertData + skipCertVerifyOpt = trueStr + insecureOpt = trueStr + }) + It("should return success and cert options should have registry cert path updated and skipCertVerify and Insecure options are updated", func() { + certOptions, err := GetRegistryCertOptions(testHostName) + Expect(err).To(BeNil()) + Expect(certOptions).ToNot(BeNil()) + + regFilePath, err := configpaths.GetRegistryCertFile() + Expect(err).To(BeNil()) + Expect(certOptions.CACertPaths).To(ContainElement(regFilePath)) + Expect(certOptions.SkipCertVerify).To(Equal(true)) + Expect(certOptions.Insecure).To(Equal(true)) + }) + It("should return defaults if the registry name doesn't match with the hostname existing in the config", func() { + certOptions, err := GetRegistryCertOptions("NonExistingRegistryName") + Expect(err).To(BeNil()) + Expect(certOptions).ToNot(BeNil()) + // check the cert options returned are default values + Expect(certOptions.CACertPaths).To(BeEmpty()) + Expect(certOptions.SkipCertVerify).To(Equal(false)) + Expect(certOptions.Insecure).To(Equal(false)) + + }) + }) + }) +}) diff --git a/pkg/constants/config_variables.go b/pkg/constants/config_variables.go index ff71a958c..0e2c6ccab 100644 --- a/pkg/constants/config_variables.go +++ b/pkg/constants/config_variables.go @@ -6,10 +6,7 @@ package constants // Configuration variable name constants const ( - TKGProxyCACert = "TKG_PROXY_CA_CERT" ConfigVariableCustomImageRepository = "TKG_CUSTOM_IMAGE_REPOSITORY" - ConfigVariableCustomImageRepositoryCaCertificate = "TKG_CUSTOM_IMAGE_REPOSITORY_CA_CERTIFICATE" - ConfigVariableCustomImageRepositorySkipTLSVerify = "TKG_CUSTOM_IMAGE_REPOSITORY_SKIP_TLS_VERIFY" ConfigVariableDefaultStandaloneDiscoveryImagePath = "TKG_DEFAULT_STANDALONE_DISCOVERY_IMAGE_PATH" ConfigVariableDefaultStandaloneDiscoveryImageTag = "TKG_DEFAULT_STANDALONE_DISCOVERY_IMAGE_TAG" ConfigVariableDefaultStandaloneDiscoveryType = "TKG_DEFAULT_STANDALONE_DISCOVERY_TYPE" diff --git a/pkg/cosignhelper/cosignverify.go b/pkg/cosignhelper/cosignverify.go index 4e951fb04..decb6bc35 100644 --- a/pkg/cosignhelper/cosignverify.go +++ b/pkg/cosignhelper/cosignverify.go @@ -7,24 +7,47 @@ package cosignhelper import ( "context" "crypto" + "crypto/tls" + "crypto/x509" "fmt" + "net/http" + "os" "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/pkcs11key" + ociremote "github.com/sigstore/cosign/pkg/oci/remote" sigs "github.com/sigstore/cosign/pkg/signature" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" ) +// RegistryOptions registry options used while interacting with registry +type RegistryOptions struct { + // CACertPaths is the path to CA certs for the registry endpoint. + // This would be required if the registry is self-signed + CACertPaths []string + // SkipCertVerify is to allow insecure connections to registries (e.g., with expired or self-signed TLS certificates) + SkipCertVerify bool + // AllowInsecure is to allow using HTTP instead of HTTPS protocol while connecting to registries + AllowInsecure bool +} + // CosignVerifyOptions implements the "cosign verify" command using cosign library type CosignVerifyOptions struct { + // PublicKeyPath is the path to custom public key to be used to verify the signature + // of the OCI image. If the path is empty, the CLI embedded public key would be used PublicKeyPath string + // RegistryOpts registry options used while interacting with registry + RegistryOpts *RegistryOptions } -func NewCosignVerifier(publicKeyPath string) Cosignhelper { +func NewCosignVerifier(publicKeyPath string, registryOpts *RegistryOptions) Cosignhelper { return &CosignVerifyOptions{ PublicKeyPath: publicKeyPath, + RegistryOpts: registryOpts, } } @@ -32,8 +55,20 @@ func NewCosignVerifier(publicKeyPath string) Cosignhelper { func (vo *CosignVerifyOptions) Verify(ctx context.Context, images []string) error { var pubKey signature.Verifier var err error + pool, err := vo.getCertPool() + if err != nil { + return errors.Wrapf(err, "loading the cert pool") + } - co := &cosign.CheckOpts{} + co := &cosign.CheckOpts{ + RegistryClientOpts: []ociremote.Option{ + ociremote.WithRemoteOptions(remote.WithContext(ctx)), + ociremote.WithRemoteOptions(remote.WithTransport(&http.Transport{ + // #nosec G402 + TLSClientConfig: &tls.Config{RootCAs: pool, InsecureSkipVerify: vo.RegistryOpts.SkipCertVerify}, + })), + }, + } switch { // If PublicKeyPath is provided(custom public key) use it, else use the embedded public key @@ -62,8 +97,13 @@ func (vo *CosignVerifyOptions) Verify(ctx context.Context, images []string) erro } co.SigVerifier = pubKey + var nameOpts []name.Option + if vo.RegistryOpts.AllowInsecure { + nameOpts = append(nameOpts, name.Insecure) + } + for _, img := range images { - ref, err := name.ParseReference(img, []name.Option{}...) + ref, err := name.ParseReference(img, nameOpts...) if err != nil { return fmt.Errorf("parsing reference: %w", err) } @@ -74,3 +114,24 @@ func (vo *CosignVerifyOptions) Verify(ctx context.Context, images []string) erro } return nil } + +func (vo *CosignVerifyOptions) getCertPool() (*x509.CertPool, error) { + var pool *x509.CertPool + + var err error + pool, err = x509.SystemCertPool() + if err != nil { + return nil, err + } + + if len(vo.RegistryOpts.CACertPaths) > 0 { + for _, path := range vo.RegistryOpts.CACertPaths { + if certs, err := os.ReadFile(path); err != nil { + return nil, errors.Wrapf(err, "failed reading CA certificates from '%s' ", path) + } else if ok := pool.AppendCertsFromPEM(certs); !ok { + return nil, errors.Wrapf(err, "failed adding CA certificates from '%s'", path) + } + } + } + return pool, nil +} diff --git a/pkg/discovery/oci_dbbacked.go b/pkg/discovery/oci_dbbacked.go index 4955fb2c7..55d2cfa73 100644 --- a/pkg/discovery/oci_dbbacked.go +++ b/pkg/discovery/oci_dbbacked.go @@ -15,6 +15,7 @@ import ( "github.com/vmware-tanzu/tanzu-cli/pkg/airgapped" "github.com/vmware-tanzu/tanzu-cli/pkg/carvelhelpers" + "github.com/vmware-tanzu/tanzu-cli/pkg/clientconfighelpers" "github.com/vmware-tanzu/tanzu-cli/pkg/common" "github.com/vmware-tanzu/tanzu-cli/pkg/constants" "github.com/vmware-tanzu/tanzu-cli/pkg/cosignhelper" @@ -159,9 +160,11 @@ func (od *DBBackedOCIDiscovery) fetchInventoryImage() error { // The DB has changed and needs to be updated in the cache. log.Infof("Reading plugin inventory for %q, this will take a few seconds.", od.image) - // Get the custom public key path and prepare cosign verifier, if empty, cosign verifier would use embedded public key for verification - customPublicKeyPath := os.Getenv(constants.PublicKeyPathForPluginDiscoveryImageSignature) - cosignVerifier := cosignhelper.NewCosignVerifier(customPublicKeyPath) + cosignVerifier, err := od.getCosignVerifier() + if err != nil { + return errors.Wrapf(err, "failed to initialize the cosign verifier") + } + if sigVerifyErr := od.verifyInventoryImageSignature(cosignVerifier); sigVerifyErr != nil { log.Warningf("Unable to verify the plugins discovery image signature: %v", sigVerifyErr) // TODO(pkalle): Update the message to convey user to check if they could use the latest public key after we get details of the well known location of the public key @@ -172,7 +175,7 @@ func (od *DBBackedOCIDiscovery) fetchInventoryImage() error { // download plugin inventory image to get the 'plugin_inventory.db' // also handle the air-gapped scenario where additional plugin inventory metadata image is present - err := od.downloadInventoryDatabase() + err = od.downloadInventoryDatabase() if err != nil { return err } @@ -313,6 +316,39 @@ func (od *DBBackedOCIDiscovery) verifyInventoryImageSignature(verifier cosignhel return nil } +func (od *DBBackedOCIDiscovery) getCosignVerifier() (cosignhelper.Cosignhelper, error) { + // Get the custom public key path and prepare cosign verifier, if empty, cosign verifier would use embedded public key for verification + customPublicKeyPath := os.Getenv(constants.PublicKeyPathForPluginDiscoveryImageSignature) + + registryOptions, err := getCosignVerifierRegistryOptions(od.image) + if err != nil { + return nil, errors.Wrapf(err, "unable to prepare the registry options for cosign verification") + } + return cosignhelper.NewCosignVerifier(customPublicKeyPath, registryOptions), nil +} + +// getCosignVerifierRegistryOptions prepares the registry options by including the custom certificate configuration if any +func getCosignVerifierRegistryOptions(imageName string) (*cosignhelper.RegistryOptions, error) { + registryOpts := &cosignhelper.RegistryOptions{ + SkipCertVerify: false, + AllowInsecure: false, + } + registryName, err := clientconfighelpers.GetRegistryName(strings.TrimSpace(imageName)) + if err != nil { + return nil, err + } + // get the certificate configuration and update the registry options + regCertOptions, err := clientconfighelpers.GetRegistryCertOptions(registryName) + if err != nil { + return nil, errors.Wrapf(err, "unable to get the registry certificate configuration") + } + registryOpts.CACertPaths = regCertOptions.CACertPaths + registryOpts.SkipCertVerify = regCertOptions.SkipCertVerify + registryOpts.AllowInsecure = regCertOptions.Insecure + + return registryOpts, nil +} + func getPluginDiscoveryImagesSkippedForSignatureVerification() map[string]struct{} { discoveryImages := map[string]struct{}{} discoveryImagesList := strings.Split(os.Getenv(constants.PluginDiscoveryImageSignatureVerificationSkipList), ",") diff --git a/pkg/discovery/oci_dbbacked_test.go b/pkg/discovery/oci_dbbacked_test.go index 23cd0ba13..9c981afd1 100644 --- a/pkg/discovery/oci_dbbacked_test.go +++ b/pkg/discovery/oci_dbbacked_test.go @@ -4,15 +4,19 @@ package discovery import ( + "encoding/base64" "fmt" "os" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/vmware-tanzu/tanzu-cli/pkg/configpaths" "github.com/vmware-tanzu/tanzu-cli/pkg/constants" + "github.com/vmware-tanzu/tanzu-cli/pkg/cosignhelper" "github.com/vmware-tanzu/tanzu-cli/pkg/fakes" "github.com/vmware-tanzu/tanzu-cli/pkg/plugininventory" + configlib "github.com/vmware-tanzu/tanzu-plugin-runtime/config" configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types" ) @@ -279,9 +283,6 @@ var _ = Describe("Unit tests for DB-backed OCI discovery", func() { ok bool ) BeforeEach(func() { - tmpDir, err = os.MkdirTemp(os.TempDir(), "") - Expect(err).To(BeNil(), "unable to create temporary directory") - configFile, err = os.CreateTemp("", "config") Expect(err).To(BeNil()) os.Setenv("TANZU_CONFIG", configFile.Name()) @@ -301,7 +302,6 @@ var _ = Describe("Unit tests for DB-backed OCI discovery", func() { os.Unsetenv("TANZU_CONFIG_NEXT_GEN") os.RemoveAll(configFile.Name()) os.RemoveAll(configFileNG.Name()) - os.RemoveAll(tmpDir) }) Context("Cosign signature verification is success", func() { It("should return success", func() { @@ -330,4 +330,86 @@ var _ = Describe("Unit tests for DB-backed OCI discovery", func() { }) }) }) + + Describe("getCosignVerifier tests", func() { + var ( + cosignVerifier cosignhelper.Cosignhelper + dbDiscovery *DBBackedOCIDiscovery + ok bool + ) + const ( + fakeCACertData = "fake ca cert data" + testHostName = "test.vmware.com" + ) + BeforeEach(func() { + configFile, err = os.CreateTemp("", "config") + Expect(err).To(BeNil()) + os.Setenv("TANZU_CONFIG", configFile.Name()) + + configFileNG, err = os.CreateTemp("", "config_ng") + Expect(err).To(BeNil()) + os.Setenv("TANZU_CONFIG_NEXT_GEN", configFileNG.Name()) + + discovery = NewOCIDiscovery("test-discovery", testHostName+"/tanzu/test-image:latest", nil) + Expect(err).To(BeNil(), "unable to create discovery") + dbDiscovery, ok = discovery.(*DBBackedOCIDiscovery) + Expect(ok).To(BeTrue(), "oci discovery is not of type DBBackedOCIDiscovery") + }) + AfterEach(func() { + os.Unsetenv("TANZU_CONFIG") + os.Unsetenv("TANZU_CONFIG_NEXT_GEN") + os.Unsetenv(constants.PublicKeyPathForPluginDiscoveryImageSignature) + os.RemoveAll(configFile.Name()) + os.RemoveAll(configFileNG.Name()) + }) + Context("When no custom cert data is provided for registry endpoint/hostname", func() { + BeforeEach(func() { + cert := &configtypes.Cert{ + HostName: testHostName, + CACertData: base64.StdEncoding.EncodeToString([]byte(fakeCACertData)), + SkipCertVerify: "true", + Insecure: "true", + } + err := configlib.SetCert(cert) + Expect(err).To(BeNil()) + }) + It("should create cosign verifier successfully with registryOptions updated with configured custom cert data", func() { + cosignVerifier, err = dbDiscovery.getCosignVerifier() + Expect(err).ToNot(HaveOccurred()) + cvo, ok := cosignVerifier.(*cosignhelper.CosignVerifyOptions) + Expect(ok).To(BeTrue()) + + regFilePath, err := configpaths.GetRegistryCertFile() + Expect(err).To(BeNil()) + Expect(cvo.RegistryOpts.CACertPaths).To(ContainElement(regFilePath)) + Expect(cvo.RegistryOpts.SkipCertVerify).To(BeTrue()) + Expect(cvo.RegistryOpts.AllowInsecure).To(BeTrue()) + }) + It("should create cosign verifier successfully with Image signature custom public key path if provided using the environment variable", func() { + keyPath := "fake/path/to/publickey" + os.Setenv(constants.PublicKeyPathForPluginDiscoveryImageSignature, keyPath) + cosignVerifier, err = dbDiscovery.getCosignVerifier() + Expect(err).ToNot(HaveOccurred()) + cvo, ok := cosignVerifier.(*cosignhelper.CosignVerifyOptions) + Expect(ok).To(BeTrue()) + + Expect(cvo.PublicKeyPath).To(Equal(keyPath)) + + }) + }) + Context("When custom cert data is not provided for registry endpoint/hostname in the config file", func() { + It("cosign verifier should be created successfully with default registryOptions", func() { + cosignVerifier, err = dbDiscovery.getCosignVerifier() + Expect(err).ToNot(HaveOccurred()) + cvo, ok := cosignVerifier.(*cosignhelper.CosignVerifyOptions) + Expect(ok).To(BeTrue()) + + regFilePath, err := configpaths.GetRegistryCertFile() + Expect(err).To(BeNil()) + Expect(cvo.RegistryOpts.CACertPaths).ToNot(ContainElement(regFilePath)) + Expect(cvo.RegistryOpts.SkipCertVerify).To(BeFalse()) + Expect(cvo.RegistryOpts.AllowInsecure).To(BeFalse()) + }) + }) + }) })