...

Source file src/helm.sh/helm/v3/pkg/provenance/sign.go

Documentation: helm.sh/helm/v3/pkg/provenance

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

View as plain text