Integrate verified installation flow (T-302)
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractorExtractVerifiedFile(t *testing.T) {
|
||||
archivePath := writeTestZIP(t, []testZIPEntry{
|
||||
{name: "app.json", body: validAppManifest("1.2.3", "bin/App.exe")},
|
||||
{name: "payload/bin/App.exe", body: []byte("executable"), mode: 0o755},
|
||||
{name: "payload/readme.txt", body: []byte("hello")},
|
||||
})
|
||||
destination := filepath.Join(t.TempDir(), "staging")
|
||||
extractor := mustExtractor(t, testLimits())
|
||||
|
||||
result, err := extractor.ExtractVerifiedFile(
|
||||
archivePath,
|
||||
destination,
|
||||
verifiedExpectation(t, archivePath, "1.2.3", "bin/App.exe"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("ExtractVerifiedFile() error = %v", err)
|
||||
}
|
||||
if result.Files != 2 || len(result.PayloadFiles) != 2 {
|
||||
t.Fatalf("files = %d, payload files = %d, want 2", result.Files, len(result.PayloadFiles))
|
||||
}
|
||||
if result.PayloadFiles[0].Path != "bin/App.exe" || result.PayloadFiles[0].Size != int64(len("executable")) {
|
||||
t.Fatalf("first payload file = %#v", result.PayloadFiles[0])
|
||||
}
|
||||
wantHash := sha256.Sum256([]byte("executable"))
|
||||
if result.PayloadFiles[0].SHA256 != hex.EncodeToString(wantHash[:]) {
|
||||
t.Fatalf("first payload hash = %q", result.PayloadFiles[0].SHA256)
|
||||
}
|
||||
if _, err := os.Stat(result.EntrypointPath); err != nil {
|
||||
t.Fatalf("entrypoint stat error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorExtractVerifiedFileRejectsBeforeStaging(t *testing.T) {
|
||||
archivePath := writeTestZIP(t, []testZIPEntry{
|
||||
{name: "app.json", body: validAppManifest("1.2.3", "App.exe")},
|
||||
{name: "payload/App.exe", body: []byte("executable"), mode: 0o755},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
modify func(PackageExpectation) PackageExpectation
|
||||
wantErr error
|
||||
stage PackageStage
|
||||
}{
|
||||
{
|
||||
name: "hash mismatch",
|
||||
modify: func(expectation PackageExpectation) PackageExpectation {
|
||||
expectation.SHA256 = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
return expectation
|
||||
},
|
||||
wantErr: ErrPackageHashMismatch,
|
||||
stage: PackageStageVerify,
|
||||
},
|
||||
{
|
||||
name: "app manifest identity mismatch",
|
||||
modify: func(expectation PackageExpectation) PackageExpectation {
|
||||
expectation.App.Version = "9.9.9"
|
||||
return expectation
|
||||
},
|
||||
wantErr: ErrPackageIdentityMismatch,
|
||||
stage: PackageStageManifest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
destination := filepath.Join(t.TempDir(), "staging")
|
||||
extractor := mustExtractor(t, testLimits())
|
||||
expectation := test.modify(verifiedExpectation(t, archivePath, "1.2.3", "App.exe"))
|
||||
|
||||
_, err := extractor.ExtractVerifiedFile(archivePath, destination, expectation)
|
||||
if !errors.Is(err, test.wantErr) {
|
||||
t.Fatalf("ExtractVerifiedFile() error = %v, want %v", err, test.wantErr)
|
||||
}
|
||||
var packageErr *PackageError
|
||||
if !errors.As(err, &packageErr) || packageErr.Stage != test.stage {
|
||||
t.Fatalf("package error = %#v, want stage %q", packageErr, test.stage)
|
||||
}
|
||||
if _, statErr := os.Stat(destination); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("rejected package left staging, stat error = %v", statErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorExtractVerifiedFileRejectsStrictAppManifest(t *testing.T) {
|
||||
manifest := validAppManifest("1.2.3", "App.exe")
|
||||
archivePath := writeTestZIP(t, []testZIPEntry{
|
||||
{name: "app.json", body: append(manifest[:len(manifest)-1], []byte(`,"unexpected":true}`)...)},
|
||||
{name: "payload/App.exe", body: []byte("executable"), mode: 0o755},
|
||||
})
|
||||
destination := filepath.Join(t.TempDir(), "staging")
|
||||
extractor := mustExtractor(t, testLimits())
|
||||
|
||||
_, err := extractor.ExtractVerifiedFile(
|
||||
archivePath,
|
||||
destination,
|
||||
verifiedExpectation(t, archivePath, "1.2.3", "App.exe"),
|
||||
)
|
||||
if !errors.Is(err, ErrAppManifestInvalid) {
|
||||
t.Fatalf("ExtractVerifiedFile() error = %v, want %v", err, ErrAppManifestInvalid)
|
||||
}
|
||||
if _, statErr := os.Stat(destination); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("invalid manifest left staging, stat error = %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorExtractVerifiedFileRejectsNonRegularDownload(t *testing.T) {
|
||||
destination := filepath.Join(t.TempDir(), "staging")
|
||||
extractor := mustExtractor(t, testLimits())
|
||||
_, err := extractor.ExtractVerifiedFile(
|
||||
t.TempDir(),
|
||||
destination,
|
||||
PackageExpectation{
|
||||
Size: 1,
|
||||
SHA256: "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
App: AppExpectation{
|
||||
ID: "test-app",
|
||||
Version: "1.2.3",
|
||||
Channel: "stable",
|
||||
MinOS: "windows-10",
|
||||
Architecture: "amd64",
|
||||
Entrypoint: "App.exe",
|
||||
},
|
||||
},
|
||||
)
|
||||
if !errors.Is(err, ErrInvalidArchive) {
|
||||
t.Fatalf("ExtractVerifiedFile() error = %v, want %v", err, ErrInvalidArchive)
|
||||
}
|
||||
if _, statErr := os.Stat(destination); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("non-regular download left staging, stat error = %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractorExtractVerifiedFileBoundsAppManifest(t *testing.T) {
|
||||
manifest := append(validAppManifest("1.2.3", "App.exe"), bytes.Repeat([]byte(" "), MaxAppManifestBytes)...)
|
||||
archivePath := writeTestZIP(t, []testZIPEntry{
|
||||
{name: "app.json", body: manifest},
|
||||
{name: "payload/App.exe", body: []byte("executable"), mode: 0o755},
|
||||
})
|
||||
destination := filepath.Join(t.TempDir(), "staging")
|
||||
extractor := mustExtractor(t, Limits{
|
||||
MaxEntries: 20,
|
||||
MaxArchiveBytes: 2 * MaxAppManifestBytes,
|
||||
MaxCentralDirectoryBytes: 4 * 1024,
|
||||
MaxUncompressedBytes: 2 * MaxAppManifestBytes,
|
||||
MaxCompressionRatio: 100,
|
||||
})
|
||||
|
||||
_, err := extractor.ExtractVerifiedFile(
|
||||
archivePath,
|
||||
destination,
|
||||
verifiedExpectation(t, archivePath, "1.2.3", "App.exe"),
|
||||
)
|
||||
if !errors.Is(err, ErrAppManifestTooLarge) {
|
||||
t.Fatalf("ExtractVerifiedFile() error = %v, want %v", err, ErrAppManifestTooLarge)
|
||||
}
|
||||
var packageErr *PackageError
|
||||
if !errors.As(err, &packageErr) || packageErr.Stage != PackageStageManifest {
|
||||
t.Fatalf("package error = %#v, want manifest stage", packageErr)
|
||||
}
|
||||
if _, statErr := os.Stat(destination); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("oversized manifest left staging, stat error = %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func verifiedExpectation(t *testing.T, archivePath, version, entrypoint string) PackageExpectation {
|
||||
t.Helper()
|
||||
document, err := os.ReadFile(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read archive: %v", err)
|
||||
}
|
||||
hash := sha256.Sum256(document)
|
||||
return PackageExpectation{
|
||||
Size: int64(len(document)),
|
||||
SHA256: hex.EncodeToString(hash[:]),
|
||||
App: AppExpectation{
|
||||
ID: "test-app",
|
||||
Version: version,
|
||||
Channel: "stable",
|
||||
MinOS: "windows-10",
|
||||
Architecture: "amd64",
|
||||
Entrypoint: entrypoint,
|
||||
RequiresAdmin: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func validAppManifest(version, entrypoint string) []byte {
|
||||
return []byte(`{"schema_version":1,"id":"test-app","name":"Test App","vendor":"SoftBox","version":"` + version + `","channel":"stable","min_os":"windows-10","architecture":"amd64","entrypoint":"` + entrypoint + `","working_directory":".","product_id":"test-product","supports_trial":false,"requires_admin":false,"data_policy":"local-app-data","update_policy":"managed-by-softbox"}`)
|
||||
}
|
||||
Reference in New Issue
Block a user