Merge pull request #16 from tobiaskohlbau/addHashSignature

Add hash and signature validation
This commit is contained in:
Linda_pp 2018-11-09 20:00:26 +09:00 committed by GitHub
commit a77177617a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 317 additions and 7 deletions

View File

@ -29,6 +29,7 @@ GitHub and replaces itself.
- Many archive and compression formats are supported (zip, tar, gzip, xzip)
- Support private repositories
- Support [GitHub Enterprise][]
- Support hash, signature validation
And small wrapper CLIs are provided:
@ -298,6 +299,44 @@ In summary, structure of releases on GitHub looks like:
Tags which don't contain a version number are ignored (i.e. `nightly`). And releases marked
as `pre-release` are also ignored.
### Hash or Signature Validation
go-github-selfupdate supports hash or signature validatiom of the downloaded files. It comes
with support for sha256 hashes or ECDSA signatures. In addition to internal functions the
user can implement the `Validator` interface for own validation mechanisms.
```go
// Validator represents an interface which enables additional validation of releases.
type Validator interface {
// Validate validates release bytes against an additional asset bytes.
// See SHA2Validator or ECDSAValidator for more information.
Validate(release, asset []byte) error
// Suffix describes the additional file ending which is used for finding the
// additional asset.
Suffix() string
}
```
## SHA256
To verify the integrity by SHA256 generate a hash sum and save it within a file which has the
same naming as original file with the suffix `.sha256`.
For e.g. use sha256sum, the file `selfupdate/testdata/foo.zip.sha256` is generated with:
```shell
sha256sum foo.zip > foo.zip.sha256
```
## ECDSA
To verify the signature by ECDSA generate a signature and save it within a file which has the
same naming as original file with the suffix `.sig`.
For e.g. use openssl, the file `selfupdate/testdata/foo.zip.sig` is generated with:
```shell
openssl dgst -sha256 -sign Test.pem -out foo.zip.sig foo.zip
```
go-github-selfupdate makes use of go internal crypto package. Therefore the used private key
has to be compatbile with FIPS 186-3.
### Development
#### Running tests

View File

@ -59,6 +59,15 @@ func findAssetFromReleasse(rel *github.RepositoryRelease, suffixes []string, tar
return nil, semver.Version{}, false
}
func findValidationAsset(rel *github.RepositoryRelease, validationName string) (*github.ReleaseAsset, bool) {
for _, asset := range rel.Assets {
if asset.GetName() == validationName {
return &asset, true
}
}
return nil, false
}
func findReleaseAndAsset(rels []*github.RepositoryRelease, targetVersion string) (*github.RepositoryRelease, *github.ReleaseAsset, semver.Version, bool) {
// Generate candidates
suffixes := make([]string, 0, 2*7*2)
@ -143,6 +152,7 @@ func (up *Updater) DetectVersion(slug string, version string) (release *Release,
url,
asset.GetSize(),
asset.GetID(),
-1,
rel.GetHTMLURL(),
rel.GetBody(),
rel.GetName(),
@ -150,6 +160,16 @@ func (up *Updater) DetectVersion(slug string, version string) (release *Release,
repo[0],
repo[1],
}
if up.validator != nil {
validationName := asset.GetName()+up.validator.Suffix()
validationAsset, ok := findValidationAsset(rel, validationName)
if !ok {
return nil, false, fmt.Errorf("Failed finding validation file %q", validationName)
}
release.ValidationAssetID = validationAsset.GetID()
}
return release, true, nil
}

View File

@ -16,6 +16,8 @@ type Release struct {
AssetByteSize int
// AssetID is the ID of the asset on GitHub
AssetID int64
// ValidationAssetID is the ID of additional validaton asset on GitHub
ValidationAssetID int64
// URL is a URL to release page for browsing
URL string
// ReleaseNotes is a release notes of the release

9
selfupdate/testdata/Test.crt vendored Normal file
View File

@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE-----
MIIBLzCB1qADAgECAgglJIDaK1tbjjAKBggqhkjOPQQDAjAPMQ0wCwYDVQQDEwRU
ZXN0MCAXDTE4MTEwNjE1MTcwMFoYDzIxMTgxMTA2MTUxNzAwWjAPMQ0wCwYDVQQD
EwRUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs8fjo/Mi5A3c2v2YxV6A
QPJnr70qYMEpsmqn0BTcI8RhZUgB46tWqeDYdO15yQKbZjfI/dr0fvS21jyW0GSX
rKMaMBgwCwYDVR0PBAQDAgXgMAkGA1UdEwQCMAAwCgYIKoZIzj0EAwIDSAAwRQIh
AI1pUr0nrw3m++sR8HEBoejM5Qh1QJA7gF9y1jY6rc/aAiALFPJXckSLAQuq5IvQ
7cugOPws7/OoUo1124LKPugISg==
-----END CERTIFICATE-----

14
selfupdate/testdata/Test.pem vendored Normal file
View File

@ -0,0 +1,14 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIJvTkRedVrQDNjCb9/RfVjzRwz8S059Y1J6w2N8gy8jVoAoGCCqGSM49
AwEHoUQDQgAEs8fjo/Mi5A3c2v2YxV6AQPJnr70qYMEpsmqn0BTcI8RhZUgB46tW
qeDYdO15yQKbZjfI/dr0fvS21jyW0GSXrA==
-----END EC PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIBLzCB1qADAgECAgglJIDaK1tbjjAKBggqhkjOPQQDAjAPMQ0wCwYDVQQDEwRU
ZXN0MCAXDTE4MTEwNjE1MTcwMFoYDzIxMTgxMTA2MTUxNzAwWjAPMQ0wCwYDVQQD
EwRUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs8fjo/Mi5A3c2v2YxV6A
QPJnr70qYMEpsmqn0BTcI8RhZUgB46tWqeDYdO15yQKbZjfI/dr0fvS21jyW0GSX
rKMaMBgwCwYDVR0PBAQDAgXgMAkGA1UdEwQCMAAwCgYIKoZIzj0EAwIDSAAwRQIh
AI1pUr0nrw3m++sR8HEBoejM5Qh1QJA7gF9y1jY6rc/aAiALFPJXckSLAQuq5IvQ
7cugOPws7/OoUo1124LKPugISg==
-----END CERTIFICATE-----

1
selfupdate/testdata/foo.zip.sha256 vendored Normal file
View File

@ -0,0 +1 @@
e412095724426c984940efde02ea000251a12b37506c977341e0a07600dbfcb6 foo.zip

BIN
selfupdate/testdata/foo.zip.sig vendored Normal file

Binary file not shown.

View File

@ -1,8 +1,10 @@
package selfupdate
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
@ -13,9 +15,7 @@ import (
"github.com/inconshreveable/go-update"
)
func uncompressAndUpdate(src io.ReadCloser, assetURL, cmdPath string) error {
defer src.Close()
func uncompressAndUpdate(src io.Reader, assetURL, cmdPath string) error {
_, cmd := filepath.Split(cmdPath)
asset, err := UncompressCommand(src, assetURL, cmd)
if err != nil {
@ -67,7 +67,41 @@ func (up *Updater) UpdateTo(rel *Release, cmdPath string) error {
return err
}
}
return uncompressAndUpdate(src, rel.AssetURL, cmdPath)
defer src.Close()
data, err := ioutil.ReadAll(src)
if err != nil {
return fmt.Errorf("Failed reading asset body: %v", err)
}
if up.validator == nil {
return uncompressAndUpdate(bytes.NewReader(data), rel.AssetURL, cmdPath)
}
validationSrc, validationRedirectURL, err := up.api.Repositories.DownloadReleaseAsset(up.apiCtx, rel.RepoOwner, rel.RepoName, rel.ValidationAssetID)
if err != nil {
return fmt.Errorf("Failed to call GitHub Releases API for getting an validation asset(ID: %d) for repository '%s/%s': %s", rel.ValidationAssetID, rel.RepoOwner, rel.RepoName, err)
}
if validationRedirectURL != "" {
log.Println("Redirect URL was returned while trying to download a release validation asset from GitHub API. Falling back to downloading from asset URL directly:", redirectURL)
validationSrc, err = up.downloadDirectlyFromURL(validationRedirectURL)
if err != nil {
return err
}
}
defer validationSrc.Close()
validationData, err := ioutil.ReadAll(validationSrc)
if err != nil {
return fmt.Errorf("Failed reading validation asset body: %v", err)
}
if err := up.validator.Validate(data, validationData); err != nil {
return fmt.Errorf("Failed validating asset content: %v", err)
}
return uncompressAndUpdate(bytes.NewReader(data), rel.AssetURL, cmdPath)
}
// UpdateCommand updates a given command binary to the latest version.
@ -129,6 +163,7 @@ func UpdateTo(assetURL, cmdPath string) error {
if err != nil {
return err
}
defer src.Close()
return uncompressAndUpdate(src, assetURL, cmdPath)
}

View File

@ -15,6 +15,7 @@ import (
type Updater struct {
api *github.Client
apiCtx context.Context
validator Validator
}
// Config represents the configuration of self-update.
@ -27,6 +28,8 @@ type Config struct {
// EnterpriseUploadURL is a URL to upload stuffs to GitHub Enterprise instance. This is often the same as an API base URL.
// So if this field is not set and EnterpriseBaseURL is set, EnterpriseBaseURL is also set to this field.
EnterpriseUploadURL string
// Validator represents types which enable additional validation of downloaded release.
Validator Validator
}
func newHTTPClient(ctx context.Context, token string) *http.Client {
@ -52,7 +55,7 @@ func NewUpdater(config Config) (*Updater, error) {
if config.EnterpriseBaseURL == "" {
client := github.NewClient(hc)
return &Updater{client, ctx}, nil
return &Updater{client, ctx, config.Validator}, nil
}
u := config.EnterpriseUploadURL
@ -63,7 +66,7 @@ func NewUpdater(config Config) (*Updater, error) {
if err != nil {
return nil, err
}
return &Updater{client, ctx}, nil
return &Updater{api: client, apiCtx: ctx, validator: config.Validator}, nil
}
// DefaultUpdater creates a new updater instance with default configuration.
@ -76,5 +79,5 @@ func DefaultUpdater() *Updater {
}
ctx := context.Background()
client := newHTTPClient(ctx, token)
return &Updater{github.NewClient(client), ctx}
return &Updater{api: github.NewClient(client), apiCtx: ctx}
}

73
selfupdate/validate.go Normal file
View File

@ -0,0 +1,73 @@
package selfupdate
import (
"crypto/ecdsa"
"crypto/sha256"
"encoding/asn1"
"fmt"
"math/big"
)
// Validator represents an interface which enables additional validation of releases.
type Validator interface {
// Validate validates release bytes against an additional asset bytes.
// See SHA2Validator or ECDSAValidator for more information.
Validate(release, asset []byte) error
// Suffix describes the additional file ending which is used for finding the
// additional asset.
Suffix() string
}
// SHA2Validator specifies a SHA256 validator for additional file validation
// before updating.
type SHA2Validator struct {
}
// Validate validates the SHA256 sum of the release against the contents of an
// additional asset file.
func (v *SHA2Validator) Validate(release, asset []byte) error {
calculatedHash := fmt.Sprintf("%x", sha256.Sum256(release))
hash := fmt.Sprintf("%s", asset[:sha256.BlockSize])
if calculatedHash != hash {
return fmt.Errorf("sha2: validation failed: hash mismatch: expected=%q, got=%q", calculatedHash, hash)
}
return nil
}
// Suffix returns the suffix for SHA2 validation.
func (v *SHA2Validator) Suffix() string {
return ".sha256"
}
// ECDSAValidator specifies a ECDSA validator for additional file validation
// before updating.
type ECDSAValidator struct {
PublicKey *ecdsa.PublicKey
}
// Validate validates the ECDSA signature the release against the signature
// contained in an addtional asset file.
// additional asset file.
func (v *ECDSAValidator) Validate(input, signature []byte) error {
h := sha256.New()
h.Write(input)
var rs struct {
R *big.Int
S *big.Int
}
if _, err := asn1.Unmarshal(signature, &rs); err != nil {
return fmt.Errorf("failed to unmarshal ecdsa signature: %v", err)
}
if !ecdsa.Verify(v.PublicKey, h.Sum([]byte{}), rs.R, rs.S) {
return fmt.Errorf("ecdsa: signature verification failed")
}
return nil
}
// Suffix returns the suffix for ECDSA validation.
func (v *ECDSAValidator) Suffix() string {
return ".sig"
}

114
selfupdate/validate_test.go Normal file
View File

@ -0,0 +1,114 @@
package selfupdate
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"testing"
)
func TestSHA2Validator(t *testing.T) {
validator := &SHA2Validator{}
data, err := ioutil.ReadFile("testdata/foo.zip")
if err != nil {
t.Fatal(err)
}
hashData, err := ioutil.ReadFile("testdata/foo.zip.sha256")
if err != nil {
t.Fatal(err)
}
if err := validator.Validate(data, hashData); err != nil {
t.Fatal(err)
}
}
func TestSHA2ValidatorFail(t *testing.T) {
validator := &SHA2Validator{}
data, err := ioutil.ReadFile("testdata/foo.zip")
if err != nil {
t.Fatal(err)
}
hashData, err := ioutil.ReadFile("testdata/foo.zip.sha256")
if err != nil {
t.Fatal(err)
}
hashData[0] = '0'
if err := validator.Validate(data, hashData); err == nil {
t.Fatal(err)
}
}
func TestECDSAValidator(t *testing.T) {
pemData, err := ioutil.ReadFile("testdata/Test.crt")
if err != nil {
t.Fatal(err)
}
block, _ := pem.Decode(pemData)
if block == nil || block.Type != "CERTIFICATE" {
t.Fatalf("failed to decode PEM block")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to parse certificate")
}
pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok {
t.Errorf("PublicKey is not ECDSA")
}
validator := &ECDSAValidator{
PublicKey: pubKey,
}
data, err := ioutil.ReadFile("testdata/foo.zip")
if err != nil {
t.Fatal(err)
}
signatureData, err := ioutil.ReadFile("testdata/foo.zip.sig")
if err != nil {
t.Fatal(err)
}
if err := validator.Validate(data, signatureData); err != nil {
t.Fatal(err)
}
}
func TestECDSAValidatorFail(t *testing.T) {
pemData, err := ioutil.ReadFile("testdata/Test.crt")
if err != nil {
t.Fatal(err)
}
block, _ := pem.Decode(pemData)
if block == nil || block.Type != "CERTIFICATE" {
t.Fatalf("failed to decode PEM block")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to parse certificate")
}
pubKey, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok {
t.Errorf("PublicKey is not ECDSA")
}
validator := &ECDSAValidator{
PublicKey: pubKey,
}
data, err := ioutil.ReadFile("testdata/foo.tar.xz")
if err != nil {
t.Fatal(err)
}
signatureData, err := ioutil.ReadFile("testdata/foo.zip.sig")
if err != nil {
t.Fatal(err)
}
if err := validator.Validate(data, signatureData); err == nil {
t.Fatal(err)
}
}