github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/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/rsa"
    13  	"crypto/sha256"
    14  	"crypto/sha512"
    15  	"crypto/subtle"
    16  	"errors"
    17  	"fmt"
    18  	"hash"
    19  	"io"
    20  	"os"
    21  
    22  	"github.com/ProtonMail/go-crypto/openpgp"
    23  	"github.com/ProtonMail/go-crypto/openpgp/packet"
    24  )
    25  
    26  // ErrUnsigned is returned for a file that failed signature verification.
    27  type ErrUnsigned struct {
    28  	// Path is the file that failed signature verification.
    29  	Path string
    30  
    31  	// Err is a nested error, if there was one.
    32  	Err error
    33  }
    34  
    35  func (e ErrUnsigned) Error() string {
    36  	if e.Err != nil {
    37  		return fmt.Sprintf("file %q is unsigned: %v", e.Path, e.Err)
    38  	}
    39  	return fmt.Sprintf("file %q is unsigned", e.Path)
    40  }
    41  
    42  func (e ErrUnsigned) Unwrap() error {
    43  	return e.Err
    44  }
    45  
    46  // ErrNoKeyRing is returned when a nil keyring was given.
    47  var ErrNoKeyRing = errors.New("no keyring given")
    48  
    49  // ErrWrongSigner represents a file signed by some key, but not the ones in the given key ring.
    50  type ErrWrongSigner struct {
    51  	// KeyRing is the expected key ring.
    52  	KeyRing openpgp.KeyRing
    53  }
    54  
    55  func (e ErrWrongSigner) Error() string {
    56  	return fmt.Sprintf("signed by a key not present in keyring %s", e.KeyRing)
    57  }
    58  
    59  // GetKeyRing returns an OpenPGP KeyRing loaded from the specified path.
    60  //
    61  // keyPath must be an already trusted path, e.g. keys are included in the initramfs.
    62  func GetKeyRing(keyPath string) (openpgp.KeyRing, error) {
    63  	key, err := os.Open(keyPath)
    64  	if err != nil {
    65  		return nil, fmt.Errorf("could not open pub key: %v", err)
    66  	}
    67  	defer key.Close()
    68  
    69  	ring, err := openpgp.ReadKeyRing(key)
    70  	if err != nil {
    71  		return nil, fmt.Errorf("could not read pub key: %v", err)
    72  	}
    73  	return ring, nil
    74  }
    75  
    76  // GetRSAKeysFromRing iterates a PGP Keyring and extracts all rsa.PublicKey.
    77  // An error is returned iff the keyring is not found or no RSA public keys were
    78  // found on it.
    79  func GetRSAKeysFromRing(ring openpgp.KeyRing) ([]*rsa.PublicKey, error) {
    80  	el, ok := ring.(openpgp.EntityList)
    81  	if !ok {
    82  		return nil, fmt.Errorf("failed to assert KeyRing as EntityList to read RSA keys")
    83  	}
    84  
    85  	var rsaKeys []*rsa.PublicKey
    86  	for _, entity := range el {
    87  		// Extract Primary Key
    88  		if entity.PrimaryKey != nil {
    89  			pk := (packet.PublicKey)(*entity.PrimaryKey)
    90  			if rsaKey, ok := pk.PublicKey.(*rsa.PublicKey); ok {
    91  				rsaKeys = append(rsaKeys, rsaKey)
    92  			}
    93  		}
    94  		// Extract any subkeys
    95  		for _, subkey := range entity.Subkeys {
    96  			pk := (packet.PublicKey)(*subkey.PublicKey)
    97  			if rsaKey, ok := pk.PublicKey.(*rsa.PublicKey); ok {
    98  				rsaKeys = append(rsaKeys, rsaKey)
    99  			}
   100  		}
   101  	}
   102  
   103  	if len(rsaKeys) == 0 {
   104  		return nil, fmt.Errorf("no RSA public keys found on keyring")
   105  	}
   106  	return rsaKeys, nil
   107  }
   108  
   109  // OpenSignedSigFile calls OpenSignedFile expecting the signature to be in path.sig.
   110  //
   111  // E.g. if path is /foo/bar, the signature is expected to be in /foo/bar.sig.
   112  func OpenSignedSigFile(keyring openpgp.KeyRing, path string) (*File, error) {
   113  	return OpenSignedFile(keyring, path, fmt.Sprintf("%s.sig", path))
   114  }
   115  
   116  // File encapsulates a bytes.Reader with the file contents and its name.
   117  type File struct {
   118  	*bytes.Reader
   119  
   120  	FileName string
   121  }
   122  
   123  // Name returns the file name.
   124  func (f *File) Name() string {
   125  	return f.FileName
   126  }
   127  
   128  // OpenSignedFile opens a file that is expected to be signed.
   129  //
   130  // WARNING! Unlike many Go functions, this may return both the file and an
   131  // error.
   132  //
   133  // It expects path.sig to be available.
   134  //
   135  // If the signature does not exist or does not match the keyring, both the file
   136  // and a signature error will be returned.
   137  func OpenSignedFile(keyring openpgp.KeyRing, path, pathSig string) (*File, error) {
   138  	content, err := os.ReadFile(path)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	f := &File{
   143  		Reader:   bytes.NewReader(content),
   144  		FileName: path,
   145  	}
   146  
   147  	signaturef, err := os.Open(pathSig)
   148  	if err != nil {
   149  		return f, ErrUnsigned{Path: path, Err: err}
   150  	}
   151  	defer signaturef.Close()
   152  
   153  	if keyring == nil {
   154  		return f, ErrUnsigned{Path: path, Err: ErrNoKeyRing}
   155  	} else if signer, err := openpgp.CheckDetachedSignature(keyring, bytes.NewReader(content), signaturef, nil); err != nil {
   156  		return f, ErrUnsigned{Path: path, Err: err}
   157  	} else if signer == nil {
   158  		return f, ErrUnsigned{Path: path, Err: ErrWrongSigner{keyring}}
   159  	}
   160  	return f, nil
   161  }
   162  
   163  // ErrInvalidHash is returned when hash verification failed.
   164  type ErrInvalidHash struct {
   165  	// Path is the path to the file that was supposed to be verified.
   166  	Path string
   167  
   168  	// Err is some underlying error.
   169  	Err error
   170  }
   171  
   172  func (e ErrInvalidHash) Error() string {
   173  	return fmt.Sprintf("invalid hash for file %q: %v", e.Path, e.Err)
   174  }
   175  
   176  func (e ErrInvalidHash) Unwrap() error {
   177  	return e.Err
   178  }
   179  
   180  // ErrHashMismatch is returned when the file's hash does not match the expected hash.
   181  type ErrHashMismatch struct {
   182  	Want []byte
   183  	Got  []byte
   184  }
   185  
   186  func (e ErrHashMismatch) Error() string {
   187  	return fmt.Sprintf("got hash %#x, expected %#x", e.Got, e.Want)
   188  }
   189  
   190  // ErrNoExpectedHash is given when the caller did not specify a hash.
   191  var ErrNoExpectedHash = errors.New("OpenHashedFile: no expected hash given")
   192  
   193  // OpenHashedFile256 opens path and verifies whether its contents match the
   194  // given sha256 hash.
   195  //
   196  // WARNING! Unlike many Go functions, this may return both the file and an
   197  // error in case the expected hash does not match the contents.
   198  //
   199  // If the contents match, the contents are returned with no error.
   200  func OpenHashedFile256(path string, wantSHA256Hash []byte) (*File, error) {
   201  	return openHashedFile(path, wantSHA256Hash, sha256.New())
   202  }
   203  
   204  // OpenHashedFile512 opens path and verifies whether its contents match the
   205  // given sha512 hash.
   206  //
   207  // WARNING! Unlike many Go functions, this may return both the file and an
   208  // error in case the expected hash does not match the contents.
   209  //
   210  // If the contents match, the contents are returned with no error.
   211  func OpenHashedFile512(path string, wantSHA512Hash []byte) (*File, error) {
   212  	return openHashedFile(path, wantSHA512Hash, sha512.New())
   213  }
   214  
   215  func openHashedFile(path string, wantHash []byte, h hash.Hash) (*File, error) {
   216  	content, err := os.ReadFile(path)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  	f := &File{
   221  		Reader:   bytes.NewReader(content),
   222  		FileName: path,
   223  	}
   224  
   225  	if len(wantHash) == 0 {
   226  		return f, ErrInvalidHash{
   227  			Path: path,
   228  			Err:  ErrNoExpectedHash,
   229  		}
   230  	}
   231  
   232  	// Hash the file.
   233  	if _, err := io.Copy(h, bytes.NewReader(content)); err != nil {
   234  		return f, ErrInvalidHash{
   235  			Path: path,
   236  			Err:  err,
   237  		}
   238  	}
   239  
   240  	got := h.Sum(nil)
   241  	if !bytes.Equal(wantHash, got) {
   242  		return f, ErrInvalidHash{
   243  			Path: path,
   244  			Err: ErrHashMismatch{
   245  				Got:  got,
   246  				Want: wantHash,
   247  			},
   248  		}
   249  	}
   250  	return f, nil
   251  }
   252  
   253  // CheckHashedContent verifies a calculated hash against an expected hash array.
   254  //
   255  // WARNING! Unlike many Go functions, this may return both the file and an
   256  // error in case the expected hash does not match the contents.
   257  //
   258  // If the contents match, the contents are returned with no error.
   259  func CheckHashedContent(b *bytes.Reader, wantHash []byte, h hash.Hash) (*bytes.Reader, error) {
   260  	if len(wantHash) == 0 {
   261  		return b, ErrInvalidHash{
   262  			Err: ErrNoExpectedHash,
   263  		}
   264  	}
   265  
   266  	got, err := CalculateHash(b, h)
   267  	if err != nil {
   268  		return b, err
   269  	}
   270  
   271  	if subtle.ConstantTimeCompare(wantHash, got) == 0 {
   272  		return b, ErrInvalidHash{
   273  			Err: ErrHashMismatch{
   274  				Got:  got,
   275  				Want: wantHash,
   276  			},
   277  		}
   278  	}
   279  	return b, nil
   280  }
   281  
   282  // CalculateHash computes the hash of the input data b given a hash function.
   283  func CalculateHash(b *bytes.Reader, h hash.Hash) ([]byte, error) {
   284  	// Hash the file.
   285  	if _, err := io.Copy(h, b); err != nil {
   286  		return nil, ErrInvalidHash{
   287  			Err: err,
   288  		}
   289  	}
   290  
   291  	return h.Sum(nil), nil
   292  }