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