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 }