diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc0ebe3..f9db0d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,9 +48,9 @@ jobs: shell: bash run: | if [[ "${GITHUB_TOKEN}" != "" ]]; then - go test -v -race -coverprofile=coverage.txt . ./update + go test -v -race -coverprofile=coverage.txt . ./update ./pgp else - go test -v -race -short -coverprofile=coverage.txt . ./update + go test -v -race -short -coverprofile=coverage.txt . ./update ./pgp fi - name: Code coverage with codecov diff --git a/Makefile b/Makefile index 07cf355..9d84111 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ ifeq ($(GOBIN),) GOBIN := $(GOPATH)/bin endif -TESTS=. ./update +TESTS=. ./update ./pgp COVERAGE_FILE=coverage.txt BUILD_DATE=`date` diff --git a/README.md b/README.md index 54284dc..7ec2f50 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Self-Update library for Github, Gitea and Gitlab hosted applications in Go * [Hash or Signature Validation](#hash-or-signature-validation) * [SHA256](#sha256) * [ECDSA](#ecdsa) + * [PGP](#pgp) * [Using a single checksum file for all your assets](#using-a-single-checksum-file-for-all-your-assets) * [macOS universal binaries](#macos-universal-binaries) * [Other providers than Github](#other-providers-than-github) @@ -261,7 +262,7 @@ More information on targeting ARM cpu can be found here: [GoArm](https://github. # Hash or Signature Validation go-selfupdate supports hash or signature validation of the downloaded files. It comes -with support for sha256 hashes or ECDSA signatures. If you need something different, +with support for sha256 hashes, ECDSA or PGP signatures. If you need something different, you can implement the `Validator` interface with your own validation mechanism: ```go @@ -298,6 +299,25 @@ openssl dgst -sha256 -sign Test.pem -out foo.zip.sig foo.zip go-selfupdate makes use of go internal crypto package. Therefore the private key has to be compatible with FIPS 186-3. +## PGP + +To verify the signature by PGP generate a detached signature and save it within a file which has the +same naming as original file with the suffix `.asc` (armored) or `.sig` (binary). +For e.g. use gpg, the file `foo.zip.asc` is generated with: +```shell +gpg --armor --detach-sign foo.zip +``` + +The PGP validator lives in the `pgp` sub-package, so applications not using it don't need to import the PGP libraries: + +```go +import "github.com/creativeprojects/go-selfupdate/pgp" + +updater, _ := selfupdate.NewUpdater(selfupdate.Config{ + Validator: new(pgp.Validator).WithArmoredKeyRing(publicKey), +}) +``` + ## Using a single checksum file for all your assets Tools like [goreleaser][] produce a single checksum file for all your assets. A Validator is provided out of the box for this case: @@ -306,6 +326,16 @@ Tools like [goreleaser][] produce a single checksum file for all your assets. A updater, _ := NewUpdater(Config{Validator: &ChecksumValidator{UniqueFilename: "checksums.txt"}}) ``` +If the checksum file is itself signed (`checksums.txt.sig` for ECDSA, `checksums.txt.asc` for PGP), +use a validator that checks the assets against the checksum file, and the checksum file against its signature: + +```go +// ECDSA +updater, _ := NewUpdater(Config{Validator: NewChecksumWithECDSAValidator("checksums.txt", pemCertificate)}) +// PGP +updater, _ := NewUpdater(Config{Validator: pgp.NewChecksumValidator("checksums.txt", armoredPublicKey)}) +``` + # macOS universal binaries You can ask the updater to choose a macOS universal binary as a fallback if the native architecture wasn't found. diff --git a/detect_test.go b/detect_test.go index 846835a..1f79758 100644 --- a/detect_test.go +++ b/detect_test.go @@ -919,13 +919,13 @@ func TestFindReleaseAndAsset(t *testing.T) { func TestBuildMultistepValidationChain(t *testing.T) { testVersion := "v0.14.0" - source, keyRing := mockPGPSourceRepository(t) + source := mockSignedSourceRepository(t) checksumValidator := &ChecksumValidator{UniqueFilename: "checksums.txt"} t.Run("ValidConfig", func(t *testing.T) { updater, _ := NewUpdater(Config{ Source: source, - Validator: NewChecksumWithPGPValidator("checksums.txt", keyRing), + Validator: newChecksumWithSignatureValidator("checksums.txt"), }) release, found, err := updater.DetectVersion(context.Background(), testGithubRepository, testVersion) diff --git a/errors.go b/errors.go index 0c3aafd..e0f43e2 100644 --- a/errors.go +++ b/errors.go @@ -18,8 +18,6 @@ var ( ErrHashNotFound = errors.New("hash not found in checksum file") ErrECDSAValidationFailed = errors.New("ECDSA signature verification failed") ErrInvalidECDSASignature = errors.New("invalid ECDSA signature") - ErrInvalidPGPSignature = errors.New("invalid PGP signature") - ErrPGPKeyRingNotSet = errors.New("PGP key ring not set") ErrCannotDecompressFile = errors.New("failed to decompress") ErrExecutableNotFoundInArchive = errors.New("executable not found") ) diff --git a/mockdata_test.go b/mockdata_test.go index 536826a..703743c 100644 --- a/mockdata_test.go +++ b/mockdata_test.go @@ -9,7 +9,6 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/ProtonMail/go-crypto/openpgp" ) // mockSourceRepository creates a new *MockSource pre-populated with different versions and assets @@ -279,15 +278,10 @@ func mockSourceRepository(t *testing.T) *MockSource { return NewMockSource(releases, files) } -// mockPGPSourceRepository creates a variant of mockSourceRepository where "checksums.txt" is signed with PGP -func mockPGPSourceRepository(t *testing.T) (source *MockSource, PGPKeyRing []byte) { - - source = mockSourceRepository(t) - - var err error - - var entity *openpgp.Entity - PGPKeyRing, entity = getTestPGPKeyRing(t) +// mockSignedSourceRepository creates a variant of mockSourceRepository where "checksums.txt" is "signed": +// "checksums.txt.asc" holds the SHA256 of "checksums.txt" and is checked by signatureValidator. +func mockSignedSourceRepository(t *testing.T) *MockSource { + source := mockSourceRepository(t) for i, release := range source.releases { rel := release.(*GitHubRelease) @@ -296,19 +290,30 @@ func mockPGPSourceRepository(t *testing.T) (source *MockSource, PGPKeyRing []byt signatureID := id + 1 shaSums := source.files[id] - // Create SHA256SUMS.asc (by signing SHA256SUMS) - signature := &bytes.Buffer{} - err = openpgp.ArmoredDetachSign(signature, entity, bytes.NewReader(shaSums), nil) - require.NoError(t, err) - rel.assets = append(rel.assets, &GitHubAsset{ id: signatureID, name: "checksums.txt.asc", }) - source.files[signatureID] = signature.Bytes() - - t.Logf("file id %d contains PGP signature:\n%s\n", signatureID, string(source.files[signatureID])) + source.files[signatureID] = []byte(fmt.Sprintf("%x", sha256.Sum256(shaSums))) } - return + return source +} + +// signatureValidator stands in for a signature validator (such as pgp.Validator) in multistep +// validation tests: it checks a SHA256 hash stored in a ".asc" file. +type signatureValidator struct { + SHAValidator +} + +func (v *signatureValidator) GetValidationAssetName(releaseFilename string) string { + return releaseFilename + ".asc" +} + +// newChecksumWithSignatureValidator mirrors pgp.NewChecksumValidator using signatureValidator +func newChecksumWithSignatureValidator(checksumsFilename string) Validator { + return new(PatternValidator). + Add(checksumsFilename, new(signatureValidator)). + Add("*", &ChecksumValidator{UniqueFilename: checksumsFilename}). + SkipValidation("*.asc") } diff --git a/pgp/updater_test.go b/pgp/updater_test.go new file mode 100644 index 0000000..e5bc439 --- /dev/null +++ b/pgp/updater_test.go @@ -0,0 +1,112 @@ +package pgp + +import ( + "bytes" + "context" + "crypto/sha256" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/creativeprojects/go-selfupdate" +) + +// memorySource is an in-memory selfupdate.Source +type memorySource struct { + releases []selfupdate.SourceRelease + files map[int64][]byte +} + +func (s *memorySource) ListReleases(_ context.Context, _ selfupdate.Repository) ([]selfupdate.SourceRelease, error) { + return s.releases, nil +} + +func (s *memorySource) DownloadReleaseAsset(_ context.Context, _ *selfupdate.Release, assetID int64) (io.ReadCloser, error) { + content, ok := s.files[assetID] + if !ok { + return nil, selfupdate.ErrAssetNotFound + } + return io.NopCloser(bytes.NewReader(content)), nil +} + +func TestUpdateWithSignedChecksums(t *testing.T) { + const ( + archiveID = 1 + checksumsID = 2 + signatureID = 3 + assetName = "new_version_1.0.0_linux_amd64.tar.gz" + ) + keyRing, entity := getTestKeyRing(t) + + archive, err := os.ReadFile("../testdata/new_version.tar.gz") + require.NoError(t, err) + + checksums := fmt.Sprintf("%x %s\n", sha256.Sum256(archive), assetName) + signature := &bytes.Buffer{} + require.NoError(t, openpgp.ArmoredDetachSign(signature, entity, strings.NewReader(checksums), nil)) + + source := &memorySource{ + releases: []selfupdate.SourceRelease{ + &selfupdate.HttpRelease{ + ID: 1, + Name: "v1.0.0", + TagName: "v1.0.0", + PublishedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + Assets: []*selfupdate.HttpAsset{ + {ID: archiveID, Name: assetName, Size: len(archive)}, + {ID: checksumsID, Name: "checksums.txt", Size: len(checksums)}, + {ID: signatureID, Name: "checksums.txt.asc", Size: signature.Len()}, + }, + }, + }, + files: map[int64][]byte{ + archiveID: archive, + checksumsID: []byte(checksums), + signatureID: signature.Bytes(), + }, + } + + updater, err := selfupdate.NewUpdater(selfupdate.Config{ + Source: source, + Validator: NewChecksumValidator("checksums.txt", keyRing), + OS: "linux", + Arch: "amd64", + }) + require.NoError(t, err) + + release, found, err := updater.DetectLatest(context.Background(), selfupdate.NewRepositorySlug("owner", "new_version")) + require.NoError(t, err) + require.True(t, found) + require.Len(t, release.ValidationChain, 2) + assert.Equal(t, "checksums.txt", release.ValidationChain[0].ValidationAssetName) + assert.Equal(t, "checksums.txt.asc", release.ValidationChain[1].ValidationAssetName) + + // the executable name must match the file inside the archive + target := filepath.Join(t.TempDir(), "new_version") + require.NoError(t, os.WriteFile(target, nil, 0o600)) + + t.Run("ValidSignature", func(t *testing.T) { + assert.NoError(t, updater.UpdateTo(context.Background(), release, target)) + }) + + t.Run("InvalidSignature", func(t *testing.T) { + source.files[signatureID] = archive + err := updater.UpdateTo(context.Background(), release, target) + assert.ErrorIs(t, err, ErrInvalidSignature) + }) + + t.Run("TamperedChecksums", func(t *testing.T) { + source.files[signatureID] = signature.Bytes() + source.files[checksumsID] = []byte(checksums + "deadbeef tampered_file\n") + err := updater.UpdateTo(context.Background(), release, target) + assert.ErrorContains(t, err, "openpgp: invalid signature") + }) +} diff --git a/pgp/validator.go b/pgp/validator.go new file mode 100644 index 0000000..3be61e0 --- /dev/null +++ b/pgp/validator.go @@ -0,0 +1,83 @@ +// Package pgp provides a PGP signature validator for go-selfupdate. +// +// It is kept in a separate package so that applications not using PGP +// signatures do not pull in the OpenPGP library. +package pgp + +import ( + "bytes" + "errors" + "fmt" + "io" + + "github.com/ProtonMail/go-crypto/openpgp" + + "github.com/creativeprojects/go-selfupdate" +) + +// Possible errors returned +var ( + ErrInvalidSignature = errors.New("invalid PGP signature") + ErrKeyRingNotSet = errors.New("PGP key ring not set") +) + +// Validator specifies a PGP validator for additional file validation +// before updating. +type Validator struct { + // KeyRing is usually filled by openpgp.ReadArmoredKeyRing(bytes.NewReader(key)) with key being the PGP pub key. + KeyRing openpgp.EntityList + // Binary toggles whether to validate detached *.sig (binary) or *.asc (ascii) signature files + Binary bool +} + +// WithArmoredKeyRing is a convenience method to set KeyRing +func (g *Validator) WithArmoredKeyRing(key []byte) *Validator { + if ring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(key)); err == nil { + g.KeyRing = ring + } else { + panic(fmt.Errorf("failed setting armored public key ring: %w", err)) + } + return g +} + +// Validate checks the PGP signature of the release against the signature +// contained in an additional asset file. +func (g *Validator) Validate(filename string, release, signature []byte) (err error) { + if g.KeyRing == nil { + return ErrKeyRingNotSet + } + + data, sig := bytes.NewReader(release), bytes.NewReader(signature) + // A nil *packet.Config selects the library defaults. + if g.Binary { + _, err = openpgp.CheckDetachedSignature(g.KeyRing, data, sig, nil) + } else { + _, err = openpgp.CheckArmoredDetachedSignature(g.KeyRing, data, sig, nil) + } + + if errors.Is(err, io.EOF) { + err = ErrInvalidSignature + } + + return err +} + +// GetValidationAssetName returns the asset name for PGP validation. +func (g *Validator) GetValidationAssetName(releaseFilename string) string { + if g.Binary { + return releaseFilename + ".sig" + } + return releaseFilename + ".asc" +} + +// NewChecksumValidator returns a validator that checks assets with a checksums file +// (e.g. SHA256SUMS) and the checksums file with an armored PGP signature (e.g. SHA256SUMS.asc). +func NewChecksumValidator(checksumsFilename string, armoredKeyRing []byte) selfupdate.Validator { + return new(selfupdate.PatternValidator). + Add(checksumsFilename, new(Validator).WithArmoredKeyRing(armoredKeyRing)). + Add("*", &selfupdate.ChecksumValidator{UniqueFilename: checksumsFilename}). + SkipValidation("*.asc") +} + +// Verify interface +var _ selfupdate.Validator = &Validator{} diff --git a/pgp/validator_test.go b/pgp/validator_test.go new file mode 100644 index 0000000..7eff522 --- /dev/null +++ b/pgp/validator_test.go @@ -0,0 +1,131 @@ +package pgp + +import ( + "bytes" + "io" + "os" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/creativeprojects/go-selfupdate" +) + +func TestValidatorAssetNames(t *testing.T) { + filename := "asset" + for _, test := range []struct { + validator selfupdate.Validator + validationName string + }{ + { + validator: &Validator{}, + validationName: filename + ".asc", + }, + { + validator: &Validator{Binary: true}, + validationName: filename + ".sig", + }, + } { + assert.Equal(t, test.validationName, test.validator.GetValidationAssetName(filename)) + } +} + +func TestValidator(t *testing.T) { + data, err := os.ReadFile("../testdata/foo.zip") + require.NoError(t, err) + + otherData, err := os.ReadFile("../testdata/foo.tar.xz") + require.NoError(t, err) + + keyRing, entity := getTestKeyRing(t) + require.NotNil(t, keyRing) + require.NotNil(t, entity) + + var signatureData []byte + { + signature := &bytes.Buffer{} + err = openpgp.ArmoredDetachSign(signature, entity, bytes.NewReader(data), nil) + require.NoError(t, err) + signatureData = signature.Bytes() + } + + t.Run("NoPublicKey", func(t *testing.T) { + validator := new(Validator) + err = validator.Validate("foo.zip", data, signatureData) + assert.ErrorIs(t, err, ErrKeyRingNotSet) + err = validator.Validate("foo.zip", data, nil) + assert.ErrorIs(t, err, ErrKeyRingNotSet) + err = validator.Validate("foo.zip", data, []byte{}) + assert.ErrorIs(t, err, ErrKeyRingNotSet) + }) + + t.Run("EmptySignature", func(t *testing.T) { + validator := new(Validator).WithArmoredKeyRing(keyRing) + err = validator.Validate("foo.zip", data, nil) + assert.ErrorIs(t, err, ErrInvalidSignature) + err = validator.Validate("foo.zip", data, []byte{}) + assert.ErrorIs(t, err, ErrInvalidSignature) + }) + + t.Run("InvalidSignature", func(t *testing.T) { + validator := new(Validator).WithArmoredKeyRing(keyRing) + err = validator.Validate("foo.zip", data, []byte{0, 1, 2}) + assert.ErrorIs(t, err, ErrInvalidSignature) + err = validator.Validate("foo.zip", data, data) + assert.ErrorIs(t, err, ErrInvalidSignature) + }) + + t.Run("ValidSignature", func(t *testing.T) { + validator := new(Validator).WithArmoredKeyRing(keyRing) + err = validator.Validate("foo.zip", data, signatureData) + assert.NoError(t, err) + }) + + t.Run("Fail", func(t *testing.T) { + validator := new(Validator).WithArmoredKeyRing(keyRing) + err = validator.Validate("foo.tar.xz", otherData, signatureData) + // Assert the error category rather than the exact wording: the reason + // reported after "invalid signature" is an implementation detail of the + // openpgp library and is not part of this package's contract. + assert.ErrorContains(t, err, "openpgp: invalid signature") + }) +} + +func TestValidatorWithArmoredKeyRing(t *testing.T) { + keyRing, entity := getTestKeyRing(t) + validator := new(Validator).WithArmoredKeyRing(keyRing) + assert.Equal(t, entity.PrimaryKey.KeyIdString(), validator.KeyRing[0].PrimaryKey.KeyIdString()) + + assert.PanicsWithError(t, "failed setting armored public key ring: openpgp: invalid argument: no armored data found", func() { + new(Validator).WithArmoredKeyRing([]byte{}) + }) +} + +func TestNewChecksumValidator(t *testing.T) { + keyRing, _ := getTestKeyRing(t) + + validator := NewChecksumValidator("checksums", keyRing) + assert.Implements(t, (*selfupdate.RecursiveValidator)(nil), validator) + assert.Equal(t, "checksums", validator.GetValidationAssetName("anything")) + assert.Equal(t, "checksums.asc", validator.GetValidationAssetName("checksums")) +} + +func getTestKeyRing(t *testing.T) (keyRing []byte, entity *openpgp.Entity) { + var err error + var armoredWriter io.WriteCloser + entity, err = openpgp.NewEntity("go-selfupdate", "", "info@go-selfupdate.local", nil) + require.NoError(t, err) + + buffer := &bytes.Buffer{} + if armoredWriter, err = armor.Encode(buffer, openpgp.PublicKeyType, nil); err == nil { + if err = entity.Serialize(armoredWriter); err == nil { + err = armoredWriter.Close() + } + } + require.NoError(t, err) + keyRing = buffer.Bytes() + return +} diff --git a/update_test.go b/update_test.go index ccaca70..b5ce8ad 100644 --- a/update_test.go +++ b/update_test.go @@ -417,10 +417,10 @@ func TestUpdateToSuccess(t *testing.T) { func TestUpdateToWithMultistepValidationChain(t *testing.T) { testVersion := "v0.14.0" - source, keyRing := mockPGPSourceRepository(t) + source := mockSignedSourceRepository(t) updater, _ := NewUpdater(Config{ Source: source, - Validator: NewChecksumWithPGPValidator("checksums.txt", keyRing), + Validator: newChecksumWithSignatureValidator("checksums.txt"), }) tempFile := createEmptyFile(t, "new_version") @@ -453,7 +453,8 @@ func TestUpdateToWithMultistepValidationChain(t *testing.T) { release.ValidationChain[1].ValidationAssetID = 1 err := updater.UpdateTo(context.Background(), release, tempFile) - assert.EqualError(t, err, "failed validating asset content \"checksums.txt\": invalid PGP signature") + assert.ErrorContains(t, err, "failed validating asset content \"checksums.txt\"") + assert.ErrorIs(t, err, ErrChecksumValidationFailed) }) } diff --git a/validate.go b/validate.go index d435b2d..0385ae6 100644 --- a/validate.go +++ b/validate.go @@ -8,13 +8,9 @@ import ( "encoding/asn1" "encoding/hex" "encoding/pem" - "errors" "fmt" - "io" "math/big" "path" - - "github.com/ProtonMail/go-crypto/openpgp" ) // Validator represents an interface which enables additional validation of releases. @@ -54,7 +50,8 @@ type RecursiveValidator interface { // // new(PatternValidator). // // "SHA256SUMS" file is checked by PGP signature (from "SHA256SUMS.asc") -// Add("SHA256SUMS", new(PGPValidator).WithArmoredKeyRing(key)). +// // using the validator from the pgp subpackage +// Add("SHA256SUMS", new(pgp.Validator).WithArmoredKeyRing(key)). // // "SHA256SUMS.asc" file is not checked (is the signature for "SHA256SUMS") // SkipValidation("*.asc"). // // All other files are checked by the "SHA256SUMS" file @@ -280,59 +277,6 @@ func (v *ECDSAValidator) GetValidationAssetName(releaseFilename string) string { //===================================================================================================================== -// PGPValidator specifies a PGP validator for additional file validation -// before updating. -type PGPValidator struct { - // KeyRing is usually filled by openpgp.ReadArmoredKeyRing(bytes.NewReader(key)) with key being the PGP pub key. - KeyRing openpgp.EntityList - // Binary toggles whether to validate detached *.sig (binary) or *.asc (ascii) signature files - Binary bool -} - -// WithArmoredKeyRing is a convenience method to set KeyRing -func (g *PGPValidator) WithArmoredKeyRing(key []byte) *PGPValidator { - if ring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(key)); err == nil { - g.KeyRing = ring - } else { - panic(fmt.Errorf("failed setting armored public key ring: %w", err)) - } - return g -} - -// Validate checks the PGP signature of the release against the signature -// contained in an additional asset file. -func (g *PGPValidator) Validate(filename string, release, signature []byte) (err error) { - if g.KeyRing == nil { - return ErrPGPKeyRingNotSet - } - log.Printf("Verifying PGP signature on %q", filename) - - data, sig := bytes.NewReader(release), bytes.NewReader(signature) - // A nil *packet.Config selects the library defaults, matching the behaviour - // of the golang.org/x/crypto/openpgp functions this replaces. - if g.Binary { - _, err = openpgp.CheckDetachedSignature(g.KeyRing, data, sig, nil) - } else { - _, err = openpgp.CheckArmoredDetachedSignature(g.KeyRing, data, sig, nil) - } - - if errors.Is(err, io.EOF) { - err = ErrInvalidPGPSignature - } - - return err -} - -// GetValidationAssetName returns the asset name for PGP validation. -func (g *PGPValidator) GetValidationAssetName(releaseFilename string) string { - if g.Binary { - return releaseFilename + ".sig" - } - return releaseFilename + ".asc" -} - -//===================================================================================================================== - func hexStringEquals(size int, a, b string) (equal bool, err error) { size *= 2 if len(a) == size && len(b) == size { @@ -355,15 +299,6 @@ func NewChecksumWithECDSAValidator(checksumsFilename string, pemECDSACertificate SkipValidation("*.sig") } -// NewChecksumWithPGPValidator returns a validator that checks assets with a checksums file -// (e.g. SHA256SUMS) and the checksums file with an armored PGP signature (e.g. SHA256SUMS.asc). -func NewChecksumWithPGPValidator(checksumsFilename string, armoredPGPKeyRing []byte) Validator { - return new(PatternValidator). - Add(checksumsFilename, new(PGPValidator).WithArmoredKeyRing(armoredPGPKeyRing)). - Add("*", &ChecksumValidator{UniqueFilename: checksumsFilename}). - SkipValidation("*.asc") -} - //===================================================================================================================== // Verify interface @@ -371,7 +306,6 @@ var ( _ Validator = &SHAValidator{} _ Validator = &ChecksumValidator{} _ Validator = &ECDSAValidator{} - _ Validator = &PGPValidator{} _ Validator = &PatternValidator{} _ RecursiveValidator = &PatternValidator{} ) diff --git a/validate_test.go b/validate_test.go index 9c8dc60..2bf178e 100644 --- a/validate_test.go +++ b/validate_test.go @@ -1,20 +1,16 @@ package selfupdate import ( - "bytes" "crypto/ecdsa" "crypto/x509" "encoding/hex" "encoding/pem" "fmt" - "io" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" ) func TestValidatorAssetNames(t *testing.T) { @@ -31,14 +27,6 @@ func TestValidatorAssetNames(t *testing.T) { validator: &ECDSAValidator{}, validationName: filename + ".sig", }, - { - validator: &PGPValidator{}, - validationName: filename + ".asc", - }, - { - validator: &PGPValidator{Binary: true}, - validationName: filename + ".sig", - }, { validator: &ChecksumValidator{"funny_sha256"}, validationName: "funny_sha256", @@ -250,96 +238,6 @@ func getTestPublicKey(t *testing.T) *ecdsa.PublicKey { return pubKey } -// ======= PGPValidator ====================================================== - -func TestPGPValidator(t *testing.T) { - data, err := os.ReadFile("testdata/foo.zip") - require.NoError(t, err) - - otherData, err := os.ReadFile("testdata/foo.tar.xz") - require.NoError(t, err) - - keyRing, entity := getTestPGPKeyRing(t) - require.NotNil(t, keyRing) - require.NotNil(t, entity) - - var signatureData []byte - { - signature := &bytes.Buffer{} - err = openpgp.ArmoredDetachSign(signature, entity, bytes.NewReader(data), nil) - require.NoError(t, err) - signatureData = signature.Bytes() - } - - t.Run("NoPublicKey", func(t *testing.T) { - validator := new(PGPValidator) - err = validator.Validate("foo.zip", data, signatureData) - assert.ErrorIs(t, err, ErrPGPKeyRingNotSet) - err = validator.Validate("foo.zip", data, nil) - assert.ErrorIs(t, err, ErrPGPKeyRingNotSet) - err = validator.Validate("foo.zip", data, []byte{}) - assert.ErrorIs(t, err, ErrPGPKeyRingNotSet) - }) - - t.Run("EmptySignature", func(t *testing.T) { - validator := new(PGPValidator).WithArmoredKeyRing(keyRing) - err = validator.Validate("foo.zip", data, nil) - assert.ErrorIs(t, err, ErrInvalidPGPSignature) - err = validator.Validate("foo.zip", data, []byte{}) - assert.ErrorIs(t, err, ErrInvalidPGPSignature) - }) - - t.Run("InvalidSignature", func(t *testing.T) { - validator := new(PGPValidator).WithArmoredKeyRing(keyRing) - err = validator.Validate("foo.zip", data, []byte{0, 1, 2}) - assert.ErrorIs(t, err, ErrInvalidPGPSignature) - err = validator.Validate("foo.zip", data, data) - assert.ErrorIs(t, err, ErrInvalidPGPSignature) - }) - - t.Run("ValidSignature", func(t *testing.T) { - validator := new(PGPValidator).WithArmoredKeyRing(keyRing) - err = validator.Validate("foo.zip", data, signatureData) - assert.NoError(t, err) - }) - - t.Run("Fail", func(t *testing.T) { - validator := new(PGPValidator).WithArmoredKeyRing(keyRing) - err = validator.Validate("foo.tar.xz", otherData, signatureData) - // Assert the error category rather than the exact wording: the reason - // reported after "invalid signature" is an implementation detail of the - // openpgp library and is not part of this package's contract. - assert.ErrorContains(t, err, "openpgp: invalid signature") - }) -} - -func TestPGPValidatorWithArmoredKeyRing(t *testing.T) { - keyRing, entity := getTestPGPKeyRing(t) - validator := new(PGPValidator).WithArmoredKeyRing(keyRing) - assert.Equal(t, entity.PrimaryKey.KeyIdString(), validator.KeyRing[0].PrimaryKey.KeyIdString()) - - assert.PanicsWithError(t, "failed setting armored public key ring: openpgp: invalid argument: no armored data found", func() { - new(PGPValidator).WithArmoredKeyRing([]byte{}) - }) -} - -func getTestPGPKeyRing(t *testing.T) (PGPKeyRing []byte, entity *openpgp.Entity) { - var err error - var armoredWriter io.WriteCloser - entity, err = openpgp.NewEntity("go-selfupdate", "", "info@go-selfupdate.local", nil) - require.NoError(t, err) - - buffer := &bytes.Buffer{} - if armoredWriter, err = armor.Encode(buffer, openpgp.PublicKeyType, nil); err == nil { - if err = entity.Serialize(armoredWriter); err == nil { - err = armoredWriter.Close() - } - } - require.NoError(t, err) - PGPKeyRing = buffer.Bytes() - return -} - // ======= ChecksumValidator ==================================================== func TestChecksumValidatorEmptyFile(t *testing.T) { @@ -449,12 +347,3 @@ func TestNewChecksumWithECDSAValidator(t *testing.T) { assert.Equal(t, "checksums", validator.GetValidationAssetName("anything")) assert.Equal(t, "checksums.sig", validator.GetValidationAssetName("checksums")) } - -func TestNewChecksumWithPGPValidator(t *testing.T) { - keyRing, _ := getTestPGPKeyRing(t) - - validator := NewChecksumWithPGPValidator("checksums", keyRing) - assert.Implements(t, (*RecursiveValidator)(nil), validator) - assert.Equal(t, "checksums", validator.GetValidationAssetName("anything")) - assert.Equal(t, "checksums.asc", validator.GetValidationAssetName("checksums")) -}