Add GPG signature verification

This commit is contained in:
2026-02-24 18:50:50 +02:00
parent 45721cfc43
commit 95dee0560a
5 changed files with 394 additions and 11 deletions
+133
View File
@@ -176,6 +176,19 @@ func main() {
setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("bpm %s <options>", subcommand), "Compare two version numbers", os.Args[2:])
compareVersions()
case "keyring":
currentFlagSet = flag.NewFlagSet("keyring", flag.ExitOnError)
currentFlagSet.StringP("root", "R", "/", "Operate on specified root directory")
currentFlagSet.BoolP("yes", "y", false, "Enter 'yes' in all prompts")
currentFlagSet.BoolP("init", "i", false, "Initialize keyring")
currentFlagSet.BoolP("populate", "p", false, "Populate keyring")
currentFlagSet.BoolP("add", "a", false, "Add the specified keys")
currentFlagSet.BoolP("remove", "r", false, "Remove the specified keys")
currentFlagSet.BoolP("list", "l", false, "List all keys")
setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("bpm %s <options>", subcommand), "Manage the BPM keyring", os.Args[2:])
manageKeyring()
case "upgrade-persistent-data":
currentFlagSet = flag.NewFlagSet("upgrade-persistent-data", flag.ExitOnError)
currentFlagSet.StringP("root", "R", "/", "Operate on specified root directory")
@@ -1469,6 +1482,125 @@ func compareVersions() {
fmt.Println(bpmlib.CompareVersions(v1, v2))
}
func manageKeyring() {
// Get flags
rootDir, _ := currentFlagSet.GetString("root")
yesAll, _ := currentFlagSet.GetBool("yes")
initKeyring, _ := currentFlagSet.GetBool("init")
populateKeyring, _ := currentFlagSet.GetBool("populate")
addKeys, _ := currentFlagSet.GetBool("add")
removeKeys, _ := currentFlagSet.GetBool("remove")
listKeys, _ := currentFlagSet.GetBool("list")
// Check for required permissions
if os.Getuid() != 0 {
log.Printf("Error: this subcommand needs to be run with superuser permissions")
exitCode = 1
return
}
if initKeyring {
err := bpmlib.InitializeKeyring(rootDir)
if err != nil {
log.Printf("Error: could not populate keyring: %s", err)
exitCode = 1
return
}
fmt.Println("Keyring initialized successfully!")
} else if populateKeyring {
if !bpmlib.IsKeyringInitialized(rootDir) {
log.Printf("Error: keyring needs to be initialized first")
exitCode = 1
return
}
err := bpmlib.PopulateKeyring(rootDir)
if err != nil {
log.Printf("Error: could not populate keyring: %s", err)
exitCode = 1
return
}
fmt.Println("Keyring populated successfully!")
} else if addKeys {
gpgHomedir := path.Join(rootDir, "/var/lib/bpm/gpg")
if currentFlagSet.NArg() == 0 {
log.Printf("Error: no keys specified")
exitCode = 1
return
}
if !bpmlib.IsKeyringInitialized(rootDir) {
log.Printf("Error: keyring needs to be initialized first")
exitCode = 1
return
}
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--import")
cmd.Args = append(cmd.Args, currentFlagSet.Args()...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
log.Printf("Error: could not add keys: %s", err)
exitCode = 1
return
}
} else if removeKeys {
gpgHomedir := path.Join(rootDir, "/var/lib/bpm/gpg")
if currentFlagSet.NArg() == 0 {
log.Printf("Error: no keys specified")
exitCode = 1
return
}
if !bpmlib.IsKeyringInitialized(rootDir) {
log.Printf("Error: keyring needs to be initialized first")
exitCode = 1
return
}
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--delete-secret-and-public-keys")
if yesAll {
cmd.Args = append(cmd.Args, "--batch", "--yes")
}
cmd.Args = append(cmd.Args, currentFlagSet.Args()...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
log.Printf("Error: could not remove keys: %s", err)
exitCode = 1
return
}
} else if listKeys {
gpgHomedir := path.Join(rootDir, "/var/lib/bpm/gpg")
if !bpmlib.IsKeyringInitialized(rootDir) {
log.Printf("Error: keyring needs to be initialized first")
exitCode = 1
return
}
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--list-keys")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
log.Printf("Error: could not list keys: %s", err)
exitCode = 1
return
}
} else {
currentFlagSet.Usage()
}
}
func printUsage() {
fmt.Printf("Usage: %s <subcommand> [options]\n", os.Args[0])
fmt.Println("Description: Manage system packages")
@@ -1486,6 +1618,7 @@ func printUsage() {
fmt.Println(" c, compile Compile source packages and convert them to binary ones")
fmt.Println(" p, vercmp Compare package version numbers")
fmt.Println("Maintenance subcommands:")
fmt.Println(" keyring Manage the BPM keyring")
fmt.Println(" upgrade-persistent-data Upgrade persistent data directory to the latest format")
}
+1
View File
@@ -16,6 +16,7 @@ type MainBPMConfigStruct struct {
type configDatabase struct {
Name string `yaml:"name"`
Source string `yaml:"source"`
VerificationLevel string `yaml:"verification_level"`
Disabled *bool `yaml:"disabled"`
}
+35 -2
View File
@@ -17,11 +17,20 @@ import (
"gopkg.in/yaml.v3"
)
type VerificationLevel int
const (
VerificationLevelNone VerificationLevel = iota
VerificationLevelAll
VerificationLevelTrusted
)
type BPMDatabase struct {
DatabaseVersion int `yaml:"database_version"`
Entries map[string]*BPMDatabaseEntry `yaml:"entries"`
VirtualPackages map[string][]*BPMDatabaseEntry
Name string
VerificationLevel VerificationLevel
Source string
}
@@ -61,6 +70,16 @@ func (db *configDatabase) ReadLocalDatabase() error {
// Initialize struct values
database.VirtualPackages = make(map[string][]*BPMDatabaseEntry)
database.Name = db.Name
switch db.VerificationLevel {
case "0", "none":
database.VerificationLevel = VerificationLevelNone
case "1", "all":
database.VerificationLevel = VerificationLevelAll
case "2", "trusted":
database.VerificationLevel = VerificationLevelTrusted
default:
database.VerificationLevel = VerificationLevelAll
}
database.Source = db.Source
for entryName, entry := range database.Entries {
@@ -257,12 +276,26 @@ func (db *BPMDatabase) FetchPackage(pkg string) (string, error) {
}
// Download package from url
err = downloadFile("Downloading "+entry.Info.Name, u, path.Join("/var/cache/bpm/fetched/", path.Base(entry.Filepath)), 0644)
filepath := path.Join("/var/cache/bpm/fetched/", path.Base(entry.Filepath))
err = downloadFile("Downloading "+entry.Info.Name, u, filepath, 0644)
if err != nil {
return "", err
}
return path.Join("/var/cache/bpm/fetched/", path.Base(entry.Filepath)), nil
// Download and verify signature if required
if db.VerificationLevel != VerificationLevelNone {
err = downloadFile("", u+".sig", filepath+".sig", 0644)
if err != nil {
return "", err
}
err := VerifySignature(filepath, filepath+".sig", db.VerificationLevel == VerificationLevelTrusted, "/")
if err != nil {
return "", fmt.Errorf("Could not verify signature for %s: %s", filepath, err)
}
}
return filepath, nil
}
func (entry *BPMDatabaseEntry) GetEntryDependants() (dependants []string) {
+216
View File
@@ -0,0 +1,216 @@
package bpmlib
import (
"fmt"
"io"
"os"
"os/exec"
"path"
"strings"
)
func InitializeKeyring(rootDir string) error {
gpgHomedir := path.Join(rootDir, "/var/lib/bpm/gpg")
// Create GPG directory
err := os.Mkdir(gpgHomedir, 0700)
if err != nil && !os.IsExist(err) {
return err
}
// Get number of secret keys
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--list-secret-keys", "--with-colons")
output, err := cmd.Output()
if err != nil {
return err
}
secretKeysLineCount := len(strings.Split(strings.TrimSpace(string(output)), "\n"))
// Create signing key
if secretKeysLineCount <= 1 {
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--batch", "--gen-key")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = strings.NewReader(`%echo Creating signing key...
Key-Type: RSA
Key-Length: 4096
Key-Usage: sign
Name-Real: BPM signing key
Name-Email: bpm@localhost
Expire-Date: 0
%no-protection
%commit
%echo Done`)
err = cmd.Run()
if err != nil {
return err
}
}
return nil
}
func IsKeyringInitialized(rootDir string) bool {
gpgHomedir := path.Join(rootDir, "/var/lib/bpm/gpg")
// Check if gpg directory exists
if stat, err := os.Stat(gpgHomedir); err != nil || !stat.IsDir() {
return false
}
// Get number of secret keys
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--list-secret-keys", "--with-colons")
output, err := cmd.Output()
if err != nil {
return false
}
secretKeysLineCount := len(strings.Split(strings.TrimSpace(string(output)), "\n"))
// Return false if no signing key has been created
if secretKeysLineCount <= 1 {
return false
}
return true
}
func PopulateKeyring(rootDir string) error {
gpgHomedir := path.Join(rootDir, "/var/lib/bpm/gpg")
keyringsDir := path.Join(rootDir, "/var/lib/bpm/keyrings")
dirEntries, err := os.ReadDir(keyringsDir)
if err != nil && !os.IsNotExist(err) {
return err
}
// Remove removed keys
for _, entry := range dirEntries {
if entry.IsDir() {
continue
}
if !strings.HasSuffix(entry.Name(), ".revoked") {
continue
}
data, err := os.ReadFile(path.Join(keyringsDir, entry.Name()))
if err != nil {
return err
}
// Loop over all key IDs
for entry := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
// Ensure key ID exists
err := exec.Command("gpg", "--homedir="+gpgHomedir, "--list-keys", entry).Run()
if err != nil {
continue
}
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--batch", "--yes", "--delete-secret-and-public-keys", entry)
err = cmd.Run()
if err != nil {
return err
}
}
}
// Import all keyrings
for _, entry := range dirEntries {
if entry.IsDir() {
continue
}
if !strings.HasSuffix(entry.Name(), ".pgp") && !strings.HasSuffix(entry.Name(), ".asc") {
continue
}
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--import", path.Join(keyringsDir, entry.Name()))
err := cmd.Run()
if err != nil {
return err
}
}
// Trust keys
for _, entry := range dirEntries {
if entry.IsDir() {
continue
}
if !strings.HasSuffix(entry.Name(), ".trustdb") {
continue
}
data, err := os.ReadFile(path.Join(keyringsDir, entry.Name()))
if err != nil {
return err
}
// Loop over all key IDs
for entry := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
keyID := strings.Split(entry, ":")[0]
// Ensure key ID exists
err := exec.Command("gpg", "--homedir="+gpgHomedir, "--list-keys", keyID).Run()
if err != nil {
continue
}
// Sign key
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--command-fd=0", "--batch", "--lsign-key", keyID)
cmd.Stdin = strings.NewReader("y\ny\n")
err = cmd.Run()
if err != nil {
return err
}
}
// Import owner trust database
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--import-ownertrust", path.Join(keyringsDir, entry.Name()))
err = cmd.Run()
if err != nil {
return err
}
}
return nil
}
func VerifySignature(filename, signature string, requireTrusted bool, rootDir string) error {
gpgHomedir := path.Join(rootDir, "/var/lib/bpm/gpg")
if _, err := os.Stat(gpgHomedir); err != nil {
return err
}
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--status-fd=3", "--verify", signature, filename)
pipeReader, pipeWriter, err := os.Pipe()
if err != nil {
return err
}
defer pipeReader.Close()
defer pipeWriter.Close()
cmd.ExtraFiles = append(cmd.ExtraFiles, pipeWriter)
err = cmd.Run()
if err != nil {
return err
}
pipeWriter.Close()
if requireTrusted {
data, err := io.ReadAll(pipeReader)
if err != nil {
return err
}
dataStr := string(data)
if !strings.Contains(dataStr, "[GNUPG:] TRUST_FULLY") && !strings.Contains(dataStr, "[GNUPG:] TRUST_ULTIMATE") {
return fmt.Errorf("signature verified but not trusted")
}
}
return err
}
+1 -1
View File
@@ -39,7 +39,7 @@ func downloadFile(barText, u, filepath string, perm os.FileMode) error {
defer file.Close()
// Create progress bar
bar := createProgressBar(resp.ContentLength, barText, false)
bar := createProgressBar(resp.ContentLength, barText, barText == "")
defer bar.Close()
// Copy data