Merge pull request #988 from technosophos/feat/sig-files

feat(pkg/provenance): add OpenPGP signatures
pull/1099/head
Matt Butcher 9 years ago committed by GitHub
commit 01e2d367dd

@ -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

@ -94,6 +94,9 @@ func newRootCmd(out io.Writer) *cobra.Command {
newStatusCmd(nil, out),
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,9 +56,7 @@ func TestListCmd(t *testing.T) {
rels: tt.resp,
}
cmd := newListCmd(c, &buf)
for flag, value := range tt.flags {
cmd.Flags().Set(flag, value)
}
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)

@ -17,12 +17,18 @@ limitations under the License.
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/repo"
)
@ -37,30 +43,51 @@ Chart.yaml file, and (if found) build the current directory into a chart.
Versioned chart archives are used by Helm package repositories.
`
var save bool
func init() {
packageCmd.Flags().BoolVar(&save, "save", true, "save packaged chart to local chart repository")
RootCommand.AddCommand(packageCmd)
type packageCmd struct {
save bool
sign bool
path string
key string
keyring string
out io.Writer
}
var packageCmd = &cobra.Command{
Use: "package [CHART_PATH]",
Short: "package a chart directory into a chart archive",
Long: packageDesc,
RunE: runPackage,
}
func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command {
pkg := &packageCmd{
out: out,
}
cmd := &cobra.Command{
Use: "package [CHART_PATH]",
Short: "package a chart directory into a chart archive",
Long: packageDesc,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("This command needs at least one argument, the path to the chart.")
}
pkg.path = args[0]
if pkg.sign {
if pkg.key == "" {
return errors.New("--key is required for signing a package")
}
if pkg.keyring == "" {
return errors.New("--keyring is required for signing a package")
}
}
return pkg.run(cmd, args)
},
}
func runPackage(cmd *cobra.Command, args []string) error {
path := "."
f := cmd.Flags()
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", defaultKeyring(), "the location of a public keyring")
if len(args) > 0 {
path = args[0]
} else {
return fmt.Errorf("This command needs at least one argument, the path to the chart.")
}
return cmd
}
path, err := filepath.Abs(path)
func (p *packageCmd) run(cmd *cobra.Command, args []string) error {
path, err := filepath.Abs(p.path)
if err != nil {
return err
}
@ -86,7 +113,7 @@ func runPackage(cmd *cobra.Command, args []string) error {
// Save to $HELM_HOME/local directory. This is second, because we don't want
// the case where we saved here, but didn't save to the default destination.
if save {
if p.save {
if err := repo.AddChartToLocalRepo(ch, localRepoDirectory()); err != nil {
return err
} else if flagDebug {
@ -94,5 +121,28 @@ func runPackage(cmd *cobra.Command, args []string) error {
}
}
if p.sign {
err = p.clearsign(name)
}
return err
}
func (p *packageCmd) clearsign(filename string) error {
// Load keyring
signer, err := provenance.NewFromKeyring(p.keyring, p.key)
if err != nil {
return err
}
sig, err := signer.ClearSign(filename)
if err != nil {
return err
}
if flagDebug {
fmt.Fprintln(p.out, sig)
}
return ioutil.WriteFile(filename+".prov", []byte(sig), 0755)
}

@ -0,0 +1,148 @@
/*
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"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"testing"
"github.com/spf13/cobra"
)
func TestPackage(t *testing.T) {
tests := []struct {
name string
flags map[string]string
args []string
expect string
hasfile string
err bool
}{
{
name: "package without chart path",
args: []string{},
flags: map[string]string{},
expect: "This command needs at least one argument, the path to the chart.",
err: true,
},
{
name: "package --sign, no --key",
args: []string{"testdata/testcharts/alpine"},
flags: map[string]string{"sign": "1"},
expect: "key is required for signing a package",
err: true,
},
{
name: "package --sign, no --keyring",
args: []string{"testdata/testcharts/alpine"},
flags: map[string]string{"sign": "1", "key": "nosuchkey", "keyring": ""},
expect: "keyring is required for signing a package",
err: true,
},
{
name: "package testdata/testcharts/alpine",
args: []string{"testdata/testcharts/alpine"},
expect: "",
hasfile: "alpine-0.1.0.tgz",
},
{
name: "package --sign --key=KEY --keyring=KEYRING testdata/testcharts/alpine",
args: []string{"testdata/testcharts/alpine"},
flags: map[string]string{"sign": "1", "keyring": "testdata/helm-test-key.secret", "key": "helm-test"},
expect: "",
hasfile: "alpine-0.1.0.tgz",
},
}
// Because these tests are destructive, we run them in a tempdir.
origDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
tmp, err := ioutil.TempDir("", "helm-package-test-")
if err != nil {
t.Fatal(err)
}
t.Logf("Running tests in %s", tmp)
if err := os.Chdir(tmp); err != nil {
t.Fatal(err)
}
defer func() {
os.Chdir(origDir)
os.RemoveAll(tmp)
}()
for _, tt := range tests {
buf := bytes.NewBuffer(nil)
c := newPackageCmd(nil, buf)
// This is an unfortunate byproduct of the tmpdir
if v, ok := tt.flags["keyring"]; ok && len(v) > 0 {
tt.flags["keyring"] = filepath.Join(origDir, v)
}
setFlags(c, tt.flags)
re := regexp.MustCompile(tt.expect)
adjustedArgs := make([]string, len(tt.args))
for i, f := range tt.args {
adjustedArgs[i] = filepath.Join(origDir, f)
}
err := c.RunE(c, adjustedArgs)
if err != nil {
if tt.err && re.MatchString(err.Error()) {
continue
}
t.Errorf("%q: expected error %q, got %q", tt.name, tt.expect, err)
continue
}
if !re.Match(buf.Bytes()) {
t.Errorf("%q: expected output %q, got %q", tt.name, tt.expect, buf.String())
}
if len(tt.hasfile) > 0 {
if fi, err := os.Stat(tt.hasfile); err != nil {
t.Errorf("%q: expected file %q, got err %q", tt.name, tt.hasfile, err)
} else if fi.Size() == 0 {
t.Errorf("%q: file %q has zero bytes.", tt.name, tt.hasfile)
}
}
if v, ok := tt.flags["sign"]; ok && v == "1" {
if fi, err := os.Stat(tt.hasfile + ".prov"); err != nil {
t.Errorf("%q: expected provenance file", tt.name)
} else if fi.Size() == 0 {
t.Errorf("%q: provenance file is empty", tt.name)
}
}
}
}
func setFlags(cmd *cobra.Command, flags map[string]string) {
dest := cmd.Flags()
for f, v := range flags {
dest.Set(f, v)
}
}

Binary file not shown.

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.

15
glide.lock generated

@ -1,5 +1,5 @@
hash: 410e784360a10f716d4bf4d22decf81f75b327d051b3f2d23f55aa9049c09676
updated: 2016-08-19T12:19:48.074620307-06:00
hash: 05c56f2ae4c8bcbaf2c428e2e070ec00f865b284ea61dd671e2c4e117f2d6528
updated: 2016-08-19T17:30:32.462379907-06:00
imports:
- name: github.com/aokoli/goutils
version: 9c37978a95bd5c709a15883b6242714ea6709e64
@ -247,6 +247,17 @@ imports:
subpackages:
- codec
- codec/codecgen
- name: golang.org/x/crypto
version: c84e1f8e3a7e322d497cd16c0e8a13c7e127baf3
subpackages:
- cast5
- openpgp
- openpgp/armor
- openpgp/clearsign
- openpgp/elgamal
- openpgp/errors
- openpgp/packet
- openpgp/s2k
- name: golang.org/x/net
version: fb93926129b8ec0056f2f458b1f519654814edf0
subpackages:

@ -50,3 +50,6 @@ import:
- package: google.golang.org/cloud
vcs: git
repo: https://code.googlesource.com/gocloud
- package: golang.org/x/crypto
subpackages:
- openpgp

@ -0,0 +1,37 @@
/*
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 provenance provides tools for establishing the authenticity of a chart.
In Helm, provenance is established via several factors. The primary factor is the
cryptographic signature of a chart. Chart authors may sign charts, which in turn
provide the necessary metadata to ensure the integrity of the chart file, the
Chart.yaml, and the referenced Docker images.
A provenance file is clear-signed. This provides cryptographic verification that
a particular block of information (Chart.yaml, archive file, images) have not
been tampered with or altered. To learn more, read the GnuPG documentation on
clear signatures:
https://www.gnupg.org/gph/en/manual/x135.html
The cryptography used by Helm should be compatible with OpenGPG. For example,
you should be able to verify a signature by importing the desired public key
and using `gpg --verify`, `keybase pgp verify`, or similar:
$ gpg --verify some.sig
gpg: Signature made Mon Jul 25 17:23:44 2016 MDT using RSA key ID 1FC18762
gpg: Good signature from "Helm Testing (This key should only be used for testing. DO NOT TRUST.) <helm-testing@helm.sh>" [ultimate]
*/
package provenance // import "k8s.io/helm/pkg/provenance"

@ -0,0 +1,351 @@
/*
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 provenance
import (
"bytes"
"crypto"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/ghodss/yaml"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/clearsign"
"golang.org/x/crypto/openpgp/packet"
"k8s.io/helm/pkg/chartutil"
hapi "k8s.io/helm/pkg/proto/hapi/chart"
)
var defaultPGPConfig = packet.Config{
DefaultHash: crypto.SHA512,
}
// SumCollection represents a collection of file and image checksums.
//
// Files are of the form:
// FILENAME: "sha256:SUM"
// Images are of the form:
// "IMAGE:TAG": "sha256:SUM"
// Docker optionally supports sha512, and if this is the case, the hash marker
// will be 'sha512' instead of 'sha256'.
type SumCollection struct {
Files map[string]string `json:"files"`
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
// or they can be constructed manually by setting the Entity to a valid
// PGP entity.
//
// The same Signatory can be used to sign or validate multiple charts.
type Signatory struct {
// The signatory for this instance of Helm. This is used for signing.
Entity *openpgp.Entity
// The keyring for this instance of Helm. This is used for verification.
KeyRing openpgp.EntityList
}
// NewFromFiles constructs a new Signatory from the PGP key in the given filename.
//
// This will emit an error if it cannot find a valid GPG keyfile (entity) at the
// given location.
//
// Note that the keyfile may have just a public key, just a private key, or
// both. The Signatory methods may have different requirements of the keys. For
// example, ClearSign must have a valid `openpgp.Entity.PrivateKey` before it
// can sign something.
func NewFromFiles(keyfile, keyringfile string) (*Signatory, error) {
e, err := loadKey(keyfile)
if err != nil {
return nil, err
}
ring, err := loadKeyRing(keyringfile)
if err != nil {
return nil, err
}
return &Signatory{
Entity: e,
KeyRing: ring,
}, nil
}
// NewFromKeyring reads a keyring file and creates a Signatory.
//
// If id is not the empty string, this will also try to find an Entity in the
// keyring whose name matches, and set that as the signing entity. It will return
// an error if the id is not empty and also not found.
func NewFromKeyring(keyringfile, id string) (*Signatory, error) {
ring, err := loadKeyRing(keyringfile)
if err != nil {
return nil, err
}
s := &Signatory{KeyRing: ring}
// If the ID is empty, we can return now.
if id == "" {
return s, nil
}
// We're gonna go all GnuPG on this and look for a string that _contains_. If
// two or more keys contain the string and none are a direct match, we error
// out.
var candidate *openpgp.Entity
vague := false
for _, e := range ring {
for n := range e.Identities {
if n == id {
s.Entity = e
return s, nil
}
if strings.Contains(n, id) {
if candidate != nil {
vague = true
}
candidate = e
}
}
}
if vague {
return s, fmt.Errorf("more than one key contain the id %q", id)
}
s.Entity = candidate
return s, nil
}
// ClearSign signs a chart with the given key.
//
// This takes the path to a chart archive file and a key, and it returns a clear signature.
//
// The Signatory must have a valid Entity.PrivateKey for this to work. If it does
// not, an error will be returned.
func (s *Signatory) ClearSign(chartpath string) (string, error) {
if s.Entity.PrivateKey == nil {
return "", errors.New("private key not found")
}
if fi, err := os.Stat(chartpath); err != nil {
return "", err
} else if fi.IsDir() {
return "", errors.New("cannot sign a directory")
}
out := bytes.NewBuffer(nil)
b, err := messageBlock(chartpath)
if err != nil {
return "", nil
}
// Sign the buffer
w, err := clearsign.Encode(out, s.Entity.PrivateKey, &defaultPGPConfig)
if err != nil {
return "", err
}
_, err = io.Copy(w, b)
w.Close()
return out.String(), err
}
// Verify checks a signature and verifies that it is legit for a chart.
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 ver, err
} else if fi.IsDir() {
return ver, fmt.Errorf("%s cannot be a directory", fname)
}
}
// First verify the signature
sig, err := s.decodeSignature(sigpath)
if err != nil {
return ver, fmt.Errorf("failed to decode signature: %s", err)
}
by, err := s.verifySignature(sig)
if err != nil {
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 ver, nil
}
func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
block, _ := clearsign.Decode(data)
if block == nil {
// There was no sig in the file.
return nil, errors.New("signature block not found")
}
return block, nil
}
// verifySignature verifies that the given block is validly signed, and returns the signer.
func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) {
return openpgp.CheckDetachedSignature(
s.KeyRing,
bytes.NewBuffer(block.Bytes),
block.ArmoredSignature.Body,
)
}
func messageBlock(chartpath string) (*bytes.Buffer, error) {
var b *bytes.Buffer
// Checksum the archive
chash, err := sumArchive(chartpath)
if err != nil {
return b, err
}
base := filepath.Base(chartpath)
sums := &SumCollection{
Files: map[string]string{
base: "sha256:" + chash,
},
}
// Load the archive into memory.
chart, err := chartutil.LoadFile(chartpath)
if err != nil {
return b, err
}
// Buffer a hash + checksums YAML file
data, err := yaml.Marshal(chart.Metadata)
if err != nil {
return b, err
}
// FIXME: YAML uses ---\n as a file start indicator, but this is not legal in a PGP
// clearsign block. So we use ...\n, which is the YAML document end marker.
// http://yaml.org/spec/1.2/spec.html#id2800168
b = bytes.NewBuffer(data)
b.WriteString("\n...\n")
data, err = yaml.Marshal(sums)
if err != nil {
return b, err
}
b.Write(data)
return b, nil
}
// parseMessageBlock
func parseMessageBlock(data []byte) (*hapi.Metadata, *SumCollection, error) {
// This sucks.
parts := bytes.Split(data, []byte("\n...\n"))
if len(parts) < 2 {
return nil, nil, errors.New("message block must have at least two parts")
}
md := &hapi.Metadata{}
sc := &SumCollection{}
if err := yaml.Unmarshal(parts[0], md); err != nil {
return md, sc, err
}
err := yaml.Unmarshal(parts[1], sc)
return md, sc, err
}
// loadKey loads a GPG key found at a particular path.
func loadKey(keypath string) (*openpgp.Entity, error) {
f, err := os.Open(keypath)
if err != nil {
return nil, err
}
defer f.Close()
pr := packet.NewReader(f)
return openpgp.ReadEntity(pr)
}
func loadKeyRing(ringpath string) (openpgp.EntityList, error) {
f, err := os.Open(ringpath)
if err != nil {
return nil, err
}
defer f.Close()
return openpgp.ReadKeyRing(f)
}
// sumArchive calculates a SHA256 hash (like Docker) for a given file.
//
// It takes the path to the archive file, and returns a string representation of
// the SHA256 sum.
//
// The intended use of this function is to generate a sum of a chart TGZ file.
func sumArchive(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close()
hash := crypto.SHA256.New()
io.Copy(hash, f)
return hex.EncodeToString(hash.Sum(nil)), nil
}

@ -0,0 +1,251 @@
/*
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 provenance
import (
"io/ioutil"
"os"
"strings"
"testing"
pgperrors "golang.org/x/crypto/openpgp/errors"
)
const (
// testKeyFile is the secret key.
// Generating keys should be done with `gpg --gen-key`. The current key
// was generated to match Go's defaults (RSA/RSA 2048). It has no pass
// phrase. Use `gpg --export-secret-keys helm-test` to export the secret.
testKeyfile = "testdata/helm-test-key.secret"
// testPubfile is the public key file.
// Use `gpg --export helm-test` to export the public key.
testPubfile = "testdata/helm-test-key.pub"
// Generated name for the PGP key in testKeyFile.
testKeyName = `Helm Testing (This key should only be used for testing. DO NOT TRUST.) <helm-testing@helm.sh>`
testChartfile = "testdata/hashtest-1.2.3.tgz"
// testSigBlock points to a signature generated by an external tool.
// This file was generated with GnuPG:
// gpg --clearsign -u helm-test --openpgp testdata/msgblock.yaml
testSigBlock = "testdata/msgblock.yaml.asc"
// testTamperedSigBlock is a tampered copy of msgblock.yaml.asc
testTamperedSigBlock = "testdata/msgblock.yaml.tampered"
// testSumfile points to a SHA256 sum generated by an external tool.
// We always want to validate against an external tool's representation to
// verify that we haven't done something stupid. This file was generated
// with shasum.
// shasum -a 256 hashtest-1.2.3.tgz > testdata/hashtest.sha256
testSumfile = "testdata/hashtest.sha256"
)
// testMessageBlock represents the expected message block for the testdata/hashtest chart.
const testMessageBlock = `description: Test chart versioning
name: hashtest
version: 1.2.3
...
files:
hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75
`
func TestMessageBlock(t *testing.T) {
out, err := messageBlock(testChartfile)
if err != nil {
t.Fatal(err)
}
got := out.String()
if got != testMessageBlock {
t.Errorf("Expected:\n%q\nGot\n%q\n", testMessageBlock, got)
}
}
func TestParseMessageBlock(t *testing.T) {
md, sc, err := parseMessageBlock([]byte(testMessageBlock))
if err != nil {
t.Fatal(err)
}
if md.Name != "hashtest" {
t.Errorf("Expected name %q, got %q", "hashtest", md.Name)
}
if lsc := len(sc.Files); lsc != 1 {
t.Errorf("Expected 1 file, got %d", lsc)
}
if hash, ok := sc.Files["hashtest-1.2.3.tgz"]; !ok {
t.Errorf("hashtest file not found in Files")
} else if hash != "sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75" {
t.Errorf("Unexpected hash: %q", hash)
}
}
func TestLoadKey(t *testing.T) {
k, err := loadKey(testKeyfile)
if err != nil {
t.Fatal(err)
}
if _, ok := k.Identities[testKeyName]; !ok {
t.Errorf("Expected to load a key for user %q", testKeyName)
}
}
func TestLoadKeyRing(t *testing.T) {
k, err := loadKeyRing(testPubfile)
if err != nil {
t.Fatal(err)
}
if len(k) > 1 {
t.Errorf("Expected 1, got %d", len(k))
}
for _, e := range k {
if ii, ok := e.Identities[testKeyName]; !ok {
t.Errorf("Expected %s in %v", testKeyName, ii)
}
}
}
func TestNewFromFiles(t *testing.T) {
s, err := NewFromFiles(testKeyfile, testPubfile)
if err != nil {
t.Fatal(err)
}
if _, ok := s.Entity.Identities[testKeyName]; !ok {
t.Errorf("Expected to load a key for user %q", testKeyName)
}
}
func TestSumArchive(t *testing.T) {
hash, err := sumArchive(testChartfile)
if err != nil {
t.Fatal(err)
}
sig, err := readSumFile(testSumfile)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(sig, hash) {
t.Errorf("Expected %s to be in %s", hash, sig)
}
}
func TestClearSign(t *testing.T) {
signer, err := NewFromFiles(testKeyfile, testPubfile)
if err != nil {
t.Fatal(err)
}
sig, err := signer.ClearSign(testChartfile)
if err != nil {
t.Fatal(err)
}
t.Logf("Sig:\n%s", sig)
if !strings.Contains(sig, testMessageBlock) {
t.Errorf("expected message block to be in sig: %s", sig)
}
}
func TestDecodeSignature(t *testing.T) {
// Unlike other tests, this does a round-trip test, ensuring that a signature
// generated by the library can also be verified by the library.
signer, err := NewFromFiles(testKeyfile, testPubfile)
if err != nil {
t.Fatal(err)
}
sig, err := signer.ClearSign(testChartfile)
if err != nil {
t.Fatal(err)
}
f, err := ioutil.TempFile("", "helm-test-sig-")
if err != nil {
t.Fatal(err)
}
tname := f.Name()
defer func() {
os.Remove(tname)
}()
f.WriteString(sig)
f.Close()
sig2, err := signer.decodeSignature(tname)
if err != nil {
t.Fatal(err)
}
by, err := signer.verifySignature(sig2)
if err != nil {
t.Fatal(err)
}
if _, ok := by.Identities[testKeyName]; !ok {
t.Errorf("Expected identity %q", testKeyName)
}
}
func TestVerify(t *testing.T) {
signer, err := NewFromFiles(testKeyfile, testPubfile)
if err != nil {
t.Fatal(err)
}
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")
}
if _, err = signer.Verify(testChartfile, testTamperedSigBlock); err == nil {
t.Errorf("Expected %s to fail.", testTamperedSigBlock)
}
switch err.(type) {
case pgperrors.SignatureError:
t.Logf("Tampered sig block error: %s (%T)", err, err)
default:
t.Errorf("Expected invalid signature error, got %q (%T)", err, err)
}
}
// readSumFile reads a file containing a sum generated by the UNIX shasum tool.
func readSumFile(sumfile string) (string, error) {
data, err := ioutil.ReadFile(sumfile)
if err != nil {
return "", err
}
sig := string(data)
parts := strings.SplitN(sig, " ", 2)
return parts[0], nil
}

Binary file not shown.

@ -0,0 +1 @@
8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 hashtest-1.2.3.tgz

@ -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: Test chart versioning
name: hashtest
version: 1.2.3

@ -0,0 +1,4 @@
# Default values for hashtest.
# This is a YAML-formatted file.
# Declare name/value pairs to be passed into your templates.
# name: value

Binary file not shown.

Binary file not shown.

@ -0,0 +1,7 @@
description: Test chart versioning
name: hashtest
version: 1.2.3
...
files:
hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75

@ -0,0 +1,21 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
description: Test chart versioning
name: hashtest
version: 1.2.3
...
files:
hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75
-----BEGIN PGP SIGNATURE-----
Comment: GPGTools - https://gpgtools.org
iQEcBAEBCgAGBQJXlp8KAAoJEIQ7v5gfwYdiE7sIAJYDiza+asekeooSXLvQiK+G
PKnveqQpx49EZ6L7Y7UlW25SyH8EjXXHeJysDywCXF3w4luxN9n56ffU0KEW11IY
F+JSjmgIWLS6ti7ZAGEi6JInQ/30rOAIpTEBRBL2IueW3m63mezrGK6XkBlGqpor
C9WKeqLi+DWlMoBtsEy3Uk0XP6pn/qBFICYAbLQQU0sCCUT8CBA8f8aidxi7aw9t
i404yYF+Dvc6i4JlSG77SV0ZJBWllUvsWoCd9Jli0NAuaMqmE7mzcEt/dE+Fm2Ql
Bx3tr1WS4xTRiFQdcOttOl93H+OaHTh+Y0qqLTzzpCvqmttG0HfI6lMeCs7LeyA=
=vEK+
-----END PGP SIGNATURE-----

@ -0,0 +1,21 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
description: Test chart versioning
name: hashtest
version: 1.2.3+tampered
...
files:
hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75
-----BEGIN PGP SIGNATURE-----
Comment: GPGTools - https://gpgtools.org
iQEcBAEBCgAGBQJXlp8KAAoJEIQ7v5gfwYdiE7sIAJYDiza+asekeooSXLvQiK+G
PKnveqQpx49EZ6L7Y7UlW25SyH8EjXXHeJysDywCXF3w4luxN9n56ffU0KEW11IY
F+JSjmgIWLS6ti7ZAGEi6JInQ/30rOAIpTEBRBL2IueW3m63mezrGK6XkBlGqpor
C9WKeqLi+DWlMoBtsEy3Uk0XP6pn/qBFICYAbLQQU0sCCUT8CBA8f8aidxi7aw9t
i404yYF+Dvc6i4JlSG77SV0ZJBWllUvsWoCd9Jli0NAuaMqmE7mzcEt/dE+Fm2Ql
Bx3tr1WS4xTRiFQdcOttOl93H+OaHTh+Y0qqLTzzpCvqmttG0HfI6lMeCs7LeyA=
=vEK+
-----END PGP SIGNATURE-----

@ -0,0 +1,3 @@
#!/bin/sh
helm package hashtest
shasum -a 256 hashtest-1.2.3.tgz > hashtest.sha256
Loading…
Cancel
Save