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  )