mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
feat(extensions): verify decentralized package integrity
This commit is contained in:
+103
-83
@@ -76,7 +76,104 @@ func safeExtensionAssetPath(root, assetPath string) (string, bool) {
|
||||
return fullPath, isPathWithinBase(root, fullPath)
|
||||
}
|
||||
|
||||
const (
|
||||
maxExtensionArchiveEntries = 2048
|
||||
maxExtensionArchiveUncompressedBytes = 256 * 1024 * 1024
|
||||
maxExtensionManifestBytes = 1024 * 1024
|
||||
)
|
||||
|
||||
func validateExtensionArchive(files []*zip.File) error {
|
||||
if len(files) > maxExtensionArchiveEntries {
|
||||
return fmt.Errorf(
|
||||
"extension archive contains too many entries (maximum %d)",
|
||||
maxExtensionArchiveEntries,
|
||||
)
|
||||
}
|
||||
|
||||
seenPaths := make(map[string]struct{}, len(files))
|
||||
var totalUncompressed uint64
|
||||
for _, file := range files {
|
||||
if file.FileInfo().Mode()&os.ModeSymlink != 0 || strings.Contains(file.Name, `\`) {
|
||||
return fmt.Errorf("unsafe path in extension archive: %s", file.Name)
|
||||
}
|
||||
|
||||
relPath := path.Clean(file.Name)
|
||||
if relPath == "." || relPath == ".." || strings.HasPrefix(relPath, "../") || path.IsAbs(relPath) {
|
||||
return fmt.Errorf("unsafe path in extension archive: %s", file.Name)
|
||||
}
|
||||
pathKey := strings.ToLower(relPath)
|
||||
if _, exists := seenPaths[pathKey]; exists {
|
||||
return fmt.Errorf("duplicate path in extension archive: %s", file.Name)
|
||||
}
|
||||
seenPaths[pathKey] = struct{}{}
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
if file.UncompressedSize64 > maxExtensionArchiveUncompressedBytes-totalUncompressed {
|
||||
return fmt.Errorf(
|
||||
"extension archive exceeds the %d MiB extracted size limit",
|
||||
maxExtensionArchiveUncompressedBytes/(1024*1024),
|
||||
)
|
||||
}
|
||||
totalUncompressed += file.UncompressedSize64
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func inspectExtensionPackage(files []*zip.File) (*ExtensionManifest, error) {
|
||||
if err := validateExtensionArchive(files); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var manifestFile *zip.File
|
||||
hasIndexJS := false
|
||||
for _, file := range files {
|
||||
switch path.Clean(file.Name) {
|
||||
case "manifest.json":
|
||||
manifestFile = file
|
||||
case "index.js":
|
||||
hasIndexJS = !file.FileInfo().IsDir()
|
||||
}
|
||||
}
|
||||
|
||||
if manifestFile == nil || manifestFile.FileInfo().IsDir() {
|
||||
return nil, fmt.Errorf("invalid extension package: root manifest.json not found")
|
||||
}
|
||||
if !hasIndexJS {
|
||||
return nil, fmt.Errorf("invalid extension package: root index.js not found")
|
||||
}
|
||||
if manifestFile.UncompressedSize64 > maxExtensionManifestBytes {
|
||||
return nil, fmt.Errorf("invalid extension package: manifest.json is too large")
|
||||
}
|
||||
|
||||
rc, err := manifestFile.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open manifest.json: %w", err)
|
||||
}
|
||||
manifestData, readErr := io.ReadAll(io.LimitReader(rc, maxExtensionManifestBytes+1))
|
||||
closeErr := rc.Close()
|
||||
if readErr != nil {
|
||||
return nil, fmt.Errorf("failed to read manifest.json: %w", readErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
return nil, fmt.Errorf("failed to close manifest.json: %w", closeErr)
|
||||
}
|
||||
if len(manifestData) > maxExtensionManifestBytes {
|
||||
return nil, fmt.Errorf("invalid extension package: manifest.json is too large")
|
||||
}
|
||||
|
||||
manifest, err := ParseManifest(manifestData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid extension manifest: %w", err)
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func extractExtensionArchive(zipReader *zip.ReadCloser, destination string) error {
|
||||
if err := validateExtensionArchive(zipReader.File); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range zipReader.File {
|
||||
if file.FileInfo().IsDir() {
|
||||
continue
|
||||
@@ -260,37 +357,9 @@ func (m *extensionManager) loadExtensionFromFileLocked(filePath string) (*loaded
|
||||
}
|
||||
defer zipReader.Close()
|
||||
|
||||
var manifestData []byte
|
||||
var hasIndexJS bool
|
||||
for _, file := range zipReader.File {
|
||||
name := filepath.Base(file.Name)
|
||||
if name == "manifest.json" {
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open manifest.json: %w", err)
|
||||
}
|
||||
manifestData, err = io.ReadAll(rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read manifest.json: %w", err)
|
||||
}
|
||||
}
|
||||
if name == "index.js" {
|
||||
hasIndexJS = true
|
||||
}
|
||||
}
|
||||
|
||||
if manifestData == nil {
|
||||
return nil, fmt.Errorf("invalid extension package: manifest.json not found")
|
||||
}
|
||||
|
||||
if !hasIndexJS {
|
||||
return nil, fmt.Errorf("invalid extension package: index.js not found")
|
||||
}
|
||||
|
||||
manifest, err := ParseManifest(manifestData)
|
||||
manifest, err := inspectExtensionPackage(zipReader.File)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid extension manifest: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
@@ -1006,37 +1075,9 @@ func (m *extensionManager) upgradeExtensionLocked(filePath string) (*loadedExten
|
||||
}
|
||||
defer zipReader.Close()
|
||||
|
||||
var manifestData []byte
|
||||
var hasIndexJS bool
|
||||
for _, file := range zipReader.File {
|
||||
name := filepath.Base(file.Name)
|
||||
if name == "manifest.json" {
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open manifest.json: %w", err)
|
||||
}
|
||||
manifestData, err = io.ReadAll(rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read manifest.json: %w", err)
|
||||
}
|
||||
}
|
||||
if name == "index.js" {
|
||||
hasIndexJS = true
|
||||
}
|
||||
}
|
||||
|
||||
if manifestData == nil {
|
||||
return nil, fmt.Errorf("invalid extension package: manifest.json not found")
|
||||
}
|
||||
|
||||
if !hasIndexJS {
|
||||
return nil, fmt.Errorf("invalid extension package: index.js not found")
|
||||
}
|
||||
|
||||
newManifest, err := ParseManifest(manifestData)
|
||||
newManifest, err := inspectExtensionPackage(zipReader.File)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid extension manifest: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
@@ -1159,30 +1200,9 @@ func (m *extensionManager) checkExtensionUpgradeInternal(filePath string) (*Exte
|
||||
}
|
||||
defer zipReader.Close()
|
||||
|
||||
var manifestData []byte
|
||||
for _, file := range zipReader.File {
|
||||
name := filepath.Base(file.Name)
|
||||
if name == "manifest.json" {
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open manifest.json")
|
||||
}
|
||||
manifestData, err = io.ReadAll(rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read manifest.json")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if manifestData == nil {
|
||||
return nil, fmt.Errorf("manifest.json not found")
|
||||
}
|
||||
|
||||
newManifest, err := ParseManifest(manifestData)
|
||||
newManifest, err := inspectExtensionPackage(zipReader.File)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid manifest: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteVerifiedExtensionPackageAcceptsMatchingSHA256(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dest := filepath.Join(dir, "verified.spotiflac-ext")
|
||||
payload := []byte("extension package")
|
||||
checksum := fmt.Sprintf("%x", sha256.Sum256(payload))
|
||||
|
||||
if err := writeVerifiedExtensionPackage(bytes.NewReader(payload), dest, checksum); err != nil {
|
||||
t.Fatalf("writeVerifiedExtensionPackage: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(dest)
|
||||
if err != nil {
|
||||
t.Fatalf("read verified package: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("verified package = %q, want %q", got, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteVerifiedExtensionPackageRejectsMismatchBeforeReplace(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dest := filepath.Join(dir, "cached.spotiflac-ext")
|
||||
if err := os.WriteFile(dest, []byte("existing"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err := writeVerifiedExtensionPackage(
|
||||
bytes.NewReader([]byte("tampered")),
|
||||
dest,
|
||||
strings.Repeat("0", sha256.Size*2),
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), "SHA-256 mismatch") {
|
||||
t.Fatalf("expected checksum mismatch, got %v", err)
|
||||
}
|
||||
got, readErr := os.ReadFile(dest)
|
||||
if readErr != nil {
|
||||
t.Fatal(readErr)
|
||||
}
|
||||
if string(got) != "existing" {
|
||||
t.Fatalf("checksum failure replaced existing package with %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryRejectsMalformedExtensionChecksum(t *testing.T) {
|
||||
_, err := parseRegistryBody([]byte(
|
||||
`{"version":1,"extensions":[{"id":"ext","name":"ext","version":"1.0.0","sha256":"not-a-hash"}]}`,
|
||||
))
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid SHA-256") {
|
||||
t.Fatalf("expected invalid checksum error, got %v", err)
|
||||
}
|
||||
|
||||
checksum := strings.Repeat("a", sha256.Size*2)
|
||||
registry, err := parseRegistryBody([]byte(
|
||||
`{"version":1,"extensions":[{"id":"ext","name":"ext","version":"1.0.0","checksumSha256":"sha256:` +
|
||||
checksum +
|
||||
`"}]}`,
|
||||
))
|
||||
if err != nil {
|
||||
t.Fatalf("parse registry with checksum alias: %v", err)
|
||||
}
|
||||
if got := registry.Extensions[0].getSHA256(); got != checksum {
|
||||
t.Fatalf("normalized checksum = %q, want %q", got, checksum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionPackageRequiresUniqueRootEntrypoints(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
duplicate := filepath.Join(dir, "duplicate.spotiflac-ext")
|
||||
createTestExtensionPackage(
|
||||
t,
|
||||
duplicate,
|
||||
"duplicate-ext",
|
||||
"1.0.0",
|
||||
`registerExtension({});`,
|
||||
map[string]string{"MANIFEST.JSON": "{}"},
|
||||
)
|
||||
|
||||
reader, err := zip.OpenReader(duplicate)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, inspectErr := inspectExtensionPackage(reader.File)
|
||||
_ = reader.Close()
|
||||
if inspectErr == nil || !strings.Contains(inspectErr.Error(), "duplicate path") {
|
||||
t.Fatalf("expected duplicate archive path error, got %v", inspectErr)
|
||||
}
|
||||
|
||||
nested := filepath.Join(dir, "nested.spotiflac-ext")
|
||||
writeTestZip(t, nested, map[string]string{
|
||||
"nested/manifest.json": validSecurityTestManifest("nested-ext"),
|
||||
"nested/index.js": `registerExtension({});`,
|
||||
})
|
||||
reader, err = zip.OpenReader(nested)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, inspectErr = inspectExtensionPackage(reader.File)
|
||||
_ = reader.Close()
|
||||
if inspectErr == nil || !strings.Contains(inspectErr.Error(), "root manifest.json") {
|
||||
t.Fatalf("expected root entrypoint error, got %v", inspectErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionPackageRejectsUnsafeAndOversizedEntries(t *testing.T) {
|
||||
for _, unsafePath := range []string{
|
||||
"../outside.js",
|
||||
`nested\outside.js`,
|
||||
"/absolute.js",
|
||||
} {
|
||||
t.Run(unsafePath, func(t *testing.T) {
|
||||
archivePath := filepath.Join(t.TempDir(), "unsafe.spotiflac-ext")
|
||||
writeTestZip(t, archivePath, map[string]string{
|
||||
"manifest.json": validSecurityTestManifest("unsafe-ext"),
|
||||
"index.js": `registerExtension({});`,
|
||||
unsafePath: "unsafe",
|
||||
})
|
||||
reader, err := zip.OpenReader(archivePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, inspectErr := inspectExtensionPackage(reader.File)
|
||||
_ = reader.Close()
|
||||
if inspectErr == nil || !strings.Contains(inspectErr.Error(), "unsafe path") {
|
||||
t.Fatalf("expected unsafe archive path error, got %v", inspectErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
oversized := &zip.File{FileHeader: zip.FileHeader{
|
||||
Name: "payload.bin",
|
||||
UncompressedSize64: maxExtensionArchiveUncompressedBytes + 1,
|
||||
}}
|
||||
if err := validateExtensionArchive([]*zip.File{oversized}); err == nil ||
|
||||
!strings.Contains(err.Error(), "extracted size limit") {
|
||||
t.Fatalf("expected extracted size error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validSecurityTestManifest(name string) string {
|
||||
return fmt.Sprintf(
|
||||
`{"name":%q,"displayName":%q,"version":"1.0.0","description":"test","type":["metadata_provider"],"permissions":{}}`,
|
||||
name,
|
||||
name,
|
||||
)
|
||||
}
|
||||
|
||||
func writeTestZip(t *testing.T, filePath string, files map[string]string) {
|
||||
t.Helper()
|
||||
output, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archive := zip.NewWriter(output)
|
||||
for name, content := range files {
|
||||
writer, createErr := archive.Create(name)
|
||||
if createErr != nil {
|
||||
t.Fatal(createErr)
|
||||
}
|
||||
if _, writeErr := writer.Write([]byte(content)); writeErr != nil {
|
||||
t.Fatal(writeErr)
|
||||
}
|
||||
}
|
||||
if err := archive.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := output.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
+128
-10
@@ -1,6 +1,8 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -34,10 +36,13 @@ type repoExtension struct {
|
||||
Downloads int `json:"downloads"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
MinAppVersion string `json:"min_app_version,omitempty"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
ChecksumSHA256 string `json:"checksum_sha256,omitempty"`
|
||||
DisplayNameAlt string `json:"displayName,omitempty"`
|
||||
DownloadURLAlt string `json:"downloadUrl,omitempty"`
|
||||
IconURLAlt string `json:"iconUrl,omitempty"`
|
||||
MinAppVersionAlt string `json:"minAppVersion,omitempty"`
|
||||
ChecksumAlt string `json:"checksumSha256,omitempty"`
|
||||
}
|
||||
|
||||
func (e *repoExtension) getDisplayName() string {
|
||||
@@ -71,6 +76,14 @@ func (e *repoExtension) getMinAppVersion() string {
|
||||
return e.MinAppVersionAlt
|
||||
}
|
||||
|
||||
func (e *repoExtension) getRawSHA256() string {
|
||||
return firstNonEmpty(e.SHA256, e.ChecksumSHA256, e.ChecksumAlt)
|
||||
}
|
||||
|
||||
func (e *repoExtension) getSHA256() string {
|
||||
return normalizeSHA256(e.getRawSHA256())
|
||||
}
|
||||
|
||||
type repoRegistry struct {
|
||||
Version int `json:"version"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
@@ -90,6 +103,7 @@ type repoExtensionResponse struct {
|
||||
Downloads int `json:"downloads"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
MinAppVersion string `json:"min_app_version,omitempty"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
IsInstalled bool `json:"is_installed"`
|
||||
InstalledVersion string `json:"installed_version,omitempty"`
|
||||
HasUpdate bool `json:"has_update"`
|
||||
@@ -108,6 +122,7 @@ func (e *repoExtension) toResponse() repoExtensionResponse {
|
||||
Downloads: e.Downloads,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
MinAppVersion: e.getMinAppVersion(),
|
||||
SHA256: e.getSHA256(),
|
||||
}
|
||||
|
||||
if len(e.Tags) > 0 {
|
||||
@@ -316,6 +331,16 @@ func parseRegistryBody(body []byte) (*repoRegistry, error) {
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse registry: %w", err)
|
||||
}
|
||||
for index := range registry.Extensions {
|
||||
ext := ®istry.Extensions[index]
|
||||
rawChecksum := ext.getRawSHA256()
|
||||
if rawChecksum != "" && normalizeSHA256(rawChecksum) == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"extension %q has an invalid SHA-256 checksum",
|
||||
ext.ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
return ®istry, nil
|
||||
}
|
||||
|
||||
@@ -398,22 +423,115 @@ func (s *extensionRepo) downloadExtension(extensionID string, destPath string) e
|
||||
return fmt.Errorf("download returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
out, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
os.Remove(destPath)
|
||||
return fmt.Errorf("failed to write file: %w", err)
|
||||
if err := writeVerifiedExtensionPackage(
|
||||
resp.Body,
|
||||
destPath,
|
||||
ext.getRawSHA256(),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
LogInfo("ExtensionRepo", "Downloaded %s to %s", ext.getDisplayName(), destPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
const maxExtensionPackageBytes int64 = 64 * 1024 * 1024
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeSHA256(value string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
normalized = strings.TrimPrefix(normalized, "sha256:")
|
||||
if len(normalized) != sha256.Size*2 {
|
||||
return ""
|
||||
}
|
||||
for _, char := range normalized {
|
||||
if (char < '0' || char > '9') && (char < 'a' || char > 'f') {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func writeVerifiedExtensionPackage(
|
||||
reader io.Reader,
|
||||
destPath string,
|
||||
expectedSHA256 string,
|
||||
) error {
|
||||
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
||||
return fmt.Errorf("failed to prepare extension download directory: %w", err)
|
||||
}
|
||||
|
||||
tempFile, err := os.CreateTemp(
|
||||
filepath.Dir(destPath),
|
||||
"."+filepath.Base(destPath)+".download-*",
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create extension download: %w", err)
|
||||
}
|
||||
tempPath := tempFile.Name()
|
||||
committed := false
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
if !committed {
|
||||
_ = os.Remove(tempPath)
|
||||
}
|
||||
}()
|
||||
|
||||
hasher := sha256.New()
|
||||
limited := &io.LimitedReader{R: reader, N: maxExtensionPackageBytes + 1}
|
||||
written, copyErr := io.Copy(io.MultiWriter(tempFile, hasher), limited)
|
||||
if copyErr != nil {
|
||||
return fmt.Errorf("failed to write extension package: %w", copyErr)
|
||||
}
|
||||
if written > maxExtensionPackageBytes {
|
||||
return fmt.Errorf(
|
||||
"extension package exceeds the %d MiB size limit",
|
||||
maxExtensionPackageBytes/(1024*1024),
|
||||
)
|
||||
}
|
||||
if err := tempFile.Sync(); err != nil {
|
||||
return fmt.Errorf("failed to flush extension package: %w", err)
|
||||
}
|
||||
if err := tempFile.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close extension package: %w", err)
|
||||
}
|
||||
|
||||
expected := normalizeSHA256(expectedSHA256)
|
||||
if strings.TrimSpace(expectedSHA256) != "" && expected == "" {
|
||||
return fmt.Errorf("registry contains an invalid extension SHA-256 checksum")
|
||||
}
|
||||
if expected != "" {
|
||||
actual := fmt.Sprintf("%x", hasher.Sum(nil))
|
||||
if subtle.ConstantTimeCompare([]byte(actual), []byte(expected)) != 1 {
|
||||
return fmt.Errorf(
|
||||
"extension package integrity check failed: SHA-256 mismatch",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LogWarn(
|
||||
"ExtensionRepo",
|
||||
"Registry entry has no SHA-256 checksum; package integrity cannot be verified",
|
||||
)
|
||||
}
|
||||
|
||||
if err := os.Remove(destPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to replace cached extension package: %w", err)
|
||||
}
|
||||
if err := os.Rename(tempPath, destPath); err != nil {
|
||||
return fmt.Errorf("failed to publish extension package: %w", err)
|
||||
}
|
||||
committed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveRegistryURL(input string) (string, error) {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
|
||||
Reference in New Issue
Block a user