github.com/qsis/helm@v3.0.0-beta.3+incompatible/pkg/provenance/sign.go (about)

     1  /*
     2  Copyright The Helm Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package provenance
    17  
    18  import (
    19  	"bytes"
    20  	"crypto"
    21  	"encoding/hex"
    22  	"io"
    23  	"io/ioutil"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/pkg/errors"
    29  	"golang.org/x/crypto/openpgp"
    30  	"golang.org/x/crypto/openpgp/clearsign"
    31  	"golang.org/x/crypto/openpgp/packet"
    32  	"sigs.k8s.io/yaml"
    33  
    34  	hapi "helm.sh/helm/pkg/chart"
    35  	"helm.sh/helm/pkg/chart/loader"
    36  )
    37  
    38  var defaultPGPConfig = packet.Config{
    39  	DefaultHash: crypto.SHA512,
    40  }
    41  
    42  // SumCollection represents a collection of file and image checksums.
    43  //
    44  // Files are of the form:
    45  //	FILENAME: "sha256:SUM"
    46  // Images are of the form:
    47  //	"IMAGE:TAG": "sha256:SUM"
    48  // Docker optionally supports sha512, and if this is the case, the hash marker
    49  // will be 'sha512' instead of 'sha256'.
    50  type SumCollection struct {
    51  	Files  map[string]string `json:"files"`
    52  	Images map[string]string `json:"images,omitempty"`
    53  }
    54  
    55  // Verification contains information about a verification operation.
    56  type Verification struct {
    57  	// SignedBy contains the entity that signed a chart.
    58  	SignedBy *openpgp.Entity
    59  	// FileHash is the hash, prepended with the scheme, for the file that was verified.
    60  	FileHash string
    61  	// FileName is the name of the file that FileHash verifies.
    62  	FileName string
    63  }
    64  
    65  // Signatory signs things.
    66  //
    67  // Signatories can be constructed from a PGP private key file using NewFromFiles
    68  // or they can be constructed manually by setting the Entity to a valid
    69  // PGP entity.
    70  //
    71  // The same Signatory can be used to sign or validate multiple charts.
    72  type Signatory struct {
    73  	// The signatory for this instance of Helm. This is used for signing.
    74  	Entity *openpgp.Entity
    75  	// The keyring for this instance of Helm. This is used for verification.
    76  	KeyRing openpgp.EntityList
    77  }
    78  
    79  // NewFromFiles constructs a new Signatory from the PGP key in the given filename.
    80  //
    81  // This will emit an error if it cannot find a valid GPG keyfile (entity) at the
    82  // given location.
    83  //
    84  // Note that the keyfile may have just a public key, just a private key, or
    85  // both. The Signatory methods may have different requirements of the keys. For
    86  // example, ClearSign must have a valid `openpgp.Entity.PrivateKey` before it
    87  // can sign something.
    88  func NewFromFiles(keyfile, keyringfile string) (*Signatory, error) {
    89  	e, err := loadKey(keyfile)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	ring, err := loadKeyRing(keyringfile)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	return &Signatory{
   100  		Entity:  e,
   101  		KeyRing: ring,
   102  	}, nil
   103  }
   104  
   105  // NewFromKeyring reads a keyring file and creates a Signatory.
   106  //
   107  // If id is not the empty string, this will also try to find an Entity in the
   108  // keyring whose name matches, and set that as the signing entity. It will return
   109  // an error if the id is not empty and also not found.
   110  func NewFromKeyring(keyringfile, id string) (*Signatory, error) {
   111  	ring, err := loadKeyRing(keyringfile)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	s := &Signatory{KeyRing: ring}
   117  
   118  	// If the ID is empty, we can return now.
   119  	if id == "" {
   120  		return s, nil
   121  	}
   122  
   123  	// We're gonna go all GnuPG on this and look for a string that _contains_. If
   124  	// two or more keys contain the string and none are a direct match, we error
   125  	// out.
   126  	var candidate *openpgp.Entity
   127  	vague := false
   128  	for _, e := range ring {
   129  		for n := range e.Identities {
   130  			if n == id {
   131  				s.Entity = e
   132  				return s, nil
   133  			}
   134  			if strings.Contains(n, id) {
   135  				if candidate != nil {
   136  					vague = true
   137  				}
   138  				candidate = e
   139  			}
   140  		}
   141  	}
   142  	if vague {
   143  		return s, errors.Errorf("more than one key contain the id %q", id)
   144  	}
   145  
   146  	s.Entity = candidate
   147  	return s, nil
   148  }
   149  
   150  // PassphraseFetcher returns a passphrase for decrypting keys.
   151  //
   152  // This is used as a callback to read a passphrase from some other location. The
   153  // given name is the Name field on the key, typically of the form:
   154  //
   155  //	USER_NAME (COMMENT) <EMAIL>
   156  type PassphraseFetcher func(name string) ([]byte, error)
   157  
   158  // DecryptKey decrypts a private key in the Signatory.
   159  //
   160  // If the key is not encrypted, this will return without error.
   161  //
   162  // If the key does not exist, this will return an error.
   163  //
   164  // If the key exists, but cannot be unlocked with the passphrase returned by
   165  // the PassphraseFetcher, this will return an error.
   166  //
   167  // If the key is successfully unlocked, it will return nil.
   168  func (s *Signatory) DecryptKey(fn PassphraseFetcher) error {
   169  	if s.Entity == nil {
   170  		return errors.New("private key not found")
   171  	} else if s.Entity.PrivateKey == nil {
   172  		return errors.New("provided key is not a private key")
   173  	}
   174  
   175  	// Nothing else to do if key is not encrypted.
   176  	if !s.Entity.PrivateKey.Encrypted {
   177  		return nil
   178  	}
   179  
   180  	fname := "Unknown"
   181  	for i := range s.Entity.Identities {
   182  		if i != "" {
   183  			fname = i
   184  			break
   185  		}
   186  	}
   187  
   188  	p, err := fn(fname)
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	return s.Entity.PrivateKey.Decrypt(p)
   194  }
   195  
   196  // ClearSign signs a chart with the given key.
   197  //
   198  // This takes the path to a chart archive file and a key, and it returns a clear signature.
   199  //
   200  // The Signatory must have a valid Entity.PrivateKey for this to work. If it does
   201  // not, an error will be returned.
   202  func (s *Signatory) ClearSign(chartpath string) (string, error) {
   203  	if s.Entity == nil {
   204  		return "", errors.New("private key not found")
   205  	} else if s.Entity.PrivateKey == nil {
   206  		return "", errors.New("provided key is not a private key")
   207  	}
   208  
   209  	if fi, err := os.Stat(chartpath); err != nil {
   210  		return "", err
   211  	} else if fi.IsDir() {
   212  		return "", errors.New("cannot sign a directory")
   213  	}
   214  
   215  	out := bytes.NewBuffer(nil)
   216  
   217  	b, err := messageBlock(chartpath)
   218  	if err != nil {
   219  		return "", nil
   220  	}
   221  
   222  	// Sign the buffer
   223  	w, err := clearsign.Encode(out, s.Entity.PrivateKey, &defaultPGPConfig)
   224  	if err != nil {
   225  		return "", err
   226  	}
   227  	_, err = io.Copy(w, b)
   228  	w.Close()
   229  	return out.String(), err
   230  }
   231  
   232  // Verify checks a signature and verifies that it is legit for a chart.
   233  func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) {
   234  	ver := &Verification{}
   235  	for _, fname := range []string{chartpath, sigpath} {
   236  		if fi, err := os.Stat(fname); err != nil {
   237  			return ver, err
   238  		} else if fi.IsDir() {
   239  			return ver, errors.Errorf("%s cannot be a directory", fname)
   240  		}
   241  	}
   242  
   243  	// First verify the signature
   244  	sig, err := s.decodeSignature(sigpath)
   245  	if err != nil {
   246  		return ver, errors.Wrap(err, "failed to decode signature")
   247  	}
   248  
   249  	by, err := s.verifySignature(sig)
   250  	if err != nil {
   251  		return ver, err
   252  	}
   253  	ver.SignedBy = by
   254  
   255  	// Second, verify the hash of the tarball.
   256  	sum, err := DigestFile(chartpath)
   257  	if err != nil {
   258  		return ver, err
   259  	}
   260  	_, sums, err := parseMessageBlock(sig.Plaintext)
   261  	if err != nil {
   262  		return ver, err
   263  	}
   264  
   265  	sum = "sha256:" + sum
   266  	basename := filepath.Base(chartpath)
   267  	if sha, ok := sums.Files[basename]; !ok {
   268  		return ver, errors.Errorf("provenance does not contain a SHA for a file named %q", basename)
   269  	} else if sha != sum {
   270  		return ver, errors.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum)
   271  	}
   272  	ver.FileHash = sum
   273  	ver.FileName = basename
   274  
   275  	// TODO: when image signing is added, verify that here.
   276  
   277  	return ver, nil
   278  }
   279  
   280  func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) {
   281  	data, err := ioutil.ReadFile(filename)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  
   286  	block, _ := clearsign.Decode(data)
   287  	if block == nil {
   288  		// There was no sig in the file.
   289  		return nil, errors.New("signature block not found")
   290  	}
   291  
   292  	return block, nil
   293  }
   294  
   295  // verifySignature verifies that the given block is validly signed, and returns the signer.
   296  func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) {
   297  	return openpgp.CheckDetachedSignature(
   298  		s.KeyRing,
   299  		bytes.NewBuffer(block.Bytes),
   300  		block.ArmoredSignature.Body,
   301  	)
   302  }
   303  
   304  func messageBlock(chartpath string) (*bytes.Buffer, error) {
   305  	var b *bytes.Buffer
   306  	// Checksum the archive
   307  	chash, err := DigestFile(chartpath)
   308  	if err != nil {
   309  		return b, err
   310  	}
   311  
   312  	base := filepath.Base(chartpath)
   313  	sums := &SumCollection{
   314  		Files: map[string]string{
   315  			base: "sha256:" + chash,
   316  		},
   317  	}
   318  
   319  	// Load the archive into memory.
   320  	chart, err := loader.LoadFile(chartpath)
   321  	if err != nil {
   322  		return b, err
   323  	}
   324  
   325  	// Buffer a hash + checksums YAML file
   326  	data, err := yaml.Marshal(chart.Metadata)
   327  	if err != nil {
   328  		return b, err
   329  	}
   330  
   331  	// FIXME: YAML uses ---\n as a file start indicator, but this is not legal in a PGP
   332  	// clearsign block. So we use ...\n, which is the YAML document end marker.
   333  	// http://yaml.org/spec/1.2/spec.html#id2800168
   334  	b = bytes.NewBuffer(data)
   335  	b.WriteString("\n...\n")
   336  
   337  	data, err = yaml.Marshal(sums)
   338  	if err != nil {
   339  		return b, err
   340  	}
   341  	b.Write(data)
   342  
   343  	return b, nil
   344  }
   345  
   346  // parseMessageBlock
   347  func parseMessageBlock(data []byte) (*hapi.Metadata, *SumCollection, error) {
   348  	// This sucks.
   349  	parts := bytes.Split(data, []byte("\n...\n"))
   350  	if len(parts) < 2 {
   351  		return nil, nil, errors.New("message block must have at least two parts")
   352  	}
   353  
   354  	md := &hapi.Metadata{}
   355  	sc := &SumCollection{}
   356  
   357  	if err := yaml.Unmarshal(parts[0], md); err != nil {
   358  		return md, sc, err
   359  	}
   360  	err := yaml.Unmarshal(parts[1], sc)
   361  	return md, sc, err
   362  }
   363  
   364  // loadKey loads a GPG key found at a particular path.
   365  func loadKey(keypath string) (*openpgp.Entity, error) {
   366  	f, err := os.Open(keypath)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	defer f.Close()
   371  
   372  	pr := packet.NewReader(f)
   373  	return openpgp.ReadEntity(pr)
   374  }
   375  
   376  func loadKeyRing(ringpath string) (openpgp.EntityList, error) {
   377  	f, err := os.Open(ringpath)
   378  	if err != nil {
   379  		return nil, err
   380  	}
   381  	defer f.Close()
   382  	return openpgp.ReadKeyRing(f)
   383  }
   384  
   385  // DigestFile calculates a SHA256 hash (like Docker) for a given file.
   386  //
   387  // It takes the path to the archive file, and returns a string representation of
   388  // the SHA256 sum.
   389  //
   390  // The intended use of this function is to generate a sum of a chart TGZ file.
   391  func DigestFile(filename string) (string, error) {
   392  	f, err := os.Open(filename)
   393  	if err != nil {
   394  		return "", err
   395  	}
   396  	defer f.Close()
   397  	return Digest(f)
   398  }
   399  
   400  // Digest hashes a reader and returns a SHA256 digest.
   401  //
   402  // Helm uses SHA256 as its default hash for all non-cryptographic applications.
   403  func Digest(in io.Reader) (string, error) {
   404  	hash := crypto.SHA256.New()
   405  	io.Copy(hash, in)
   406  	return hex.EncodeToString(hash.Sum(nil)), nil
   407  }