github.com/danielqsj/helm@v2.0.0-alpha.4.0.20160908204436-976e0ba5199b+incompatible/pkg/provenance/sign.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors All rights reserved.
     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  }
    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, fmt.Errorf("more than one key contain the id %q", id)
   144  	}
   145  	s.Entity = candidate
   146  	return s, nil
   147  }
   148  
   149  // ClearSign signs a chart with the given key.
   150  //
   151  // This takes the path to a chart archive file and a key, and it returns a clear signature.
   152  //
   153  // The Signatory must have a valid Entity.PrivateKey for this to work. If it does
   154  // not, an error will be returned.
   155  func (s *Signatory) ClearSign(chartpath string) (string, error) {
   156  	if s.Entity.PrivateKey == nil {
   157  		return "", errors.New("private key not found")
   158  	}
   159  
   160  	if fi, err := os.Stat(chartpath); err != nil {
   161  		return "", err
   162  	} else if fi.IsDir() {
   163  		return "", errors.New("cannot sign a directory")
   164  	}
   165  
   166  	out := bytes.NewBuffer(nil)
   167  
   168  	b, err := messageBlock(chartpath)
   169  	if err != nil {
   170  		return "", nil
   171  	}
   172  
   173  	// Sign the buffer
   174  	w, err := clearsign.Encode(out, s.Entity.PrivateKey, &defaultPGPConfig)
   175  	if err != nil {
   176  		return "", err
   177  	}
   178  	_, err = io.Copy(w, b)
   179  	w.Close()
   180  	return out.String(), err
   181  }
   182  
   183  // Verify checks a signature and verifies that it is legit for a chart.
   184  func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) {
   185  	ver := &Verification{}
   186  	for _, fname := range []string{chartpath, sigpath} {
   187  		if fi, err := os.Stat(fname); err != nil {
   188  			return ver, err
   189  		} else if fi.IsDir() {
   190  			return ver, fmt.Errorf("%s cannot be a directory", fname)
   191  		}
   192  	}
   193  
   194  	// First verify the signature
   195  	sig, err := s.decodeSignature(sigpath)
   196  	if err != nil {
   197  		return ver, fmt.Errorf("failed to decode signature: %s", err)
   198  	}
   199  
   200  	by, err := s.verifySignature(sig)
   201  	if err != nil {
   202  		return ver, err
   203  	}
   204  	ver.SignedBy = by
   205  
   206  	// Second, verify the hash of the tarball.
   207  	sum, err := sumArchive(chartpath)
   208  	if err != nil {
   209  		return ver, err
   210  	}
   211  	_, sums, err := parseMessageBlock(sig.Plaintext)
   212  	if err != nil {
   213  		return ver, err
   214  	}
   215  
   216  	sum = "sha256:" + sum
   217  	basename := filepath.Base(chartpath)
   218  	if sha, ok := sums.Files[basename]; !ok {
   219  		return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", basename)
   220  	} else if sha != sum {
   221  		return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum)
   222  	}
   223  	ver.FileHash = sum
   224  
   225  	// TODO: when image signing is added, verify that here.
   226  
   227  	return ver, nil
   228  }
   229  
   230  func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) {
   231  	data, err := ioutil.ReadFile(filename)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	block, _ := clearsign.Decode(data)
   237  	if block == nil {
   238  		// There was no sig in the file.
   239  		return nil, errors.New("signature block not found")
   240  	}
   241  
   242  	return block, nil
   243  }
   244  
   245  // verifySignature verifies that the given block is validly signed, and returns the signer.
   246  func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) {
   247  	return openpgp.CheckDetachedSignature(
   248  		s.KeyRing,
   249  		bytes.NewBuffer(block.Bytes),
   250  		block.ArmoredSignature.Body,
   251  	)
   252  }
   253  
   254  func messageBlock(chartpath string) (*bytes.Buffer, error) {
   255  	var b *bytes.Buffer
   256  	// Checksum the archive
   257  	chash, err := sumArchive(chartpath)
   258  	if err != nil {
   259  		return b, err
   260  	}
   261  
   262  	base := filepath.Base(chartpath)
   263  	sums := &SumCollection{
   264  		Files: map[string]string{
   265  			base: "sha256:" + chash,
   266  		},
   267  	}
   268  
   269  	// Load the archive into memory.
   270  	chart, err := chartutil.LoadFile(chartpath)
   271  	if err != nil {
   272  		return b, err
   273  	}
   274  
   275  	// Buffer a hash + checksums YAML file
   276  	data, err := yaml.Marshal(chart.Metadata)
   277  	if err != nil {
   278  		return b, err
   279  	}
   280  
   281  	// FIXME: YAML uses ---\n as a file start indicator, but this is not legal in a PGP
   282  	// clearsign block. So we use ...\n, which is the YAML document end marker.
   283  	// http://yaml.org/spec/1.2/spec.html#id2800168
   284  	b = bytes.NewBuffer(data)
   285  	b.WriteString("\n...\n")
   286  
   287  	data, err = yaml.Marshal(sums)
   288  	if err != nil {
   289  		return b, err
   290  	}
   291  	b.Write(data)
   292  
   293  	return b, nil
   294  }
   295  
   296  // parseMessageBlock
   297  func parseMessageBlock(data []byte) (*hapi.Metadata, *SumCollection, error) {
   298  	// This sucks.
   299  	parts := bytes.Split(data, []byte("\n...\n"))
   300  	if len(parts) < 2 {
   301  		return nil, nil, errors.New("message block must have at least two parts")
   302  	}
   303  
   304  	md := &hapi.Metadata{}
   305  	sc := &SumCollection{}
   306  
   307  	if err := yaml.Unmarshal(parts[0], md); err != nil {
   308  		return md, sc, err
   309  	}
   310  	err := yaml.Unmarshal(parts[1], sc)
   311  	return md, sc, err
   312  }
   313  
   314  // loadKey loads a GPG key found at a particular path.
   315  func loadKey(keypath string) (*openpgp.Entity, error) {
   316  	f, err := os.Open(keypath)
   317  	if err != nil {
   318  		return nil, err
   319  	}
   320  	defer f.Close()
   321  
   322  	pr := packet.NewReader(f)
   323  	return openpgp.ReadEntity(pr)
   324  }
   325  
   326  func loadKeyRing(ringpath string) (openpgp.EntityList, error) {
   327  	f, err := os.Open(ringpath)
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  	defer f.Close()
   332  	return openpgp.ReadKeyRing(f)
   333  }
   334  
   335  // sumArchive calculates a SHA256 hash (like Docker) for a given file.
   336  //
   337  // It takes the path to the archive file, and returns a string representation of
   338  // the SHA256 sum.
   339  //
   340  // The intended use of this function is to generate a sum of a chart TGZ file.
   341  func sumArchive(filename string) (string, error) {
   342  	f, err := os.Open(filename)
   343  	if err != nil {
   344  		return "", err
   345  	}
   346  	defer f.Close()
   347  
   348  	hash := crypto.SHA256.New()
   349  	io.Copy(hash, f)
   350  	return hex.EncodeToString(hash.Sum(nil)), nil
   351  }