From 4dbb253696864c8c205b9cd5d2e6932f184d521e Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Fri, 19 Jun 2026 03:26:04 -0400 Subject: [PATCH] check sigs before extraction --- internal/appherder/appimage.go | 2 +- internal/appherder/install.go | 32 ++++++++++++++++++++++---------- internal/appherder/list_test.go | 2 +- internal/appherder/sync.go | 2 +- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/internal/appherder/appimage.go b/internal/appherder/appimage.go index 83cda0e..3585187 100644 --- a/internal/appherder/appimage.go +++ b/internal/appherder/appimage.go @@ -38,7 +38,7 @@ func openAppImage(path string) (fs.FS, func(), error) { return openSquashFS(file, offset) } - // Not at the expected offset — try scanning forward. + // Not at the expected offset; try scanning forward. if scanned, ok := scanForSquashFS(file, offset); ok { return openSquashFS(file, scanned) } diff --git a/internal/appherder/install.go b/internal/appherder/install.go index a4335b2..b8a8d25 100644 --- a/internal/appherder/install.go +++ b/internal/appherder/install.go @@ -20,6 +20,14 @@ func (a App) install(appimage string, want expectedChecksum) (appName string, er return "", fmt.Errorf("resolve AppImage path %q: %w", appimage, err) } + // Verify before openAppImage: the DwarFS fallback executes the AppImage to + // extract it, so bad images must be refused first. Pinned-key check is + // deferred until the app name is known from the desktop file inside. + fingerprint, err := verifyAppImage(appimage, "", want) + if err != nil { + return "", err + } + fsys, closeAppImage, err := openAppImage(appimage) if err != nil { return "", err @@ -41,12 +49,17 @@ func (a App) install(appimage string, want expectedChecksum) (appName string, er icon := resolveIcon(fsys) appName = deriveAppName(desktop, desktopName, appimage) - // Verify integrity and signature before any filesystem writes, so a refused - // AppImage installs nothing. - pin, err := verifyAppImage(appimage, a.pinnedSigningKey(appName), want) - if err != nil { - return "", err + // Pinned-key check deferred from above; app name now known. + pinned := a.pinnedSigningKey(appName) + if pinned != "" { + if fingerprint == "" { + return "", fmt.Errorf("refusing unsigned AppImage: a signing key is pinned (%s); uninstall and reinstall to trust a different build", pinned) + } + if !strings.EqualFold(pinned, fingerprint) { + return "", fmt.Errorf("refusing AppImage: signing key changed (pinned %s, got %s); uninstall and reinstall to trust the new key", pinned, fingerprint) + } } + pin := fingerprint // No desktop file inside the AppImage: synthesize a terminal launcher so // CLI apps still get a menu entry and are tracked by managedApps. @@ -114,9 +127,9 @@ func (a App) InstallFromURL(ctx context.Context, url string) (string, error) { return a.install(tmpName, expectedChecksum{}) } -// deriveAppName picks the canonical install name, matching GearLever so the -// two tools land at the same path: the desktop entry's Name field (e.g. -// "ES-DE" -> "esde"), then the desktop-file id, then the source filename. +// deriveAppName picks the canonical install name: the desktop entry's Name +// field (e.g. "ES-DE" -> "esde"), then the desktop-file id, then the source +// filename. func deriveAppName(desktop *desktopfile.File, desktopName string, appimagePath string) string { if desktop != nil { if name, ok := desktop.Get(desktopEntrySection, "Name"); ok && name != "" { @@ -130,8 +143,7 @@ func deriveAppName(desktop *desktopfile.File, desktopName string, appimagePath s } // sanitizeAppName lowercases s, turns spaces into underscores, and drops any -// character that isn't alphanumeric, underscore, or dot — GearLever's naming -// rule. +// character that isn't alphanumeric, underscore, or dot. func sanitizeAppName(name string) string { name = strings.ToLower(name) name = strings.ReplaceAll(name, " ", "_") diff --git a/internal/appherder/list_test.go b/internal/appherder/list_test.go index b7e1894..acac030 100644 --- a/internal/appherder/list_test.go +++ b/internal/appherder/list_test.go @@ -135,7 +135,7 @@ func TestListFallsBackToFilenameForName(t *testing.T) { t.Fatal(err) } - // Desktop file with no Name= field — list should fall back to the filename. + // Desktop file with no Name= field; list should fall back to the filename. if err := os.WriteFile(filepath.Join(appimages, "noname.appimage"), []byte("x"), 0o644); err != nil { t.Fatal(err) } diff --git a/internal/appherder/sync.go b/internal/appherder/sync.go index c3afd6c..c025f24 100644 --- a/internal/appherder/sync.go +++ b/internal/appherder/sync.go @@ -151,7 +151,7 @@ func findAppImagePath(dir, appid string) (string, error) { } // appImageBackedOrphans returns appids of unmanaged desktop entries whose -// TryExec or Exec points at a missing file inside appimagesDir — launchers left +// TryExec or Exec points at a missing file inside appimagesDir; launchers left // by another tool after their AppImage was deleted. func appImageBackedOrphans(applicationsDir, appimagesDir string) ([]string, error) { matches, err := filepath.Glob(filepath.Join(applicationsDir, "*.desktop")) -- 2.51.2