github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+incompatible/pkg/vfile/vfile.go (about)

     1  // Copyright 2020 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package vfile verifies files against a hash or signature.
     6  //
     7  // vfile aims to be TOCTTOU-safe by reading files into memory before verifying.
     8  package vfile
     9  
    10  import (
    11  	"bytes"
    12  	"crypto/sha256"
    13  	"crypto/sha512"
    14  	"errors"
    15  	"fmt"
    16  	"hash"
    17  	"io"
    18  	"io/ioutil"
    19  	"os"
    20  
    21  	"golang.org/x/crypto/openpgp"
    22  )
    23  
    24  // ErrUnsigned is returned for a file that failed signature verification.
    25  type ErrUnsigned struct {
    26  	// Path is the file that failed signature verification.
    27  	Path string
    28  
    29  	// Err is a nested error, if there was one.
    30  	Err error
    31  }
    32  
    33  func (e ErrUnsigned) Error() string {
    34  	if e.Err != nil {
    35  		return fmt.Sprintf("file %q is unsigned: %v", e.Path, e.Err)
    36  	}
    37  	return fmt.Sprintf("file %q is unsigned", e.Path)
    38  }
    39  
    40  func (e ErrUnsigned) Unwrap() error {
    41  	return e.Err
    42  }
    43  
    44  // ErrNoKeyRing is returned when a nil keyring was given.
    45  var ErrNoKeyRing = errors.New("no keyring given")
    46  
    47  // ErrWrongSigner represents a file signed by some key, but not the ones in the given key ring.
    48  type ErrWrongSigner struct {
    49  	// KeyRing is the expected key ring.
    50  	KeyRing openpgp.KeyRing
    51  }
    52  
    53  func (e ErrWrongSigner) Error() string {
    54  	return fmt.Sprintf("signed by a key not present in keyring %s", e.KeyRing)
    55  }
    56  
    57  // GetKeyRing returns an OpenPGP KeyRing loaded from the specified path.
    58  //
    59  // keyPath must be an already trusted path, e.g. keys are included in the initramfs.
    60  func GetKeyRing(keyPath string) (openpgp.KeyRing, error) {
    61  	key, err := os.Open(keyPath)
    62  	if err != nil {
    63  		return nil, fmt.Errorf("could not open pub key: %v", err)
    64  	}
    65  	defer key.Close()
    66  
    67  	ring, err := openpgp.ReadKeyRing(key)
    68  	if err != nil {
    69  		return nil, fmt.Errorf("could not read pub key: %v", err)
    70  	}
    71  	return ring, nil
    72  }
    73  
    74  // OpenSignedSigFile calls OpenSignedFile expecting the signature to be in path.sig.
    75  //
    76  // E.g. if path is /foo/bar, the signature is expected to be in /foo/bar.sig.
    77  func OpenSignedSigFile(keyring openpgp.KeyRing, path string) (*File, error) {
    78  	return OpenSignedFile(keyring, path, fmt.Sprintf("%s.sig", path))
    79  }
    80  
    81  // File encapsulates a bytes.Reader with the file contents and its name.
    82  type File struct {
    83  	*bytes.Reader
    84  
    85  	FileName string
    86  }
    87  
    88  // Name returns the file name.
    89  func (f *File) Name() string {
    90  	return f.FileName
    91  }
    92  
    93  // OpenSignedFile opens a file that is expected to be signed.
    94  //
    95  // WARNING! Unlike many Go functions, this may return both the file and an
    96  // error.
    97  //
    98  // It expects path.sig to be available.
    99  //
   100  // If the signature does not exist or does not match the keyring, both the file
   101  // and a signature error will be returned.
   102  func OpenSignedFile(keyring openpgp.KeyRing, path, pathSig string) (*File, error) {
   103  	content, err := ioutil.ReadFile(path)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	f := &File{
   108  		Reader:   bytes.NewReader(content),
   109  		FileName: path,
   110  	}
   111  
   112  	signaturef, err := os.Open(pathSig)
   113  	if err != nil {
   114  		return f, ErrUnsigned{Path: path, Err: err}
   115  	}
   116  	defer signaturef.Close()
   117  
   118  	if keyring == nil {
   119  		return f, ErrUnsigned{Path: path, Err: ErrNoKeyRing}
   120  	} else if signer, err := openpgp.CheckDetachedSignature(keyring, bytes.NewReader(content), signaturef); err != nil {
   121  		return f, ErrUnsigned{Path: path, Err: err}
   122  	} else if signer == nil {
   123  		return f, ErrUnsigned{Path: path, Err: ErrWrongSigner{keyring}}
   124  	}
   125  	return f, nil
   126  }
   127  
   128  // ErrInvalidHash is returned when hash verification failed.
   129  type ErrInvalidHash struct {
   130  	// Path is the path to the file that was supposed to be verified.
   131  	Path string
   132  
   133  	// Err is some underlying error.
   134  	Err error
   135  }
   136  
   137  func (e ErrInvalidHash) Error() string {
   138  	return fmt.Sprintf("invalid hash for file %q: %v", e.Path, e.Err)
   139  }
   140  
   141  func (e ErrInvalidHash) Unwrap() error {
   142  	return e.Err
   143  }
   144  
   145  // ErrHashMismatch is returned when the file's hash does not match the expected hash.
   146  type ErrHashMismatch struct {
   147  	Want []byte
   148  	Got  []byte
   149  }
   150  
   151  func (e ErrHashMismatch) Error() string {
   152  	return fmt.Sprintf("got hash %#x, expected %#x", e.Got, e.Want)
   153  }
   154  
   155  // ErrNoExpectedHash is given when the caller did not specify a hash.
   156  var ErrNoExpectedHash = errors.New("OpenHashedFile: no expected hash given")
   157  
   158  // OpenHashedFile256 opens path and verifies whether its contents match the
   159  // given sha256 hash.
   160  //
   161  // WARNING! Unlike many Go functions, this may return both the file and an
   162  // error in case the expected hash does not match the contents.
   163  //
   164  // If the contents match, the contents are returned with no error.
   165  func OpenHashedFile256(path string, wantSHA256Hash []byte) (*File, error) {
   166  	return openHashedFile(path, wantSHA256Hash, sha256.New())
   167  }
   168  
   169  // OpenHashedFile512 opens path and verifies whether its contents match the
   170  // given sha512 hash.
   171  //
   172  // WARNING! Unlike many Go functions, this may return both the file and an
   173  // error in case the expected hash does not match the contents.
   174  //
   175  // If the contents match, the contents are returned with no error.
   176  func OpenHashedFile512(path string, wantSHA512Hash []byte) (*File, error) {
   177  	return openHashedFile(path, wantSHA512Hash, sha512.New())
   178  }
   179  
   180  func openHashedFile(path string, wantHash []byte, h hash.Hash) (*File, error) {
   181  	content, err := ioutil.ReadFile(path)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	f := &File{
   186  		Reader:   bytes.NewReader(content),
   187  		FileName: path,
   188  	}
   189  
   190  	if len(wantHash) == 0 {
   191  		return f, ErrInvalidHash{
   192  			Path: path,
   193  			Err:  ErrNoExpectedHash,
   194  		}
   195  	}
   196  
   197  	// Hash the file.
   198  	if _, err := io.Copy(h, bytes.NewReader(content)); err != nil {
   199  		return f, ErrInvalidHash{
   200  			Path: path,
   201  			Err:  err,
   202  		}
   203  	}
   204  
   205  	got := h.Sum(nil)
   206  	if !bytes.Equal(wantHash, got) {
   207  		return f, ErrInvalidHash{
   208  			Path: path,
   209  			Err: ErrHashMismatch{
   210  				Got:  got,
   211  				Want: wantHash,
   212  			},
   213  		}
   214  	}
   215  	return f, nil
   216  }