diff --git a/src/bpm/main.go b/src/bpm/main.go index 34158fc..4f66361 100644 --- a/src/bpm/main.go +++ b/src/bpm/main.go @@ -176,6 +176,19 @@ func main() { setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("bpm %s ", 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 ", 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 [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") } diff --git a/src/bpmlib/config.go b/src/bpmlib/config.go index 70af00f..77193bb 100644 --- a/src/bpmlib/config.go +++ b/src/bpmlib/config.go @@ -14,9 +14,10 @@ type MainBPMConfigStruct struct { } type configDatabase struct { - Name string `yaml:"name"` - Source string `yaml:"source"` - Disabled *bool `yaml:"disabled"` + Name string `yaml:"name"` + Source string `yaml:"source"` + VerificationLevel string `yaml:"verification_level"` + Disabled *bool `yaml:"disabled"` } type CompilationBPMConfigStruct struct { diff --git a/src/bpmlib/databases.go b/src/bpmlib/databases.go index fe0df1b..73aa320 100644 --- a/src/bpmlib/databases.go +++ b/src/bpmlib/databases.go @@ -17,12 +17,21 @@ 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 - Source string + DatabaseVersion int `yaml:"database_version"` + Entries map[string]*BPMDatabaseEntry `yaml:"entries"` + VirtualPackages map[string][]*BPMDatabaseEntry + Name string + VerificationLevel VerificationLevel + Source string } type BPMDatabaseEntry struct { @@ -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) { diff --git a/src/bpmlib/gpg.go b/src/bpmlib/gpg.go new file mode 100644 index 0000000..a6f48a9 --- /dev/null +++ b/src/bpmlib/gpg.go @@ -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 +} diff --git a/src/bpmlib/net.go b/src/bpmlib/net.go index 420a96b..803fd5d 100644 --- a/src/bpmlib/net.go +++ b/src/bpmlib/net.go @@ -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