mirror of https://github.com/helm/helm
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 #983pull/988/head
parent
64b73081ee
commit
ce83a8a777
@ -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,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
|
||||
}
|
@ -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
|
||||
}
|
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…
Reference in new issue