Merge pull request #16 from tobiaskohlbau/addHashSignature
Add hash and signature validation
This commit is contained in:
commit
a77177617a
39
README.md
39
README.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBLzCB1qADAgECAgglJIDaK1tbjjAKBggqhkjOPQQDAjAPMQ0wCwYDVQQDEwRU
|
||||
ZXN0MCAXDTE4MTEwNjE1MTcwMFoYDzIxMTgxMTA2MTUxNzAwWjAPMQ0wCwYDVQQD
|
||||
EwRUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs8fjo/Mi5A3c2v2YxV6A
|
||||
QPJnr70qYMEpsmqn0BTcI8RhZUgB46tWqeDYdO15yQKbZjfI/dr0fvS21jyW0GSX
|
||||
rKMaMBgwCwYDVR0PBAQDAgXgMAkGA1UdEwQCMAAwCgYIKoZIzj0EAwIDSAAwRQIh
|
||||
AI1pUr0nrw3m++sR8HEBoejM5Qh1QJA7gF9y1jY6rc/aAiALFPJXckSLAQuq5IvQ
|
||||
7cugOPws7/OoUo1124LKPugISg==
|
||||
-----END CERTIFICATE-----
|
|
@ -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-----
|
|
@ -0,0 +1 @@
|
|||
e412095724426c984940efde02ea000251a12b37506c977341e0a07600dbfcb6 foo.zip
|
Binary file not shown.
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue