A shepherd for your Appimages.
0

Configure Feed

Select the types of activity you want to include in your feed.

list: show signature status

Aly Raffauf (Jun 18, 2026, 10:50 AM EDT) ee0ad24b 32af0698

+80 -9
+3 -3
cmd/appherder/format.go
··· 11 11 12 12 func printAppList(out io.Writer, infos []appherder.AppInfo) { 13 13 tw := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) 14 - fmt.Fprintln(tw, "NAME\tFILENAME\tVERSION\tSIZE\tSOURCE") 14 + fmt.Fprintln(tw, "NAME\tFILENAME\tVERSION\tSIZE\tSOURCE\tSIGNATURE") 15 15 for _, info := range infos { 16 16 size := "-" 17 17 if info.Size > 0 { 18 18 size = humanSize(info.Size) 19 19 } 20 - fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", 21 - info.Name, orDash(info.Filename), orDash(info.Version), size, orDash(info.Source)) 20 + fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n", 21 + info.Name, orDash(info.Filename), orDash(info.Version), size, orDash(info.Source), info.Signature) 22 22 } 23 23 tw.Flush() 24 24 }
+12 -6
internal/appherder/list.go
··· 10 10 11 11 // AppInfo is one managed app's display metadata, as returned by List. 12 12 type AppInfo struct { 13 - AppID string 14 - Name string // desktop Name= field, falls back to filename 15 - Filename string // basename of the AppImage in ~/AppImages, "" when missing 16 - Version string // desktop X-AppImage-Version= 17 - Size int64 18 - Source string // update source kind, "none" when no info 13 + AppID string 14 + Name string // desktop Name= field, falls back to filename 15 + Filename string // basename of the AppImage in ~/AppImages, "" when missing 16 + Version string // desktop X-AppImage-Version= 17 + Size int64 18 + Source string // update source kind, "none" when no info 19 + Signature string // "pinned", "signed", or "none" 19 20 } 20 21 21 22 // List returns display metadata for every app appherder manages. Offline and ··· 39 40 func gatherAppInfo(appsDir, appimagesDir, appid string) AppInfo { 40 41 info := AppInfo{AppID: appid, Source: "none"} 41 42 43 + var pinned string 42 44 if desktop, err := desktopfile.Read(filepath.Join(appsDir, appid+".desktop")); err == nil { 43 45 if name, ok := desktop.Get(desktopEntrySection, "Name"); ok && name != "" { 44 46 info.Name = name ··· 46 48 if version, ok := desktop.Get(desktopEntrySection, "X-AppImage-Version"); ok { 47 49 info.Version = version 48 50 } 51 + pinned, _ = desktop.Get(desktopEntrySection, desktopSigningKey) 49 52 } 50 53 54 + var signed bool 51 55 if path, err := findAppImagePath(appimagesDir, appid); err == nil && path != "" { 52 56 info.Filename = filepath.Base(path) 53 57 if stat, err := os.Stat(path); err == nil { ··· 56 60 if src, err := SourceForAppImage(path); err == nil && src != nil { 57 61 info.Source = src.Kind() 58 62 } 63 + signed = appImageSigned(path) 59 64 } 65 + info.Signature = signatureStatus(pinned, signed) 60 66 61 67 if info.Name == "" { 62 68 if info.Filename != "" {
+43
internal/appherder/list_test.go
··· 70 70 } 71 71 } 72 72 73 + func TestListSignatureStatus(t *testing.T) { 74 + a, home := newTestApp(t) 75 + appimages := filepath.Join(home, "AppImages") 76 + appsDir := filepath.Join(home, ".local", "share", "applications") 77 + if err := os.MkdirAll(appimages, 0o755); err != nil { 78 + t.Fatal(err) 79 + } 80 + if err := os.MkdirAll(appsDir, 0o755); err != nil { 81 + t.Fatal(err) 82 + } 83 + 84 + layout := buildSignableELF(2048, 4096) 85 + sig, key := signWith(t, layout, newTestEntity(t)) 86 + signedImage := layout.embed(sig, key) 87 + unsignedImage := buildSignableELF(2048, 4096).bytes 88 + 89 + write := func(name string, image []byte, desktop string) { 90 + if err := os.WriteFile(filepath.Join(appimages, name+".appimage"), image, 0o644); err != nil { 91 + t.Fatal(err) 92 + } 93 + if err := os.WriteFile(filepath.Join(appsDir, name+".desktop"), []byte(desktop), 0o644); err != nil { 94 + t.Fatal(err) 95 + } 96 + } 97 + write("signed", signedImage, "[Desktop Entry]\nName=Signed\n"+desktopOwnerKey+"=true\n") 98 + write("pinned", signedImage, "[Desktop Entry]\nName=Pinned\n"+desktopOwnerKey+"=true\n"+desktopSigningKey+"=ABC123\n") 99 + write("plain", unsignedImage, "[Desktop Entry]\nName=Plain\n"+desktopOwnerKey+"=true\n") 100 + 101 + infos, err := a.List() 102 + if err != nil { 103 + t.Fatal(err) 104 + } 105 + got := make(map[string]string, len(infos)) 106 + for _, info := range infos { 107 + got[info.Name] = info.Signature 108 + } 109 + for name, want := range map[string]string{"Signed": "signed", "Pinned": "pinned", "Plain": "none"} { 110 + if got[name] != want { 111 + t.Errorf("%s signature = %q, want %q", name, got[name], want) 112 + } 113 + } 114 + } 115 + 73 116 func TestListEmptyWhenNothingManaged(t *testing.T) { 74 117 a, _ := newTestApp(t) 75 118 infos, err := a.List()
+22
internal/appherder/signature.go
··· 193 193 return fingerprint, nil 194 194 } 195 195 196 + // appImageSigned reports whether file carries a signature, without verifying it 197 + // (verification happens at install). It reads only the section, not the whole 198 + // file, so it is cheap enough for listing. 199 + func appImageSigned(file string) bool { 200 + sig, _, _, err := readSignatureSections(file) 201 + return err == nil && len(bytes.TrimSpace(sig)) > 0 202 + } 203 + 204 + // signatureStatus summarizes an app's signature trust for display: "pinned" when 205 + // a signing key is locked in, "signed" when the AppImage carries an as-yet 206 + // unverified signature, "none" otherwise. 207 + func signatureStatus(pinned string, signed bool) string { 208 + switch { 209 + case pinned != "": 210 + return "pinned" 211 + case signed: 212 + return "signed" 213 + default: 214 + return "none" 215 + } 216 + } 217 + 196 218 // pinnedSigningKey returns the fingerprint appherder pinned for appName, or "". 197 219 func (a App) pinnedSigningKey(appName string) string { 198 220 desktop, err := desktopfile.Read(filepath.Join(a.applicationsDir, appName+".desktop"))