feat(helm): add --verify flag to commands

This adds the --verify and --keyring flags to:

helm fetch
helm inspect
helm install
helm upgrade

Each of these commands can now make cryptographic verification a
prerequisite for using a chart.
pull/988/head
Matt Butcher 8 years ago
parent d80df93414
commit f1d07c3f11

@ -17,8 +17,11 @@ limitations under the License.
package main
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
@ -27,65 +30,179 @@ import (
"github.com/spf13/cobra"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/repo"
)
var untarFile bool
var untarDir string
const fetchDesc = `
Retrieve a package from a package repository, and download it locally.
func init() {
RootCommand.AddCommand(fetchCmd)
fetchCmd.Flags().BoolVar(&untarFile, "untar", false, "If set to true, will untar the chart after downloading it.")
fetchCmd.Flags().StringVar(&untarDir, "untardir", ".", "If untar is specified, this flag specifies where to untar the chart.")
}
This is useful for fetching packages to inspect, modify, or repackage. It can
also be used to perform cryptographic verification of a chart without installing
the chart.
There are options for unpacking the chart after download. This will create a
directory for the chart and uncomparess into that directory.
If the --verify flag is specified, the requested chart MUST have a provenance
file, and MUST pass the verification process. Failure in any part of this will
result in an error, and the chart will not be saved locally.
`
var fetchCmd = &cobra.Command{
Use: "fetch [chart URL | repo/chartname]",
Short: "download a chart from a repository and (optionally) unpack it in local directory",
Long: "",
RunE: fetch,
type fetchCmd struct {
untar bool
untardir string
chartRef string
verify bool
keyring string
out io.Writer
}
func fetch(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("This command needs at least one argument, url or repo/name of the chart.")
func newFetchCmd(out io.Writer) *cobra.Command {
fch := &fetchCmd{out: out}
cmd := &cobra.Command{
Use: "fetch [chart URL | repo/chartname]",
Short: "download a chart from a repository and (optionally) unpack it in local directory",
Long: fetchDesc,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("This command needs at least one argument, url or repo/name of the chart.")
}
fch.chartRef = args[0]
return fch.run()
},
}
pname := args[0]
f := cmd.Flags()
f.BoolVar(&fch.untar, "untar", false, "If set to true, will untar the chart after downloading it.")
f.StringVar(&fch.untardir, "untardir", ".", "If untar is specified, this flag specifies where to untar the chart.")
f.BoolVar(&fch.verify, "verify", false, "Verify the package against its signature.")
f.StringVar(&fch.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.")
return cmd
}
func (f *fetchCmd) run() error {
pname := f.chartRef
if filepath.Ext(pname) != ".tgz" {
pname += ".tgz"
}
return fetchChart(pname)
return downloadChart(pname, f.untar, f.untardir, f.verify, f.keyring)
}
func fetchChart(pname string) error {
f, err := repo.LoadRepositoriesFile(repositoriesFile())
// downloadChart fetches a chart over HTTP, and then (if verify is true) verifies it.
//
// If untar is true, it also unpacks the file into untardir.
func downloadChart(pname string, untar bool, untardir string, verify bool, keyring string) error {
r, err := repo.LoadRepositoriesFile(repositoriesFile())
if err != nil {
return err
}
// get download url
u, err := mapRepoArg(pname, f.Repositories)
u, err := mapRepoArg(pname, r.Repositories)
if err != nil {
return err
}
resp, err := http.Get(u.String())
href := u.String()
buf, err := fetchChart(href)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return fmt.Errorf("Failed to fetch %s : %s", u.String(), resp.Status)
if verify {
basename := filepath.Base(pname)
sigref := href + ".prov"
sig, err := fetchChart(sigref)
if err != nil {
return fmt.Errorf("provenance data not downloaded from %s: %s", sigref, err)
}
if err := ioutil.WriteFile(basename+".prov", sig.Bytes(), 0755); err != nil {
return fmt.Errorf("provenance data not saved: %s", err)
}
if err := verifyChart(basename, keyring); err != nil {
return err
}
}
defer resp.Body.Close()
if untarFile {
return chartutil.Expand(untarDir, resp.Body)
return saveChart(pname, buf, untar, untardir)
}
// verifyChart takes a path to a chart archive and a keyring, and verifies the chart.
//
// It assumes that a chart archive file is accompanied by a provenance file whose
// name is the archive file name plus the ".prov" extension.
func verifyChart(path string, keyring string) error {
// For now, error out if it's not a tar file.
if fi, err := os.Stat(path); err != nil {
return err
} else if fi.IsDir() {
return errors.New("unpacked charts cannot be verified")
} else if !isTar(path) {
return errors.New("chart must be a tgz file")
}
p := strings.Split(u.String(), "/")
return saveChartFile(p[len(p)-1], resp.Body)
provfile := path + ".prov"
if _, err := os.Stat(provfile); err != nil {
return fmt.Errorf("could not load provenance file %s: %s", provfile, err)
}
sig, err := provenance.NewFromKeyring(keyring, "")
if err != nil {
return fmt.Errorf("failed to load keyring: %s", err)
}
ver, err := sig.Verify(path, provfile)
if flagDebug {
for name := range ver.SignedBy.Identities {
fmt.Printf("Signed by %q\n", name)
}
}
return err
}
// defaultKeyring returns the expanded path to the default keyring.
func defaultKeyring() string {
return os.ExpandEnv("$HOME/.gnupg/pubring.gpg")
}
// isTar tests whether the given file is a tar file.
//
// Currently, this simply checks extension, since a subsequent function will
// untar the file and validate its binary format.
func isTar(filename string) bool {
return strings.ToLower(filepath.Ext(filename)) == ".tgz"
}
// saveChart saves a chart locally.
func saveChart(name string, buf *bytes.Buffer, untar bool, untardir string) error {
if untar {
return chartutil.Expand(untardir, buf)
}
p := strings.Split(name, "/")
return saveChartFile(p[len(p)-1], buf)
}
// fetchChart retrieves a chart over HTTP.
func fetchChart(href string) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil)
resp, err := http.Get(href)
if err != nil {
return buf, err
}
if resp.StatusCode != 200 {
return buf, fmt.Errorf("Failed to fetch %s : %s", href, resp.Status)
}
_, err = io.Copy(buf, resp.Body)
resp.Body.Close()
return buf, err
}
// mapRepoArg figures out which format the argument is given, and creates a fetchable

@ -95,6 +95,8 @@ func newRootCmd(out io.Writer) *cobra.Command {
newUpgradeCmd(nil, out),
newRollbackCmd(nil, out),
newPackageCmd(nil, out),
newFetchCmd(out),
newVerifyCmd(out),
)
return cmd
}

@ -46,6 +46,8 @@ of the Charts.yaml file
type inspectCmd struct {
chartpath string
output string
verify bool
keyring string
out io.Writer
client helm.Interface
}
@ -71,7 +73,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command {
if err := checkArgsLength(1, len(args), "chart name"); err != nil {
return err
}
cp, err := locateChartPath(args[0])
cp, err := locateChartPath(args[0], insp.verify, insp.keyring)
if err != nil {
return err
}
@ -86,7 +88,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command {
Long: inspectValuesDesc,
RunE: func(cmd *cobra.Command, args []string) error {
insp.output = valuesOnly
cp, err := locateChartPath(args[0])
cp, err := locateChartPath(args[0], insp.verify, insp.keyring)
if err != nil {
return err
}
@ -101,7 +103,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command {
Long: inspectChartDesc,
RunE: func(cmd *cobra.Command, args []string) error {
insp.output = chartOnly
cp, err := locateChartPath(args[0])
cp, err := locateChartPath(args[0], insp.verify, insp.keyring)
if err != nil {
return err
}
@ -110,6 +112,19 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command {
},
}
vflag := "verify"
vdesc := "verify the provenance data for this chart"
inspectCommand.Flags().BoolVar(&insp.verify, vflag, false, vdesc)
valuesSubCmd.Flags().BoolVar(&insp.verify, vflag, false, vdesc)
chartSubCmd.Flags().BoolVar(&insp.verify, vflag, false, vdesc)
kflag := "keyring"
kdesc := "the path to the keyring containing public verification keys"
kdefault := defaultKeyring()
inspectCommand.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc)
valuesSubCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc)
chartSubCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc)
inspectCommand.AddCommand(valuesSubCmd)
inspectCommand.AddCommand(chartSubCmd)

@ -18,6 +18,7 @@ package main
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
@ -54,6 +55,9 @@ or
To check the generated manifests of a release without installing the chart,
the '--debug' and '--dry-run' flags can be combined. This will still require a
round-trip to the Tiller server.
If --verify is set, the chart MUST have a provenance file, and the provenenace
fall MUST pass all verification steps.
`
type installCmd struct {
@ -64,6 +68,8 @@ type installCmd struct {
dryRun bool
disableHooks bool
replace bool
verify bool
keyring string
out io.Writer
client helm.Interface
values *values
@ -86,7 +92,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
if err := checkArgsLength(1, len(args), "chart name"); err != nil {
return err
}
cp, err := locateChartPath(args[0])
cp, err := locateChartPath(args[0], inst.verify, inst.keyring)
if err != nil {
return err
}
@ -106,6 +112,8 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
f.BoolVar(&inst.replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production")
f.Var(inst.values, "set", "set values on the command line. Separate values with commas: key1=val1,key2=val2")
f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release")
f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it")
f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification")
return cmd
}
@ -171,6 +179,7 @@ func (i *installCmd) vals() ([]byte, error) {
return buffer.Bytes(), nil
}
// printRelease prints info about a release if the flagDebug is true.
func (i *installCmd) printRelease(rel *release.Release) {
if rel == nil {
return
@ -251,9 +260,23 @@ func splitPair(item string) (name string, value interface{}) {
// - current working directory
// - if path is absolute or begins with '.', error out here
// - chart repos in $HELM_HOME
func locateChartPath(name string) (string, error) {
if _, err := os.Stat(name); err == nil {
return filepath.Abs(name)
//
// If 'verify' is true, this will attempt to also verify the chart.
func locateChartPath(name string, verify bool, keyring string) (string, error) {
if fi, err := os.Stat(name); err == nil {
abs, err := filepath.Abs(name)
if err != nil {
return abs, err
}
if verify {
if fi.IsDir() {
return "", errors.New("cannot verify a directory")
}
if err := verifyChart(abs, keyring); err != nil {
return "", err
}
}
return abs, nil
}
if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
return name, fmt.Errorf("path %q not found", name)
@ -269,7 +292,7 @@ func locateChartPath(name string) (string, error) {
if filepath.Ext(name) != ".tgz" {
name += ".tgz"
}
if err := fetchChart(name); err == nil {
if err := downloadChart(name, false, ".", verify, keyring); err == nil {
lname, err := filepath.Abs(filepath.Base(name))
if err != nil {
return lname, err

@ -74,6 +74,24 @@ func TestInstall(t *testing.T) {
expected: "FOOBAR",
resp: releaseMock(&releaseOptions{name: "FOOBAR"}),
},
// Install, perform chart verification along the way.
{
name: "install with verification, missing provenance",
args: []string{"testdata/testcharts/compressedchart-0.1.0.tgz"},
flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "),
err: true,
},
{
name: "install with verification, directory instead of file",
args: []string{"testdata/testcharts/signtest"},
flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "),
err: true,
},
{
name: "install with verification, valid",
args: []string{"testdata/testcharts/signtest-0.1.0.tgz"},
flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "),
},
}
runReleaseCases(t, tests, func(c *fakeReleaseClient, out io.Writer) *cobra.Command {

@ -28,7 +28,6 @@ func TestListCmd(t *testing.T) {
tests := []struct {
name string
args []string
flags map[string]string
resp []*release.Release
expected string
err bool
@ -41,8 +40,9 @@ func TestListCmd(t *testing.T) {
expected: "thomas-guide",
},
{
name: "list --long",
flags: map[string]string{"long": "1"},
name: "list --long",
//flags: map[string]string{"long": "1"},
args: []string{"--long"},
resp: []*release.Release{
releaseMock(&releaseOptions{name: "atlas"}),
},
@ -56,7 +56,7 @@ func TestListCmd(t *testing.T) {
rels: tt.resp,
}
cmd := newListCmd(c, &buf)
setFlags(cmd, tt.flags)
cmd.ParseFlags(tt.args)
err := cmd.RunE(cmd, tt.args)
if (err != nil) != tt.err {
t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err)

@ -43,11 +43,6 @@ Chart.yaml file, and (if found) build the current directory into a chart.
Versioned chart archives are used by Helm package repositories.
`
const (
envSigningKey = "HELM_SIGNING_KEY"
envKeyring = "HELM_KEYRING"
)
type packageCmd struct {
save bool
sign bool
@ -86,7 +81,7 @@ func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command {
f.BoolVar(&pkg.save, "save", true, "save packaged chart to local chart repository")
f.BoolVar(&pkg.sign, "sign", false, "use a PGP private key to sign this package")
f.StringVar(&pkg.key, "key", "", "the name of the key to use when signing. Used if --sign is true.")
f.StringVar(&pkg.keyring, "keyring", os.ExpandEnv("$HOME/.gnupg/pubring.gpg"), "the location of a public keyring")
f.StringVar(&pkg.keyring, "keyring", defaultKeyring(), "the location of a public keyring")
return cmd
}

Binary file not shown.

Binary file not shown.

@ -0,0 +1,20 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
description: A Helm chart for Kubernetes
name: signtest
version: 0.1.0
...
files:
signtest-0.1.0.tgz: sha256:dee72947753628425b82814516bdaa37aef49f25e8820dd2a6e15a33a007823b
-----BEGIN PGP SIGNATURE-----
wsBcBAEBCgAQBQJXomNHCRCEO7+YH8GHYgAALywIAG1Me852Fpn1GYu8Q1GCcw4g
l2k7vOFchdDwDhdSVbkh4YyvTaIO3iE2Jtk1rxw+RIJiUr0eLO/rnIJuxZS8WKki
DR1LI9J1VD4dxN3uDETtWDWq7ScoPsRY5mJvYZXC8whrWEt/H2kfqmoA9LloRPWp
flOE0iktA4UciZOblTj6nAk3iDyjh/4HYL4a6tT0LjjKI7OTw4YyHfjHad1ywVCz
9dMUc1rPgTnl+fnRiSPSrlZIWKOt1mcQ4fVrU3nwtRUwTId2k8FtygL0G6M+Y6t0
S6yaU7qfk9uTxkdkUF7Bf1X3ukxfe+cNBC32vf4m8LY4NkcYfSqK2fGtQsnVr6s=
=NyOM
-----END PGP SIGNATURE-----

@ -0,0 +1,5 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
.git

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: signtest
version: 0.1.0

@ -0,0 +1,6 @@
description: Deploy a basic Alpine Linux pod
home: https://k8s.io/helm
name: alpine
sources:
- https://github.com/kubernetes/helm
version: 0.1.0

@ -0,0 +1,9 @@
This example was generated using the command `helm create alpine`.
The `templates/` directory contains a very simple pod resource with a
couple of parameters.
The `values.yaml` file contains the default values for the
`alpine-pod.yaml` template.
You can install this example using `helm install docs/examples/alpine`.

@ -0,0 +1,16 @@
apiVersion: v1
kind: Pod
metadata:
name: {{.Release.Name}}-{{.Chart.Name}}
labels:
heritage: {{.Release.Service}}
chartName: {{.Chart.Name}}
chartVersion: {{.Chart.Version | quote}}
annotations:
"helm.sh/created": "{{.Release.Time.Seconds}}"
spec:
restartPolicy: {{default "Never" .restart_policy}}
containers:
- name: waiter
image: "alpine:3.3"
command: ["/bin/sleep","9000"]

@ -0,0 +1,2 @@
# The pod name
name: my-alpine

@ -0,0 +1,10 @@
apiVersion: v1
kind: Pod
metadata:
name: signtest
spec:
restartPolicy: Never
containers:
- name: waiter
image: "alpine:3.3"
command: ["/bin/sleep","9000"]

@ -46,6 +46,8 @@ type upgradeCmd struct {
disableHooks bool
valuesFile string
values *values
verify bool
keyring string
}
func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
@ -79,6 +81,8 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
f.BoolVar(&upgrade.dryRun, "dry-run", false, "simulate an upgrade")
f.Var(upgrade.values, "set", "set values on the command line. Separate values with commas: key1=val1,key2=val2")
f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks")
f.BoolVar(&upgrade.verify, "verify", false, "verify the provenance of the chart before upgrading")
f.StringVar(&upgrade.keyring, "keyring", defaultKeyring(), "the path to the keyring that contains public singing keys")
return cmd
}
@ -109,7 +113,7 @@ func (u *upgradeCmd) vals() ([]byte, error) {
}
func (u *upgradeCmd) run() error {
chartPath, err := locateChartPath(u.chart)
chartPath, err := locateChartPath(u.chart, u.verify, u.keyring)
if err != nil {
return err
}

@ -0,0 +1,67 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 main
import (
"errors"
"io"
"github.com/spf13/cobra"
)
const verifyDesc = `
Verify that the given chart has a valid provenance file.
Provenance files provide crytographic verification that a chart has not been
tampered with, and was packaged by a trusted provider.
This command can be used to verify a local chart. Several other commands provide
'--verify' flags that run the same validation. To generate a signed package, use
the 'helm package --sign' command.
`
type verifyCmd struct {
keyring string
chartfile string
out io.Writer
}
func newVerifyCmd(out io.Writer) *cobra.Command {
vc := &verifyCmd{out: out}
cmd := &cobra.Command{
Use: "verify [flags] PATH",
Short: "verify that a chart at the given path has been signed and is valid",
Long: verifyDesc,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("a path to a package file is required")
}
vc.chartfile = args[0]
return vc.run()
},
}
f := cmd.Flags()
f.StringVar(&vc.keyring, "keyring", defaultKeyring(), "the keyring containing public keys.")
return cmd
}
func (v *verifyCmd) run() error {
return verifyChart(v.chartfile, v.keyring)
}

@ -0,0 +1,83 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 main
import (
"bytes"
"testing"
)
func TestVerifyCmd(t *testing.T) {
tests := []struct {
name string
args []string
flags []string
expect string
err bool
}{
{
name: "verify requires a chart",
expect: "a path to a package file is required",
err: true,
},
{
name: "verify requires that chart exists",
args: []string{"no/such/file"},
expect: "stat no/such/file: no such file or directory",
err: true,
},
{
name: "verify requires that chart is not a directory",
args: []string{"testdata/testcharts/signtest"},
expect: "unpacked charts cannot be verified",
err: true,
},
{
name: "verify requires that chart has prov file",
args: []string{"testdata/testcharts/compressedchart-0.1.0.tgz"},
expect: "could not load provenance file testdata/testcharts/compressedchart-0.1.0.tgz.prov: stat testdata/testcharts/compressedchart-0.1.0.tgz.prov: no such file or directory",
err: true,
},
{
name: "verify validates a properly signed chart",
args: []string{"testdata/testcharts/signtest-0.1.0.tgz"},
flags: []string{"--keyring", "testdata/helm-test-key.pub"},
expect: "",
err: false,
},
}
for _, tt := range tests {
b := bytes.NewBuffer(nil)
vc := newVerifyCmd(b)
vc.ParseFlags(tt.flags)
err := vc.RunE(vc, tt.args)
if tt.err {
if err == nil {
t.Errorf("Expected error, but got none: %q", b.String())
}
if err.Error() != tt.expect {
t.Errorf("Expected error %q, got %q", tt.expect, err)
}
continue
} else if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if b.String() != tt.expect {
t.Errorf("Expected %q, got %q", tt.expect, b.String())
}
}
}

@ -0,0 +1,173 @@
# Helm Provenance and Integrity
Helm has provenance tools which help chart users verify the integrity and origin
of a package. Using industry-standard tools based on PKI, GnuPG, and well-resepected
package managers, Helm can generate and verify signature files.
**Note:**
Version 2.0.0-alpha.4 introduced a system for verifying the authenticity of charts.
While we do not anticipate that any major changes will be made to the file formats
or provenancing algorithms, this portion of Helm is not considered _frozen_ until
2.0.0-RC1 is released. The original plan for this feature can be found
[at issue 983](https://github.com/kubernetes/helm/issues/983).
## Overview
Integrity is established by comparing a chart to a provenance record. Provenance
records are stored in _provenance files_, which are stored alongside a packaged
chart. For example, if a chart is named `myapp-1.2.3.tgz`, its provenance file
will be `myapp-1.2.3.tgz.prov`.
Provenance files are generated at packaging time (`helm package --sign ...`), and
can be checked by multiple commands, notable `helm install --verify`.
## The Workflow
This section describes a potential workflow for using provenance data effectively.
WHAT YOU WILL NEED:
- A valid PGP keypair in a binary (not ASCII-armored) format
- helm
Creating a new chart is the same as before:
```
$ helm create mychart
Creating mychart
```
Once ready to package, add the `--verify` flag to `helm package`. Also, specify
the signing key and the keyring:
```
$ helm package --sign --key helm --keyring path/to/keyring.secret mychart
```
Tip: for GnuPG users, your secret keyring is in `~/.gpg/secring.gpg`.
At this point, you should see both `mychart-0.1.0.tgz` and `mychart-0.1.0.tgz.prov`.
Both files should eventually be uploaded to your desired chart repository.
You can verify a chart using `helm verify`:
```
$ helm verify mychart-0.1.0.tgz
```
A failed verification looks like this:
```
$ helm verify topchart-0.1.0.tgz
Error: sha256 sum does not match for topchart-0.1.0.tgz: "sha256:1939fbf7c1023d2f6b865d137bbb600e0c42061c3235528b1e8c82f4450c12a7" != "sha256:5a391a90de56778dd3274e47d789a2c84e0e106e1a37ef8cfa51fd60ac9e623a"
```
To verify during an install, use the `--verify` flag.
```
$ helm install --verify mychart-0.1.0.tgz
```
If the keyring is not in the default location, you may need to point to the
keyring with `--keyring PATH` as in the `helm package` example.
If verification fails, the install will be aborted before the chart is even pushed
up to Tiller.
### Reasons a chart may not verify
These are common reasons for failure.
- The prov file is missing or corrupt. This indicates that something is misconfigured
or that the original maintainer did not create a provenance file.
- The key used to sign the file is not in your keyring. This indicate that the
entity who signed the chart is not someone you've already signaled that you trust.
- The verification of the prov file failed. This indicates that something is wrong
with either the chart or the provenance data.
- The file hashes in the provenance file do not match the hash of the archive file. This
indicates that the archive has been tampered with.
If a verification fails, there is reason to distrust the package.
## The Provenance File
The provenance file contains a charts YAML file plus several pieces of
verification information. Provenance files are designed to be automatically
generated.
The following pieces of provenance data are added:
* The chart file (Chart.yaml) is included to give both humans and tools an easy
view into the contents of the chart.
* **Not Complete yet:** Every image file that the project references is
correlated with its hash (SHA256, used by Docker) for verification.
* The signature (SHA256, just like Docker) of the chart package (the .tgz file)
is included, and may be used to verify the integrity of the chart package.
* The entire body is signed using the algorithm used by PGP (see
[http://keybase.io] for an emerging way of making crypto signing and
verification easy).
The combination of this gives users the following assurances:
* The images this chart references at build time are still the same exact
version when installed (checksum images).
* This is distinct from asserting that the image Kubernetes is running is
exactly the same version that a chart references. Kubernetes does not
currently give us a way of verifying this.
* The package itself has not been tampered with (checksum package tgz).
* The entity who released this package is known (via the GnuPG/PGP signature).
The format of the file looks something like this:
```
-----BEGIN PGP SIGNED MESSAGE-----
name: nginx
description: The nginx web server as a replication controller and service pair.
version: 0.5.1
keywords:
- https
- http
- web server
- proxy
source:
- https://github.com/foo/bar
home: http://nginx.com
...
files:
nginx-0.5.1.tgz: “sha256:9f5270f50fc842cfcb717f817e95178f”
images:
“hub.docker.com/_/nginx:5.6.0”: “sha256:f732c04f585170ed3bc99”
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
iEYEARECAAYFAkjilUEACgQkB01zfu119ZnHuQCdGCcg2YxF3XFscJLS4lzHlvte
WkQAmQGHuuoLEJuKhRNo+Wy7mhE7u1YG
=eifq
-----END PGP SIGNATURE-----
```
Note that the YAML section contains two documents (separated by `...\n`). The
first is the Chart.yaml. The second is the checksums, defined as follows.
* Files: A map of filenames to SHA-256 checksums (value shown is
fake/truncated)
* Images: A map of image URLs to checksums (value shown is fake/truncated)
The signature block is a standard PGP signature, which provides [tamper
resistance](http://www.rossde.com/PGP/pgp_signatures.html).
## Chart Repositories
Chart repositories serve as a centralized collection of Helm charts.
Chart repositories must make it possible to serve provenance files over HTTP via
a specific request, and must make them available at the same URI path as the chart.
For example, if the base URL for a package is `https://example.com/charts/mychart-1.2.3.tgz`,
the provenance file, if it exists, MUST be accessible at `https://example.com/charts/mychart-1.2.3.tgz.prov`.
From the end user's perspective, `helm install --verify myrepo/mychart-1.2.3`
should result in the download of both the chart and the provenance file with no
additional user configuration or action.

@ -23,7 +23,6 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
@ -42,7 +41,7 @@ var defaultPGPConfig = packet.Config{
DefaultHash: crypto.SHA512,
}
// SumCollection represents a collecton of file and image checksums.
// SumCollection represents a collection of file and image checksums.
//
// Files are of the form:
// FILENAME: "sha256:SUM"
@ -55,6 +54,14 @@ type SumCollection struct {
Images map[string]string `json:"images,omitempty"`
}
// Verification contains information about a verification operation.
type Verification struct {
// SignedBy contains the entity that signed a chart.
SignedBy *openpgp.Entity
// FileHash is the hash, prepended with the scheme, for the file that was verified.
FileHash string
}
// Signatory signs things.
//
// Signatories can be constructed from a PGP private key file using NewFromFiles
@ -174,32 +181,50 @@ func (s *Signatory) ClearSign(chartpath string) (string, error) {
}
// Verify checks a signature and verifies that it is legit for a chart.
func (s *Signatory) Verify(chartpath, sigpath string) (bool, error) {
func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) {
ver := &Verification{}
for _, fname := range []string{chartpath, sigpath} {
if fi, err := os.Stat(fname); err != nil {
return false, err
return ver, err
} else if fi.IsDir() {
return false, fmt.Errorf("%s cannot be a directory", fname)
return ver, fmt.Errorf("%s cannot be a directory", fname)
}
}
// First verify the signature
sig, err := s.decodeSignature(sigpath)
if err != nil {
return false, fmt.Errorf("failed to decode signature: %s", err)
return ver, fmt.Errorf("failed to decode signature: %s", err)
}
by, err := s.verifySignature(sig)
if err != nil {
return false, err
}
for n := range by.Identities {
log.Printf("info: %s signed by %q", sigpath, n)
return ver, err
}
ver.SignedBy = by
// Second, verify the hash of the tarball.
sum, err := sumArchive(chartpath)
if err != nil {
return ver, err
}
_, sums, err := parseMessageBlock(sig.Plaintext)
if err != nil {
return ver, err
}
sum = "sha256:" + sum
basename := filepath.Base(chartpath)
if sha, ok := sums.Files[basename]; !ok {
return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", basename)
} else if sha != sum {
return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum)
}
ver.FileHash = sum
// TODO: when image signing is added, verify that here.
return true, nil
return ver, nil
}
func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) {

@ -218,13 +218,15 @@ func TestVerify(t *testing.T) {
t.Fatal(err)
}
passed, err := signer.Verify(testChartfile, testSigBlock)
if !passed {
if ver, err := signer.Verify(testChartfile, testSigBlock); err != nil {
t.Errorf("Failed to pass verify. Err: %s", err)
} else if len(ver.FileHash) == 0 {
t.Error("Verification is missing hash.")
} else if ver.SignedBy == nil {
t.Error("No SignedBy field")
}
passed, err = signer.Verify(testChartfile, testTamperedSigBlock)
if passed {
if _, err = signer.Verify(testChartfile, testTamperedSigBlock); err == nil {
t.Errorf("Expected %s to fail.", testTamperedSigBlock)
}

Loading…
Cancel
Save