diff --git a/cmd/helm/chart_pull.go b/cmd/helm/chart_pull.go index 760ff3e2c..9b411be30 100644 --- a/cmd/helm/chart_pull.go +++ b/cmd/helm/chart_pull.go @@ -17,7 +17,17 @@ limitations under the License. package main import ( + "encoding/hex" + "fmt" + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/trustpinning" + "github.com/theupdateframework/notary/tuf/data" + "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/pkg/helmpath" "io" + "os" + "path/filepath" + "strings" "github.com/spf13/cobra" @@ -32,7 +42,8 @@ This will store the chart in the local registry cache to be used later. ` func newChartPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - return &cobra.Command{ + signOpts := &signatureOptions{} + cmd := &cobra.Command{ Use: "pull [ref]", Short: "pull a chart from remote", Long: chartPullDesc, @@ -40,7 +51,107 @@ func newChartPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Hidden: !FeatureGateOCI.IsEnabled(), RunE: func(cmd *cobra.Command, args []string) error { ref := args[0] - return action.NewChartPull(cfg).Run(out, ref) + err := action.NewChartPull(cfg).Run(out, ref) + if err != nil { + return err + } + + if signOpts.Sign { + sha, err := GetSHA(signOpts.trustDir, signOpts.trustServer, ref, signOpts.caCert, signOpts.rootKey) + if err != nil { + return err + } + + r, err := registry.ParseReference(ref) + if err != nil { + return err + } + + c, err := registry.NewCache( + registry.CacheOptWriter(out), + registry.CacheOptRoot(filepath.Join(helmpath.CachePath(), "registry", registry.CacheRootDir))) + + cs, err := c.FetchReference(r) + if err != nil { + return err + } + + if cs.Digest.Hex() != sha { + fmt.Fprintf(out, "digests do not match: %v and %v", cs.Digest.Hex(), sha) + _, err = c.DeleteReference(r) + return err + } + } + return nil }, } + td := filepath.Join(helmpath.ConfigPath(), ".trust") + cmd.Flags().StringVarP(&signOpts.trustServer, "trust-server", "", "", "The trust server to use for signature verification") + cmd.Flags().StringVarP(&signOpts.trustDir, "trust-dir", "", td, "Location where trust data is stored") + cmd.Flags().StringVarP(&signOpts.rootKey, "root-key", "", "", "Root Key to initialize repository with") + cmd.Flags().StringVarP(&signOpts.caCert, "ca-cert", "", "", "Trust certs signed only by this CA will be considered") + cmd.Flags().BoolVarP(&signOpts.Sign, "sign", "", true, "Enable signature checking") + + return cmd +} + +func GetSHA(trustDir, trustServer, ref, tlscacert, rootKey string) (string, error) { + r, tag := GetRepoAndTag(ref) + target, err := GetTargetWithRole(r, tag, trustServer, tlscacert, trustDir) + if err != nil { + return "", err + } + + return hex.EncodeToString(target.Hashes["sha256"]), nil } + +func GetRepoAndTag(ref string) (string, string) { + parts := strings.Split(ref, "/") + return strings.Split(parts[1], ":")[0], strings.Split(parts[1], ":")[1] +} + +func GetTargetWithRole(gun, name, trustServer, tlscacert, trustDir string) (*client.TargetWithRole, error) { + targets, err := GetTargets(gun, trustServer, tlscacert, trustDir) + if err != nil { + return nil, fmt.Errorf("cannot list targets:%v", err) + } + + for _, target := range targets { + if target.Name == name { + return target, nil + } + } + + return nil, fmt.Errorf("cannot find target %v in trusted collection %v", name, gun) +} + +// GetTargets returns all targets for a given gun from the trusted collection +func GetTargets(gun, trustServer, tlscacert, trustDir string) ([]*client.TargetWithRole, error) { + if err := ensureTrustDir(trustDir); err != nil { + return nil, fmt.Errorf("cannot ensure trust directory: %v", err) + } + + transport, err := action.MakeTransport(trustServer, gun, tlscacert) + if err != nil { + return nil, fmt.Errorf("cannot make transport: %v", err) + } + + repo, err := client.NewFileCachedRepository( + trustDir, + data.GUN(gun), + trustServer, + transport, + nil, + trustpinning.TrustPinConfig{}, + ) + if err != nil { + return nil, fmt.Errorf("cannot create new file cached repository: %v", err) + } + + return repo.ListTargets() +} + +// ensureTrustDir ensures the trust directory exists +func ensureTrustDir(trustDir string) error { + return os.MkdirAll(trustDir, 0700) +} \ No newline at end of file diff --git a/cmd/helm/chart_push.go b/cmd/helm/chart_push.go index ff34632b1..86e62fa4d 100644 --- a/cmd/helm/chart_push.go +++ b/cmd/helm/chart_push.go @@ -17,7 +17,9 @@ limitations under the License. package main import ( + "helm.sh/helm/v3/pkg/helmpath" "io" + "path/filepath" "github.com/spf13/cobra" @@ -33,8 +35,18 @@ Note: the ref must already exist in the local registry cache. Must first run "helm chart save" or "helm chart pull". ` +// Used if --check-signature flag is used +type signatureOptions struct { + Sign bool + trustServer string + trustDir string + caCert string + rootKey string +} + func newChartPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - return &cobra.Command{ + signOpts := &signatureOptions{} + cmd := &cobra.Command{ Use: "push [ref]", Short: "push a chart to remote", Long: chartPushDesc, @@ -42,7 +54,28 @@ func newChartPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Hidden: !FeatureGateOCI.IsEnabled(), RunE: func(cmd *cobra.Command, args []string) error { ref := args[0] + + if signOpts.Sign { + err := action.NewChartSign( + cfg, + signOpts.trustServer, + signOpts.trustDir, + signOpts.caCert, + signOpts.rootKey).Run(out, ref) + if err != nil { + return err + } + } + return action.NewChartPush(cfg).Run(out, ref) }, } + td := filepath.Join(helmpath.ConfigPath(), ".trust") + cmd.Flags().StringVarP(&signOpts.trustServer, "trust-server", "", "", "The trust server to use for signature verification") + cmd.Flags().StringVarP(&signOpts.trustDir, "trust-dir", "", td, "Location where trust data is stored") + cmd.Flags().StringVarP(&signOpts.rootKey, "root-key", "", "", "Root Key to initialize repository with") + cmd.Flags().StringVarP(&signOpts.caCert, "ca-cert", "", "", "Trust certs signed only by this CA will be considered") + cmd.Flags().BoolVarP(&signOpts.Sign, "sign", "", true, "Enable signature checking") + + return cmd } diff --git a/go.mod b/go.mod index 3413112cd..a77aa58c7 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,10 @@ require ( github.com/containerd/containerd v1.3.2 github.com/cyphar/filepath-securejoin v0.2.2 github.com/deislabs/oras v0.8.1 + github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492 github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce + github.com/docker/go v1.5.1-1 // indirect github.com/docker/go-units v0.4.0 github.com/evanphx/json-patch v4.5.0+incompatible github.com/gobwas/glob v0.2.3 @@ -27,6 +29,7 @@ require ( github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.4.0 + github.com/theupdateframework/notary v0.6.1 github.com/xeipuuv/gojsonschema v1.1.0 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 k8s.io/api v0.18.0 @@ -37,6 +40,7 @@ require ( k8s.io/klog v1.0.0 k8s.io/kubectl v0.18.0 sigs.k8s.io/yaml v1.2.0 + github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 ) replace ( diff --git a/go.sum b/go.sum index 3c08dba4b..8b44a3682 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,9 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= +github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/agl/ed25519 v0.0.0-20200225211852-fd4d107ace12 h1:iPf1jQ8yKTms6k6L5vYSE7RZJpjEe5vLTOmzRZdpnKc= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -129,6 +132,8 @@ github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce h1:KXS1Jg+ddGcWA8e github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/go v1.5.1-1 h1:hr4w35acWBPhGBXlzPoHpmZ/ygPjnmFVxGxxGnMyP7k= +github.com/docker/go v1.5.1-1/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916 h1:yWHOI+vFjEsAakUTSrtqc/SAHrhSkmn48pqjidZX3QA= @@ -459,6 +464,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0= +github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= diff --git a/pkg/action/chart_sign.go b/pkg/action/chart_sign.go new file mode 100644 index 000000000..48cc2cbbb --- /dev/null +++ b/pkg/action/chart_sign.go @@ -0,0 +1,302 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/config/types" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/auth/challenge" + "github.com/docker/distribution/registry/client/transport" + "github.com/theupdateframework/notary" + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/cryptoservice" + "github.com/theupdateframework/notary/passphrase" + "github.com/theupdateframework/notary/trustmanager" + "github.com/theupdateframework/notary/trustpinning" + "github.com/theupdateframework/notary/tuf/data" + "github.com/theupdateframework/notary/tuf/utils" + "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/pkg/helmpath" +) + +// ChartPush performs a chart sign operation +type ChartSign struct { + cfg *Configuration + trustDir string + trustServer string + ref string + caCert string + rootKey string +} + +// NewChartPush creates a new ChartPush object with the given configuration. +func NewChartSign(cfg *Configuration, trustServer, ref, caCert, rootKey string) *ChartSign { + return &ChartSign{ + cfg: cfg, + trustServer: trustServer, + ref: ref, + caCert: caCert, + rootKey: rootKey, + } +} + +// Run executes the chart push operation +func (a *ChartSign) Run(out io.Writer, ref string) error { + + // Init Registry Cache + cacheDir := filepath.Join(helmpath.CachePath(), "registry", registry.CacheRootDir) + cache, err := registry.NewCache(registry.CacheOptWriter(out), registry.CacheOptRoot(cacheDir)) + r, err := registry.ParseReference(ref) + if err != nil { + return err + } + + cacheSummary, err := cache.FetchReference(r) + if err != nil { + return err + } + + cachedChart := filepath.Join(cacheDir, "blobs", "sha256", strings.Split(cacheSummary.Digest.String(), ":")[1]) + + /// Export to action and tuf experimental + + transport, err := MakeTransport(a.trustServer, r.Repo, a.caCert) + if err != nil { + return fmt.Errorf("cannot make transport: %v", err) + } + + passphraseRetriever := passphrase.PromptRetriever() + + repo, err := client.NewFileCachedRepository( + a.trustDir, + data.GUN(r.Repo), + a.trustServer, + transport, + passphraseRetriever, + trustpinning.TrustPinConfig{}, + ) + + if err != nil { + return fmt.Errorf("cannot create new file cached repository: %v", err) + } + + err = clearChangeList(repo) + if err != nil { + return fmt.Errorf("cannot clear change list: %v", err) + } + + if _, err = repo.ListTargets(); err != nil { + switch err.(type) { + case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: + rootKeyIDs, err := importRootKey(a.rootKey, repo, passphraseRetriever) + if err != nil { + return err + } + + if err = repo.Initialize(rootKeyIDs); err != nil { + return fmt.Errorf("cannot initialize repo: %v", err) + } + + default: + return fmt.Errorf("cannot list targets: %v", err) + } + } + + target, err := client.NewTarget(r.Tag, cachedChart, nil) + if err != nil { + return err + } + + // TODO - Radu M + // decide whether to allow actually passing roles as flags + + // If roles is empty, we default to adding to targets + if err = repo.AddTarget(target, data.NewRoleList([]string{})...); err != nil { + return err + } + + err = repo.Publish() + + defer clearChangeList(repo) + + return err +} + +func MakeTransport(server, gun, tlsCaCert string) (http.RoundTripper, error) { + modifiers := []transport.RequestModifier{ + transport.NewHeaderRequestModifier(http.Header{ + "User-Agent": []string{"signy"}, + }), + } + + base := http.DefaultTransport + if tlsCaCert != "" { + caCert, err := ioutil.ReadFile(tlsCaCert) + if err != nil { + return nil, fmt.Errorf("cannot read cert file: %v", err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + base = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: caCertPool, + }, + } + } + + authTransport := transport.NewTransport(base, modifiers...) + pingClient := &http.Client{ + Transport: authTransport, + Timeout: 5 * time.Second, + } + req, err := http.NewRequest("GET", server+"/v2/", nil) + if err != nil { + return nil, fmt.Errorf("cannot create HTTP request: %v", err) + } + + challengeManager := challenge.NewSimpleManager() + resp, err := pingClient.Do(req) + if err != nil { + return nil, fmt.Errorf("cannot get response from ping client: %v", err) + } + defer resp.Body.Close() + if err := challengeManager.AddResponse(resp); err != nil { + return nil, fmt.Errorf("cannot add response to challenge manager: %v", err) + } + + defaultAuth, err := getDefaultAuth() + if err != nil { + return nil, fmt.Errorf("cannot get default credentials: %v", err) + } + + creds := simpleCredentialStore{auth: defaultAuth} + tokenHandler := auth.NewTokenHandler(base, creds, gun, "push", "pull") + modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler)) + + return transport.NewTransport(base, modifiers...), nil +} + +// clearChangelist clears the notary staging changelist +func clearChangeList(notaryRepo client.Repository) error { + cl, err := notaryRepo.GetChangelist() + if err != nil { + return err + } + return cl.Clear("") +} + +func getDefaultAuth() (types.AuthConfig, error) { + cfg, err := config.Load(defaultCfgDir()) + if err != nil { + return types.AuthConfig{}, err + } + + return cfg.AuthConfigs["https://index.docker.io/v1/"], nil +} + +type simpleCredentialStore struct { + auth types.AuthConfig +} + +func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { + return scs.auth.Username, scs.auth.Password +} + +func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { + return scs.auth.IdentityToken +} + +func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { +} + +func defaultCfgDir() string { + homeEnvPath := os.Getenv("HOME") + if homeEnvPath == "" && runtime.GOOS == "windows" { + homeEnvPath = os.Getenv("USERPROFILE") + } + + return filepath.Join(homeEnvPath, ".docker") +} + +func importRootKey(rootKey string, nRepo client.Repository, retriever notary.PassRetriever) ([]string, error) { + var rootKeyList []string + + if rootKey != "" { + privKey, err := readKey(data.CanonicalRootRole, rootKey, retriever) + if err != nil { + return nil, err + } + // add root key to repo + err = nRepo.GetCryptoService().AddKey(data.CanonicalRootRole, "", privKey) + if err != nil { + return nil, fmt.Errorf("Error importing key: %v", err) + } + rootKeyList = []string{privKey.ID()} + } else { + rootKeyList = nRepo.GetCryptoService().ListKeys(data.CanonicalRootRole) + } + + if len(rootKeyList) > 0 { + // Chooses the first root key available, which is initialization specific + // but should return the HW one first. + rootKeyID := rootKeyList[0] + fmt.Printf("Root key found, using: %s\n", rootKeyID) + + return []string{rootKeyID}, nil + } + + return []string{}, nil +} + +func readKey(role data.RoleName, keyFilename string, retriever notary.PassRetriever) (data.PrivateKey, error) { + pemBytes, err := ioutil.ReadFile(keyFilename) + if err != nil { + return nil, fmt.Errorf("Error reading input root key file: %v", err) + } + isEncrypted := true + if err = cryptoservice.CheckRootKeyIsEncrypted(pemBytes); err != nil { + if role == data.CanonicalRootRole { + return nil, err + } + isEncrypted = false + } + var privKey data.PrivateKey + if isEncrypted { + privKey, _, err = trustmanager.GetPasswdDecryptBytes(retriever, pemBytes, "", data.CanonicalRootRole.String()) + } else { + privKey, err = utils.ParsePEMPrivateKey(pemBytes, "") + } + if err != nil { + return nil, err + } + + return privKey, nil +}