github.com/creativeprojects/go-selfupdate@v1.2.0/validate.go (about) 1 package selfupdate 2 3 import ( 4 "bytes" 5 "crypto/ecdsa" 6 "crypto/sha256" 7 "crypto/x509" 8 "encoding/asn1" 9 "encoding/hex" 10 "encoding/pem" 11 "errors" 12 "fmt" 13 "io" 14 "math/big" 15 "path" 16 17 "golang.org/x/crypto/openpgp" 18 ) 19 20 // Validator represents an interface which enables additional validation of releases. 21 type Validator interface { 22 // Validate validates release bytes against an additional asset bytes. 23 // See SHAValidator or ECDSAValidator for more information. 24 Validate(filename string, release, asset []byte) error 25 // GetValidationAssetName returns the additional asset name containing the validation checksum. 26 // The asset containing the checksum can be based on the release asset name 27 // Please note if the validation file cannot be found, the DetectLatest and DetectVersion methods 28 // will fail with a wrapped ErrValidationAssetNotFound error 29 GetValidationAssetName(releaseFilename string) string 30 } 31 32 // RecursiveValidator may be implemented by validators that can continue validation on 33 // validation assets (multistep validation). 34 type RecursiveValidator interface { 35 // MustContinueValidation returns true if validation must continue on the provided filename 36 MustContinueValidation(filename string) bool 37 } 38 39 //===================================================================================================================== 40 41 // PatternValidator specifies a validator for additional file validation 42 // that redirects to other validators depending on glob file patterns. 43 // 44 // Unlike others, PatternValidator is a recursive validator that also checks 45 // validation assets (e.g. SHA256SUMS file checks assets and SHA256SUMS.asc 46 // checks the SHA256SUMS file). 47 // Depending on the used validators, a validation loop might be created, 48 // causing validation errors. In order to prevent this, use SkipValidation 49 // for validation assets that should not be checked (e.g. signature files). 50 // Note that glob pattern are matched in the order of addition. Add general 51 // patterns like "*" at last. 52 // 53 // Usage Example (validate assets by SHA256SUMS and SHA256SUMS.asc): 54 // 55 // new(PatternValidator). 56 // // "SHA256SUMS" file is checked by PGP signature (from "SHA256SUMS.asc") 57 // Add("SHA256SUMS", new(PGPValidator).WithArmoredKeyRing(key)). 58 // // "SHA256SUMS.asc" file is not checked (is the signature for "SHA256SUMS") 59 // SkipValidation("*.asc"). 60 // // All other files are checked by the "SHA256SUMS" file 61 // Add("*", &ChecksumValidator{UniqueFilename:"SHA256SUMS"}) 62 type PatternValidator struct { 63 validators []struct { 64 pattern string 65 validator Validator 66 } 67 } 68 69 // Add maps a new validator to the given glob pattern. 70 func (m *PatternValidator) Add(glob string, validator Validator) *PatternValidator { 71 m.validators = append(m.validators, struct { 72 pattern string 73 validator Validator 74 }{glob, validator}) 75 76 if _, err := path.Match(glob, ""); err != nil { 77 panic(fmt.Errorf("failed adding %q: %w", glob, err)) 78 } 79 return m 80 } 81 82 // SkipValidation skips validation for the given glob pattern. 83 func (m *PatternValidator) SkipValidation(glob string) *PatternValidator { 84 _ = m.Add(glob, nil) 85 // move skip rule to the beginning of the list to ensure it is matched 86 // before the validation rules 87 if size := len(m.validators); size > 0 { 88 m.validators = append(m.validators[size-1:], m.validators[0:size-1]...) 89 } 90 return m 91 } 92 93 func (m *PatternValidator) findValidator(filename string) (Validator, error) { 94 for _, item := range m.validators { 95 if match, err := path.Match(item.pattern, filename); match { 96 return item.validator, nil 97 } else if err != nil { 98 return nil, err 99 } 100 } 101 return nil, ErrValidatorNotFound 102 } 103 104 // Validate delegates to the first matching Validator that was configured with Add. 105 // It fails with ErrValidatorNotFound if no matching validator is configured. 106 func (m *PatternValidator) Validate(filename string, release, asset []byte) error { 107 if validator, err := m.findValidator(filename); err == nil { 108 if validator == nil { 109 return nil // OK, this file does not need to be validated 110 } 111 return validator.Validate(filename, release, asset) 112 } else { 113 return err 114 } 115 } 116 117 // GetValidationAssetName returns the asset name for validation. 118 func (m *PatternValidator) GetValidationAssetName(releaseFilename string) string { 119 if validator, err := m.findValidator(releaseFilename); err == nil { 120 if validator == nil { 121 return releaseFilename // Return a file that we know will exist 122 } 123 return validator.GetValidationAssetName(releaseFilename) 124 } else { 125 return releaseFilename // do not produce an error here to ensure err will be logged. 126 } 127 } 128 129 // MustContinueValidation returns true if validation must continue on the specified filename 130 func (m *PatternValidator) MustContinueValidation(filename string) bool { 131 if validator, err := m.findValidator(filename); err == nil && validator != nil { 132 if rv, ok := validator.(RecursiveValidator); ok && rv != m { 133 return rv.MustContinueValidation(filename) 134 } 135 return true 136 } 137 return false 138 } 139 140 //===================================================================================================================== 141 142 // SHAValidator specifies a SHA256 validator for additional file validation 143 // before updating. 144 type SHAValidator struct { 145 } 146 147 // Validate checks the SHA256 sum of the release against the contents of an 148 // additional asset file. 149 func (v *SHAValidator) Validate(filename string, release, asset []byte) error { 150 // we'd better check the size of the file otherwise it's going to panic 151 if len(asset) < sha256.BlockSize { 152 return ErrIncorrectChecksumFile 153 } 154 155 hash := fmt.Sprintf("%s", asset[:sha256.BlockSize]) 156 calculatedHash := fmt.Sprintf("%x", sha256.Sum256(release)) 157 158 if equal, err := hexStringEquals(sha256.Size, calculatedHash, hash); !equal { 159 if err == nil { 160 return fmt.Errorf("expected %q, found %q: %w", hash, calculatedHash, ErrChecksumValidationFailed) 161 } else { 162 return fmt.Errorf("%s: %w", err.Error(), ErrChecksumValidationFailed) 163 } 164 } 165 return nil 166 } 167 168 // GetValidationAssetName returns the asset name for SHA256 validation. 169 func (v *SHAValidator) GetValidationAssetName(releaseFilename string) string { 170 return releaseFilename + ".sha256" 171 } 172 173 //===================================================================================================================== 174 175 // ChecksumValidator is a SHA256 checksum validator where all the validation hash are in a single file (one per line) 176 type ChecksumValidator struct { 177 // UniqueFilename is the name of the global file containing all the checksums 178 // Usually "checksums.txt", "SHA256SUMS", etc. 179 UniqueFilename string 180 } 181 182 // Validate the SHA256 sum of the release against the contents of an 183 // additional asset file containing all the checksums (one file per line). 184 func (v *ChecksumValidator) Validate(filename string, release, asset []byte) error { 185 hash, err := findChecksum(filename, asset) 186 if err != nil { 187 return err 188 } 189 return new(SHAValidator).Validate(filename, release, []byte(hash)) 190 } 191 192 func findChecksum(filename string, content []byte) (string, error) { 193 // check if the file has windows line ending (probably better than just testing the platform) 194 crlf := []byte("\r\n") 195 lf := []byte("\n") 196 eol := lf 197 if bytes.Contains(content, crlf) { 198 log.Print("Checksum file is using windows line ending") 199 eol = crlf 200 } 201 lines := bytes.Split(content, eol) 202 log.Printf("Checksum validator: %d checksums available, searching for %q", len(lines), filename) 203 for _, line := range lines { 204 // skip empty line 205 if len(line) == 0 { 206 continue 207 } 208 parts := bytes.Split(line, []byte(" ")) 209 if len(parts) != 2 { 210 return "", ErrIncorrectChecksumFile 211 } 212 if string(parts[1]) == filename { 213 return string(parts[0]), nil 214 } 215 } 216 return "", ErrHashNotFound 217 } 218 219 // GetValidationAssetName returns the unique asset name for SHA256 validation. 220 func (v *ChecksumValidator) GetValidationAssetName(releaseFilename string) string { 221 return v.UniqueFilename 222 } 223 224 //===================================================================================================================== 225 226 // ECDSAValidator specifies a ECDSA validator for additional file validation 227 // before updating. 228 type ECDSAValidator struct { 229 PublicKey *ecdsa.PublicKey 230 } 231 232 // WithPublicKey is a convenience method to set PublicKey from a PEM encoded 233 // ECDSA certificate 234 func (v *ECDSAValidator) WithPublicKey(pemData []byte) *ECDSAValidator { 235 block, _ := pem.Decode(pemData) 236 if block == nil || block.Type != "CERTIFICATE" { 237 panic(fmt.Errorf("failed to decode PEM block")) 238 } 239 240 cert, err := x509.ParseCertificate(block.Bytes) 241 if err == nil { 242 var ok bool 243 if v.PublicKey, ok = cert.PublicKey.(*ecdsa.PublicKey); !ok { 244 err = fmt.Errorf("not an ECDSA public key") 245 } 246 } 247 if err != nil { 248 panic(fmt.Errorf("failed to parse certificate in PEM block: %w", err)) 249 } 250 251 return v 252 } 253 254 // Validate checks the ECDSA signature of the release against the signature 255 // contained in an additional asset file. 256 func (v *ECDSAValidator) Validate(filename string, input, signature []byte) error { 257 h := sha256.New() 258 h.Write(input) 259 260 log.Printf("Verifying ECDSA signature on %q", filename) 261 var rs struct { 262 R *big.Int 263 S *big.Int 264 } 265 if _, err := asn1.Unmarshal(signature, &rs); err != nil { 266 return ErrInvalidECDSASignature 267 } 268 269 if v.PublicKey == nil || !ecdsa.Verify(v.PublicKey, h.Sum([]byte{}), rs.R, rs.S) { 270 return ErrECDSAValidationFailed 271 } 272 273 return nil 274 } 275 276 // GetValidationAssetName returns the asset name for ECDSA validation. 277 func (v *ECDSAValidator) GetValidationAssetName(releaseFilename string) string { 278 return releaseFilename + ".sig" 279 } 280 281 //===================================================================================================================== 282 283 // PGPValidator specifies a PGP validator for additional file validation 284 // before updating. 285 type PGPValidator struct { 286 // KeyRing is usually filled by openpgp.ReadArmoredKeyRing(bytes.NewReader(key)) with key being the PGP pub key. 287 KeyRing openpgp.EntityList 288 // Binary toggles whether to validate detached *.sig (binary) or *.asc (ascii) signature files 289 Binary bool 290 } 291 292 // WithArmoredKeyRing is a convenience method to set KeyRing 293 func (g *PGPValidator) WithArmoredKeyRing(key []byte) *PGPValidator { 294 if ring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(key)); err == nil { 295 g.KeyRing = ring 296 } else { 297 panic(fmt.Errorf("failed setting armored public key ring: %w", err)) 298 } 299 return g 300 } 301 302 // Validate checks the PGP signature of the release against the signature 303 // contained in an additional asset file. 304 func (g *PGPValidator) Validate(filename string, release, signature []byte) (err error) { 305 if g.KeyRing == nil { 306 return ErrPGPKeyRingNotSet 307 } 308 log.Printf("Verifying PGP signature on %q", filename) 309 310 data, sig := bytes.NewReader(release), bytes.NewReader(signature) 311 if g.Binary { 312 _, err = openpgp.CheckDetachedSignature(g.KeyRing, data, sig) 313 } else { 314 _, err = openpgp.CheckArmoredDetachedSignature(g.KeyRing, data, sig) 315 } 316 317 if errors.Is(err, io.EOF) { 318 err = ErrInvalidPGPSignature 319 } 320 321 return err 322 } 323 324 // GetValidationAssetName returns the asset name for PGP validation. 325 func (g *PGPValidator) GetValidationAssetName(releaseFilename string) string { 326 if g.Binary { 327 return releaseFilename + ".sig" 328 } 329 return releaseFilename + ".asc" 330 } 331 332 //===================================================================================================================== 333 334 func hexStringEquals(size int, a, b string) (equal bool, err error) { 335 size *= 2 336 if len(a) == size && len(b) == size { 337 var bytesA, bytesB []byte 338 if bytesA, err = hex.DecodeString(a); err == nil { 339 if bytesB, err = hex.DecodeString(b); err == nil { 340 equal = bytes.Equal(bytesA, bytesB) 341 } 342 } 343 } 344 return 345 } 346 347 // NewChecksumWithECDSAValidator returns a validator that checks assets with a checksums file 348 // (e.g. SHA256SUMS) and the checksums file with an ECDSA signature (e.g. SHA256SUMS.sig). 349 func NewChecksumWithECDSAValidator(checksumsFilename string, pemECDSACertificate []byte) Validator { 350 return new(PatternValidator). 351 Add(checksumsFilename, new(ECDSAValidator).WithPublicKey(pemECDSACertificate)). 352 Add("*", &ChecksumValidator{UniqueFilename: checksumsFilename}). 353 SkipValidation("*.sig") 354 } 355 356 // NewChecksumWithPGPValidator returns a validator that checks assets with a checksums file 357 // (e.g. SHA256SUMS) and the checksums file with an armored PGP signature (e.g. SHA256SUMS.asc). 358 func NewChecksumWithPGPValidator(checksumsFilename string, armoredPGPKeyRing []byte) Validator { 359 return new(PatternValidator). 360 Add(checksumsFilename, new(PGPValidator).WithArmoredKeyRing(armoredPGPKeyRing)). 361 Add("*", &ChecksumValidator{UniqueFilename: checksumsFilename}). 362 SkipValidation("*.asc") 363 } 364 365 //===================================================================================================================== 366 367 // Verify interface 368 var ( 369 _ Validator = &SHAValidator{} 370 _ Validator = &ChecksumValidator{} 371 _ Validator = &ECDSAValidator{} 372 _ Validator = &PGPValidator{} 373 _ Validator = &PatternValidator{} 374 _ RecursiveValidator = &PatternValidator{} 375 )