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:]) setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("bpm %s <options>", subcommand), "Compare two version numbers", os.Args[2:])
compareVersions() 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": case "upgrade-persistent-data":
currentFlagSet = flag.NewFlagSet("upgrade-persistent-data", flag.ExitOnError) currentFlagSet = flag.NewFlagSet("upgrade-persistent-data", flag.ExitOnError)
currentFlagSet.StringP("root", "R", "/", "Operate on specified root directory") currentFlagSet.StringP("root", "R", "/", "Operate on specified root directory")
@@ -1469,6 +1482,125 @@ func compareVersions() {
fmt.Println(bpmlib.CompareVersions(v1, v2)) 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() { func printUsage() {
fmt.Printf("Usage: %s <subcommand> [options]\n", os.Args[0]) fmt.Printf("Usage: %s <subcommand> [options]\n", os.Args[0])
fmt.Println("Description: Manage system packages") 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(" c, compile Compile source packages and convert them to binary ones")
fmt.Println(" p, vercmp Compare package version numbers") fmt.Println(" p, vercmp Compare package version numbers")
fmt.Println("Maintenance subcommands:") fmt.Println("Maintenance subcommands:")
fmt.Println(" keyring Manage the BPM keyring")
fmt.Println(" upgrade-persistent-data Upgrade persistent data directory to the latest format") fmt.Println(" upgrade-persistent-data Upgrade persistent data directory to the latest format")
} }
+4 -3
View File
@@ -14,9 +14,10 @@ type MainBPMConfigStruct struct {
} }
type configDatabase struct { type configDatabase struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Source string `yaml:"source"` Source string `yaml:"source"`
Disabled *bool `yaml:"disabled"` VerificationLevel string `yaml:"verification_level"`
Disabled *bool `yaml:"disabled"`
} }
type CompilationBPMConfigStruct struct { type CompilationBPMConfigStruct struct {
+40 -7
View File
@@ -17,12 +17,21 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
type VerificationLevel int
const (
VerificationLevelNone VerificationLevel = iota
VerificationLevelAll
VerificationLevelTrusted
)
type BPMDatabase struct { type BPMDatabase struct {
DatabaseVersion int `yaml:"database_version"` DatabaseVersion int `yaml:"database_version"`
Entries map[string]*BPMDatabaseEntry `yaml:"entries"` Entries map[string]*BPMDatabaseEntry `yaml:"entries"`
VirtualPackages map[string][]*BPMDatabaseEntry VirtualPackages map[string][]*BPMDatabaseEntry
Name string Name string
Source string VerificationLevel VerificationLevel
Source string
} }
type BPMDatabaseEntry struct { type BPMDatabaseEntry struct {
@@ -61,6 +70,16 @@ func (db *configDatabase) ReadLocalDatabase() error {
// Initialize struct values // Initialize struct values
database.VirtualPackages = make(map[string][]*BPMDatabaseEntry) database.VirtualPackages = make(map[string][]*BPMDatabaseEntry)
database.Name = db.Name 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 database.Source = db.Source
for entryName, entry := range database.Entries { for entryName, entry := range database.Entries {
@@ -257,12 +276,26 @@ func (db *BPMDatabase) FetchPackage(pkg string) (string, error) {
} }
// Download package from url // 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 { if err != nil {
return "", err 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) { 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() defer file.Close()
// Create progress bar // Create progress bar
bar := createProgressBar(resp.ContentLength, barText, false) bar := createProgressBar(resp.ContentLength, barText, barText == "")
defer bar.Close() defer bar.Close()
// Copy data // Copy data