From ce83a8a7776809d2257cc966f62d2cc857cf672b Mon Sep 17 00:00:00 2001
From: Matt Butcher <mbutcher@engineyard.com>
Date: Mon, 25 Jul 2016 17:34:05 -0600
Subject: [PATCH] feat(pkg/provenance): add OpenPGP signatures

This adds support for OpenPGP signatures containing provenance data.
Such information can be used to verify the integrity of a Chart by
testing that its file hash, metadata, and images are correct.

This first PR does not contain all of the tooling necessary for
end-to-end chart integrity. It contains just the library.

See #983
---
 glide.lock                                    |  15 +-
 glide.yaml                                    |   3 +
 pkg/provenance/doc.go                         |  37 +++
 pkg/provenance/sign.go                        | 280 ++++++++++++++++++
 pkg/provenance/sign_test.go                   | 249 ++++++++++++++++
 pkg/provenance/testdata/hashtest-1.2.3.tgz    | Bin 0 -> 465 bytes
 pkg/provenance/testdata/hashtest.sha256       |   1 +
 pkg/provenance/testdata/hashtest/.helmignore  |   5 +
 pkg/provenance/testdata/hashtest/Chart.yaml   |   3 +
 pkg/provenance/testdata/hashtest/values.yaml  |   4 +
 pkg/provenance/testdata/helm-test-key.pub     | Bin 0 -> 1243 bytes
 pkg/provenance/testdata/helm-test-key.secret  | Bin 0 -> 2545 bytes
 pkg/provenance/testdata/msgblock.yaml         |   7 +
 pkg/provenance/testdata/msgblock.yaml.asc     |  21 ++
 .../testdata/msgblock.yaml.tampered           |  21 ++
 pkg/provenance/testdata/regen-hashtest.sh     |   3 +
 16 files changed, 647 insertions(+), 2 deletions(-)
 create mode 100644 pkg/provenance/doc.go
 create mode 100644 pkg/provenance/sign.go
 create mode 100644 pkg/provenance/sign_test.go
 create mode 100644 pkg/provenance/testdata/hashtest-1.2.3.tgz
 create mode 100644 pkg/provenance/testdata/hashtest.sha256
 create mode 100644 pkg/provenance/testdata/hashtest/.helmignore
 create mode 100755 pkg/provenance/testdata/hashtest/Chart.yaml
 create mode 100644 pkg/provenance/testdata/hashtest/values.yaml
 create mode 100644 pkg/provenance/testdata/helm-test-key.pub
 create mode 100644 pkg/provenance/testdata/helm-test-key.secret
 create mode 100644 pkg/provenance/testdata/msgblock.yaml
 create mode 100644 pkg/provenance/testdata/msgblock.yaml.asc
 create mode 100644 pkg/provenance/testdata/msgblock.yaml.tampered
 create mode 100755 pkg/provenance/testdata/regen-hashtest.sh

diff --git a/glide.lock b/glide.lock
index 1bf1d6940..808be278c 100644
--- a/glide.lock
+++ b/glide.lock
@@ -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:
diff --git a/glide.yaml b/glide.yaml
index ed96b0204..2f7567469 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -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
diff --git a/pkg/provenance/doc.go b/pkg/provenance/doc.go
new file mode 100644
index 000000000..dacfa9e69
--- /dev/null
+++ b/pkg/provenance/doc.go
@@ -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"
diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go
new file mode 100644
index 000000000..3dd8cfb74
--- /dev/null
+++ b/pkg/provenance/sign.go
@@ -0,0 +1,280 @@
+/*
+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"
+	"log"
+	"os"
+	"path/filepath"
+
+	"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 collecton 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"`
+}
+
+// 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
+}
+
+// Sign 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
+}
+
+func (s *Signatory) Verify(chartpath, sigpath string) (bool, error) {
+	for _, fname := range []string{chartpath, sigpath} {
+		if fi, err := os.Stat(fname); err != nil {
+			return false, err
+		} else if fi.IsDir() {
+			return false, 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)
+	}
+
+	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)
+	}
+
+	// Second, verify the hash of the tarball.
+
+	return true, 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
+}
diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go
new file mode 100644
index 000000000..5dfd6bd68
--- /dev/null
+++ b/pkg/provenance/sign_test.go
@@ -0,0 +1,249 @@
+/*
+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)
+	}
+
+	passed, err := signer.Verify(testChartfile, testSigBlock)
+	if !passed {
+		t.Errorf("Failed to pass verify. Err: %s", err)
+	}
+
+	passed, err = signer.Verify(testChartfile, testTamperedSigBlock)
+	if passed {
+		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
+}
diff --git a/pkg/provenance/testdata/hashtest-1.2.3.tgz b/pkg/provenance/testdata/hashtest-1.2.3.tgz
new file mode 100644
index 0000000000000000000000000000000000000000..1e89b524f4fc89e4a483ef8dc4fefdc79d64786f
GIT binary patch
literal 465
zcmV;?0WSU@iwG0|32ul0|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc
zVQyr3R8em|NM&qo0PL2*>(ek4$A9Pk6;F0i(Ac#6HrP$vQBl|~o+N8un_!xhB;DM9
z?@Oyirm(~2hL8_~X6Z|tviJF}Qg|8Ahqv#gaDkmfr=M<3POP4v$0Kom%z4h|@i@<m
z<tl%In{VWA3_b`)x4-r}=MVRvP@}J$>VvXo4LfQCsA40)0iCBgW!lV$4%hIjQL>+B
z*1%c8%Iwh(khqH3|AWv2`hOAtm;5hydFFq~%Od%I4;HY&Mhu#a9~%G~>t@$kwt$^f
z9_S<B%e#*s-q7hzgn%_PN@Fb8V5Nmah%qsB5%fYi55dAdj=iU9W$5e44h{l#y%u!&
zxOmEzaaBr^M=<-(B<fDJ#yY(9JudpcT>jxd=gI$juz=4|XJ<xVr{5or5)JGJrEB{C
zCxfeRqMdvl)4&gXZ(X1P^^AsA+dWVyRFbM+gF~`g9isU`{x&a69fikM#Fq_t**k31
zcZ!CiqPiEa7<@AN1lEiz<CdUD2U;158MfJe-3HoQ##$9pR3s9K#MS%=00960=<3*)
H01yBG#+u^@

literal 0
HcmV?d00001

diff --git a/pkg/provenance/testdata/hashtest.sha256 b/pkg/provenance/testdata/hashtest.sha256
new file mode 100644
index 000000000..829031f9d
--- /dev/null
+++ b/pkg/provenance/testdata/hashtest.sha256
@@ -0,0 +1 @@
+8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75  hashtest-1.2.3.tgz
diff --git a/pkg/provenance/testdata/hashtest/.helmignore b/pkg/provenance/testdata/hashtest/.helmignore
new file mode 100644
index 000000000..435b756d8
--- /dev/null
+++ b/pkg/provenance/testdata/hashtest/.helmignore
@@ -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
diff --git a/pkg/provenance/testdata/hashtest/Chart.yaml b/pkg/provenance/testdata/hashtest/Chart.yaml
new file mode 100755
index 000000000..342631ef8
--- /dev/null
+++ b/pkg/provenance/testdata/hashtest/Chart.yaml
@@ -0,0 +1,3 @@
+description: Test chart versioning
+name: hashtest
+version: 1.2.3
diff --git a/pkg/provenance/testdata/hashtest/values.yaml b/pkg/provenance/testdata/hashtest/values.yaml
new file mode 100644
index 000000000..0827a01fb
--- /dev/null
+++ b/pkg/provenance/testdata/hashtest/values.yaml
@@ -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
diff --git a/pkg/provenance/testdata/helm-test-key.pub b/pkg/provenance/testdata/helm-test-key.pub
new file mode 100644
index 0000000000000000000000000000000000000000..38714f25adaf701b08e11fd559a587074bbde0e4
GIT binary patch
literal 1243
zcmV<11SI>J0SyFKmTjH^2mr{k15wFPQdpTAAE<XxWtXrITzgx-V5`~*fl`c%qbny9
z0-%CFx|COAqyK?6%|{LoU;O@OgTmzJH8S&cEI7J5MU<pc!@S;=0XnlA$PYq{(v%A9
z3`0$OCjYP>clY4oV^-7nTy5x$&xF;PC4il}o-Pk4@#$knlp#(|J0GE?qli_lr;7-o
zyY8vBsN_GJe;#w<`JdR7riNL&RJlcS)FG+W=91;dYS6NZ2tY?kZ8Sw9{r=e4|L3E{
zRod|EPC!PWgW&pe&4qiqKQAijj;G~fyjcC^m%0p54Tn{h%YFKd0VC4n=#~SRc@BVd
znj66*)Om%*SEQfX{1*Tb0RRECT}WkYZ6H)-b98BLXCNq4XlZjGYh`&Lb7*gMY-AvB
zZftoVVr3w8b7f>8W^ZyJbY*jNX>MmOAVg0fPES-IR8mz_R4yqXJZNQXZ7p<Vb98BL
zXFzCWY;7)cXg-MnHv|&_3IHJm0#}x8o&p;K1q%rX2LlBa3JC}c1r`DW0RRFX0Rk6*
z0162Zggd{OAHj!WiA4wiwE5`M<<b|@JCT==9UG;<lt=v{x1o_)XwuGG8&w>6uVakV
zT7GGV$jaKjyjfI_a~N1!Hk?5C$0wa&4)R=i$v7t&ZMycW#RkavpF%A?><BFeV@yae
zEr<1?&8i4mWc$i527Fm5toJ9B3aL#!%(J%00`4P`QwSw4q@B(fipspQcpqr&Mo;;U
z3W$QDXti(n1+3cRH>MTT2anNDzOQUm<++zEOykJ9-@&c2QXq3owqf7fek`=L@+7iF
zv;IW2Q>Q&r+V@cWDF&hAUUsCKlDinerKgvJUJCl$5gjb7NhM{mBP%!M^mX-iS8xFf
zuLB{@MDqvtZzF#Bxd9CXSC(y_0SExW>8~h=U8|!do4*OJj2u#!KD<F};x(4i3gm1v
zg^boLy7o&cBy?~jMMx8xWT98p>e3v+1T<DcW~-y${+x<<z!#@Wyl$RK(q%+qDmDG5
zVl2(%UtFbnh08TY^?=*#lcK5U7D7<eW{jy@D;B9Fq2@2@DPP4Xc(;TpC<!#bBfgF2
zMhHH@gXdH_BF{eZ({Xf@gqwiTouCiJwVOY^#H$hf&MpErM-&ovKi)n}VD&rD$^{Z)
z02OVCKuZpnGG~9yIOKdkYHLY%Stp}TD<|`o`qx#f$T~oYi5LrUvn$9uOVx0Y2JNrE
zTx@T(b~!-dp}<O*rTa8>+aVzU5di=Ji2)x37y$|Z2?YXImTjH_8w>yn2@r%kznCAv
zhhp0`2mpxVj5j%o&5i)?`r7iES|8dA@p2kk@+XS(tjBGN)6>tm^=gayC<Y`($r_<`
zRJckL&Y<Mc=F_Zo^XU-8$DCv}`Zc~!4oqJC=rPF_!*&E>n`gTEC*K74Y~{<Z=a`j_
z8Pwx!`j8jE$dKDL&A6fI18315!F*dRIqtBPpR&1~2pI{racagT&hbW`Yk!K=g69vw
zoQuc^GnR6Jg(*0msuKhvK(fB?gEq4Ox@p7N`i0twY%_v02t(2^L@FeKvt>I_PREk)
z)PstIMx1RxB@cK8%Mey%;nVnKriAKUk2Ky?dBMG3uXItKL$3N(#3P^pQa*K$l)wUy
F^>pMLK0g2e

literal 0
HcmV?d00001

diff --git a/pkg/provenance/testdata/helm-test-key.secret b/pkg/provenance/testdata/helm-test-key.secret
new file mode 100644
index 0000000000000000000000000000000000000000..a966aef93ed97d01d764f29940738df6df2d9d24
GIT binary patch
literal 2545
zcmV<N2@dv^1DFI?mTjH^2mr{k15wFPQdpTAAE<XxWtXrITzgx-V5`~*fl`c%qbny9
z0-%CFx|COAqyK?6%|{LoU;O@OgTmzJH8S&cEI7J5MU<pc!@S;=0XnlA$PYq{(v%A9
z3`0$OCjYP>clY4oV^-7nTy5x$&xF;PC4il}o-Pk4@#$knlp#(|J0GE?qli_lr;7-o
zyY8vBsN_GJe;#w<`JdR7riNL&RJlcS)FG+W=91;dYS6NZ2tY?kZ8Sw9{r=e4|L3E{
zRod|EPC!PWgW&pe&4qiqKQAijj;G~fyjcC^m%0p54Tn{h%YFKd0VC4n=#~SRc@BVd
znj66*)Om%*SEQfX{1*Tb0RRC22mUT~!#(ymA#eaSp1lpODzX${Vf^l{qDyu}xC-Z;
zRnH<54GSVm<$?Ua1k#(+mu~3_*CIx=sPuoZB#9t`5)>)SncaZ0<~%)I$~BM-5aP3W
z%`ewoaI;P40uHnDeE!9-_o2L<WUNl-F;Js#`W6kXi+jf>r{wDfL45jGGU-JZ36<Vm
z-G-gp2LukasXGS)>T9ToJqMX(TnRN-EvGi{o6aI#oT_2HU(J8=theYZsj5h?ml@F2
zq<lMlQM=+y7G~T+Czeq}xse}?oj@{K)Lv?F6izCyr&-<>CpxqkdZi=~i+&Z}q^cR<
zq>lNT5cnJ5X@K!3vOww0B>@Bg*7x*i59vbegj}$ELl?K2l`+`uY;jn;@-#}^!(c8$
z&Y`@LLxZ_Y>^#gGbxsy-2s=w7cVmR@z_%b#0_e^qDmIrpKw6U7N;6^TN}@&nxKj6i
zje++&m}XQA&G8O8FX86?Frxrjmu5ktfDRyHBb|j&n&H#v>T!Mdmk8Y#1OV><pTA0<
zb0yYsL61Ho7aM6Dy<hT!gL-D}0e=_Al9WUJ(;Jgdi5uD9qdH6-C9##p(WjeROb)^#
z&>P(*gow;}0v-BdsmdUSV3M9tIkRO0OTBw16eYCzxs>OEG!?i}$^8yY+hFlb3GJ~F
z@#2Vimrfeb0(o3X?>!tSIROL!mGC1>cHXGVp;VD$oE{N!h=IF(C(PNLd6^nZO^!ix
zHnE%@Y*d~bJl_M}WW0D1EM+&xdQI5#y67>-8{P4^*?j9-RL!3>e89fC4fbJFTGXY*
zQA`Z&jZ*qV!0N>p>(<2RFPDhHj^h*B*O(i139Dwv{>MY%puY021Or@)I~ufINM&qo
zAXH^@bZKs9AShI5X>%ZJWqBZTXm53FWFT*DY<VDJWgvBPWn>^`Z*m}XWpi|CZf7na
zL{A`2PgEdOQdLt_E-4^9Xk~0|Ep%mbbZKs9Kxk!bZ7y?YK8XQ01QP)Y03iheSC(y_
z0viJb3ke7Z0|gZd2?z@X76JnS00JHX0vCV)3JDN|JHMD8!G~grMF;@2`RLQ-(ihS@
zk(ZDi8>PUMNBttVp^;f=(#~5ORUCP*V~o^Verbou%G$oXSyYd67+6|1oIv=;C!Jsp
z@?3ezI42oxy7sHZ2FUrJLM=V)2rUL<Oh_;-hxMV&st8(S`^ql{d|4^1_a~GJsZBl1
zv$n_r?jw;?2qiA0oz57F%Dl38A871GPx+1th=QVMwQu+ZtlHx@rWMo&kI_%QuWVK2
zxtA<V<H)+-!L71VAahK%Vc*++EVQ-qB(g=b{zSD?r#({I_fY&P2BG6#cBGn;yBNWx
zr<a3X3i{j;9W1O#C1tTAD>vozb@g^vZ~+Ui10l{t^9T2DBYydv1DFI?mTjH^2mrz9
zuPBIJtD_~GzX`6498#D*yg_W@HI~u}<ZLsAjMgl=_Dd-wbZ{g^NE4c5p;y)F(j37A
zG*&`ptE1okoQinB7pF_SZk|fgWkg~sHT|bzEY0L!T%~%2%QZ&zfZOboqN(T>LQvFZ
zjHz2I7O5nm<}d0gU&SbRw}dGu2{gYWzK!Qb2tL4r=Ttf(&pz_gadeY}n}E@spby2h
zn?Jq8s}cOpE&?`36cTnn-abrV^*hkY1rlNa6>W(?OAePZXMfE&<a|GBYe{!mC!<a)
zC-av2*Hx>?IzWku7z=T;E66)b)o_po?XSOFY;U!8IY8l|z)F~<`!sdiAt3+}0RRC2
z2mUKrERA52qzq^qU4-%uqeMA@h`$YTvMnKwO3MFdg819*{h|i5{tcC;Av-jm`%7`?
zISDa>*_u$~x5)kpVt_aYB_e`#K)Xd5tcJ05BQ>ps?qeo`#OS{-ilRZ+9`nljqxsy1
zp;Lu#*--$l?6qncfhI%m^w(3lOt}ywL5?%+_Ov|T=-O)O#|1&>=}51a%Sb~KTR2_K
z!};{n;NgPO;;v%0;n-j>b-Y|l)x=^&d84lKmr8o*+q*$Sul50u>9%n+e!b~90-}xc
znpRXgsh*hBzGXpmnXaxdFnD1FEnbiC?537`DY#mL7&iHNEY4|+!A|s9dFssoYIy_z
z+imR1K+cnVPeX&M1X~ed#U~gsS6HR0zgm1NR~u@{BN;*5Gvl)42%Kq{=4gSyFIAOo
zw)ZGKn^3RZn+iXfb*zL1mnJJGsvTLnDB5DF8)!;+KX&@(mJ7k5LnTlXYxI(#)c`4{
zo6I4Djv|uTRI{JJ9glvUHq0WkzV7H91OVbY6u%#c1Z-!^cIjhIC)Ek7Hx7cRvtc6M
zYLV(#kP^D1#2+7pDzLBFanZFqRw>On{`4qC48A)&{zk{n0CZEKY$SfN1Rk^!<Y7={
zr;oNZLX@eBH?kV94!jp`VvM&*K|C)S)PyDr3Bo>_V}?Oa05R<~;U7Vou+rQ<HPr+F
z*K>Zcj7^Zr@2q2}K8g2gzsQ|Y$Hp^5`riTL^4T#Q?}_!b9ge@36zaVNe`|(D|D@%b
z?q#ETmMPVDW6=SC<m)9rDk(5mwj6q3^Eea_o$!OKw&`DVY@b=Nxe|GLn{BKWdL^q0
zEv&oexn88w!2Vr4<}@aHe%F0E1vvNjK{AN}9|RZy3IGWO0#}x8o&p;T0162Zggd{O
zAHj!W+cgLPh~tbmIf~7W0Pp(R@|s#7+6(b=8d~xviu0_;ZHm*=&$#t!i~1-ABu2>^
zp><TaN)yhY<k9BStabD05X8rvWHtIVzE2KJUj67X$ri(Q1Y(<Ky80*I1o&*_%dh8{
zm5v$I<81nn7r@An+cnL&q3Hu>(H_BkTP!*5u$7;(xt$0Z3AJ%*#wE`2MxJYbiqwMV
z55Sy@$Oto*a)E^@IG(B#1R_APzVCxJvjDnj!`b?U+KFs4f-?w1(lA6SB!RPKJ5Wx?
zlJL}niiAd-Z9pXtcm~T5R%GGR_+_Sq>RpdC-c)(Py<e|%QJzDt`c}jvpa4=nb&r(5
H0+01{(=@zm

literal 0
HcmV?d00001

diff --git a/pkg/provenance/testdata/msgblock.yaml b/pkg/provenance/testdata/msgblock.yaml
new file mode 100644
index 000000000..0fdbda8ce
--- /dev/null
+++ b/pkg/provenance/testdata/msgblock.yaml
@@ -0,0 +1,7 @@
+description: Test chart versioning
+name: hashtest
+version: 1.2.3
+
+...
+files:
+  hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75
diff --git a/pkg/provenance/testdata/msgblock.yaml.asc b/pkg/provenance/testdata/msgblock.yaml.asc
new file mode 100644
index 000000000..5a34d6c52
--- /dev/null
+++ b/pkg/provenance/testdata/msgblock.yaml.asc
@@ -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-----
diff --git a/pkg/provenance/testdata/msgblock.yaml.tampered b/pkg/provenance/testdata/msgblock.yaml.tampered
new file mode 100644
index 000000000..f15811bb2
--- /dev/null
+++ b/pkg/provenance/testdata/msgblock.yaml.tampered
@@ -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-----
diff --git a/pkg/provenance/testdata/regen-hashtest.sh b/pkg/provenance/testdata/regen-hashtest.sh
new file mode 100755
index 000000000..4381fd0b1
--- /dev/null
+++ b/pkg/provenance/testdata/regen-hashtest.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+helm package hashtest
+shasum -a 256 hashtest-1.2.3.tgz > hashtest.sha256
-- 
GitLab