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 }