From 6f25da860c4540b193e64be1e3bc9035a76f0f5b Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Sat, 10 Mar 2018 22:00:45 -0500 Subject: [PATCH] Ask if yay should try to import missing PGP keys When building a package from the AUR for which there are missing keys, yay will now prompt the user whether it should try to import such keys using gpg: [...] :: Parsing SRCINFO (1/3): libc++ (libc++abi libc++) :: Parsing SRCINFO (2/3): aurutils :: Parsing SRCINFO (3/3): cower ==> GPG keys need importing: 487EACC08557AD082088DABA1EB2638FF56C0C53, required by: cower 11E521D646982372EB577A1F8F0871F202119294, required by: libc++ (libc++abi libc++) B6C8F98282B944E3B0D5C2530FC3042E345AD05D, required by: libc++ (libc++abi libc++) DBE7D3DD8C81D58D0A13D0E76BC26A17B9B7018A, required by: aurutils ==> Import? [Y/n] [...] Default is to try to import the problematic keys ([Y/n]). --- config.go | 2 + install.go | 9 +- keys.go | 121 +++++++++++++++++++++++ keys_test.go | 268 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 keys.go create mode 100644 keys_test.go diff --git a/config.go b/config.go index 52e961b4..d6aeac08 100644 --- a/config.go +++ b/config.go @@ -33,6 +33,7 @@ type Configuration struct { TarBin string `json:"tarbin"` ReDownload string `json:"redownload"` GitBin string `json:"gitbin"` + GpgBin string `json:"gpgbin"` MFlags string `json:"mflags"` RequestSplitN int `json:"requestsplitn"` SearchMode int `json:"-"` @@ -130,6 +131,7 @@ func defaultSettings(config *Configuration) { config.SudoLoop = false config.TarBin = "bsdtar" config.GitBin = "git" + config.GpgBin = "gpg" config.TimeUpdate = false config.RequestSplitN = 150 config.ReDownload = "no" diff --git a/install.go b/install.go index a2a18b65..6cae5a45 100644 --- a/install.go +++ b/install.go @@ -200,12 +200,17 @@ func install(parser *arguments) error { return nil } - err = downloadPkgBuildsSources(dc.Aur, dc.Bases) + err = parsesrcinfosGenerate(dc.Aur, srcinfos, dc.Bases) if err != nil { return err } - err = parsesrcinfosGenerate(dc.Aur, srcinfos, dc.Bases) + err = checkPgpKeys(dc.Aur, srcinfos, dc.Bases, nil) + if err != nil { + return err + } + + err = downloadPkgBuildsSources(dc.Aur, dc.Bases) if err != nil { return err } diff --git a/keys.go b/keys.go new file mode 100644 index 00000000..30840bb2 --- /dev/null +++ b/keys.go @@ -0,0 +1,121 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strings" + + rpc "github.com/mikkeloscar/aur" + gopkg "github.com/mikkeloscar/gopkgbuild" +) + +// pgpKeySet maps a PGP key with a list of PKGBUILDs that require it. +// This is similar to stringSet, used throughout the code. +type pgpKeySet map[string][]*rpc.Pkg + +func (set pgpKeySet) toSlice() []string { + slice := make([]string, 0, len(set)) + for v := range set { + slice = append(slice, v) + } + return slice +} + +func (set pgpKeySet) set(key string, p *rpc.Pkg) { + // Using ToUpper to make sure keys with a different case will be + // considered the same. + upperKey := strings.ToUpper(key) + if _, exists := set[upperKey]; !exists { + set[upperKey] = []*rpc.Pkg{} + } + set[key] = append(set[key], p) +} + +func (set pgpKeySet) get(key string) bool { + upperKey := strings.ToUpper(key) + _, exists := set[upperKey] + return exists +} + +// checkPgpKeys iterates through the keys listed in the PKGBUILDs and if needed, +// asks the user whether yay should try to import them. gpgExtraArgs are extra +// parameters to pass to gpg, in order to facilitate testing, such as using a +// different keyring. It can be nil. +func checkPgpKeys(pkgs []*rpc.Pkg, srcinfos map[string]*gopkg.PKGBUILD, bases map[string][]*rpc.Pkg, gpgExtraArgs []string) error { + // Let's check the keys individually, and then we can offer to import + // the problematic ones. + problematic := make(pgpKeySet) + args := append(gpgExtraArgs, "--list-keys") + + // Mapping all the keys. + for _, pkg := range pkgs { + for _, key := range srcinfos[pkg.PackageBase].Validpgpkeys { + // If key already marked as problematic, indicate the current + // PKGBUILD requires it. + if problematic.get(key) { + problematic.set(key, pkg) + continue + } + + cmd := exec.Command(config.GpgBin, append(args, key)...) + err := cmd.Run() + if err != nil { + problematic.set(key, pkg) + } + } + } + + // No key issues! + if len(problematic) == 0 { + return nil + } + + question, err := formatKeysToImport(problematic, bases) + if err != nil { + return err + } + if continueTask(question, "nN") { + return importKeys(gpgExtraArgs, problematic.toSlice()) + } + + return nil +} + +// importKeys tries to import the list of keys specified in its argument. As +// in checkGpgKeys, gpgExtraArgs are extra parameters to pass to gpg. +func importKeys(gpgExtraArgs, keys []string) error { + args := append(gpgExtraArgs, "--recv-keys") + cmd := exec.Command(config.GpgBin, append(args, keys...)...) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + + fmt.Printf("%s Importing keys with gpg...\n", bold(cyan("::"))) + err := cmd.Run() + + if err != nil { + return fmt.Errorf("%s Problem importing keys", bold(red(arrow+" Error:"))) + } + return nil +} + +// formatKeysToImport receives a set of keys and returns a string containing the +// question asking the user wants to import the problematic keys. +func formatKeysToImport(keys pgpKeySet, bases map[string][]*rpc.Pkg) (string, error) { + if len(keys) == 0 { + return "", fmt.Errorf("%s No keys to import", bold(red(arrow+" Error:"))) + } + + var buffer bytes.Buffer + buffer.WriteString(bold(green(("GPG keys need importing:\n")))) + for key, pkgs := range keys { + pkglist := "" + for _, pkg := range pkgs { + pkglist += formatPkgbase(pkg, bases) + " " + } + pkglist = strings.TrimRight(pkglist, " ") + buffer.WriteString(fmt.Sprintf("\t%s, required by: %s\n", green(key), cyan(pkglist))) + } + buffer.WriteString(bold(green(fmt.Sprintf("%s Import?", arrow)))) + return buffer.String(), nil +} diff --git a/keys_test.go b/keys_test.go new file mode 100644 index 00000000..9b173da6 --- /dev/null +++ b/keys_test.go @@ -0,0 +1,268 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + + rpc "github.com/mikkeloscar/aur" + gopkg "github.com/mikkeloscar/gopkgbuild" +) + +func newPkg(basename string) *rpc.Pkg { + return &rpc.Pkg{Name: basename, PackageBase: basename} +} + +func newSplitPkg(basename, name string) *rpc.Pkg { + return &rpc.Pkg{Name: name, PackageBase: basename} +} + +func initTestKeyring() (string, error) { + config.GpgBin = "gpg" + tmpdir, err := ioutil.TempDir("/tmp", "yay-test-keyring") + if err != nil { + return "", err + } + return tmpdir, nil +} + +func TestFormatKeysToImport(t *testing.T) { + casetests := []struct { + keySet pgpKeySet + bases map[string][]*rpc.Pkg + expected string + alternate string + wantError bool + }{ + // Single key, required by single package. + { + keySet: pgpKeySet{"KEY-1": []*rpc.Pkg{newPkg("PKG-foo")}}, + expected: fmt.Sprintf("GPG keys need importing:\n\tKEY-1, required by: PKG-foo\n%s Import?", arrow), + wantError: false, + }, + // Single key, required by two packages. + { + keySet: pgpKeySet{"KEY-1": []*rpc.Pkg{newPkg("PKG-foo"), newPkg("PKG-bar")}}, + expected: fmt.Sprintf("GPG keys need importing:\n\tKEY-1, required by: PKG-foo PKG-bar\n%s Import?", arrow), + wantError: false, + }, + // Two keys, each required by a single package. Since iterating the map + // does not force any particular order, we cannot really predict the + // order in which the elements will appear. As we have only two cases, + // let's add the second possibility to the alternate variable, to check + // if there are any errors. + { + keySet: pgpKeySet{"KEY-1": []*rpc.Pkg{newPkg("PKG-foo")}, "KEY-2": []*rpc.Pkg{newPkg("PKG-bar")}}, + expected: fmt.Sprintf("GPG keys need importing:\n\tKEY-1, required by: PKG-foo\n\tKEY-2, required by: PKG-bar\n%s Import?", arrow), + alternate: fmt.Sprintf("GPG keys need importing:\n\tKEY-2, required by: PKG-bar\n\tKEY-1, required by: PKG-foo\n%s Import?", arrow), + wantError: false, + }, + // Two keys required by single package. + { + keySet: pgpKeySet{"KEY-1": []*rpc.Pkg{newPkg("PKG-foo")}, "KEY-2": []*rpc.Pkg{newPkg("PKG-foo")}}, + expected: fmt.Sprintf("GPG keys need importing:\n\tKEY-1, required by: PKG-foo\n\tKEY-2, required by: PKG-foo\n%s Import?", arrow), + alternate: fmt.Sprintf("GPG keys need importing:\n\tKEY-2, required by: PKG-foo\n\tKEY-1, required by: PKG-foo\n%s Import?", arrow), + wantError: false, + }, + // Two keys, one of them required by two packages. + { + keySet: pgpKeySet{"KEY-1": []*rpc.Pkg{newPkg("PKG-foo"), newPkg("PKG-bar")}, "KEY-2": []*rpc.Pkg{newPkg("PKG-bar")}}, + expected: fmt.Sprintf("GPG keys need importing:\n\tKEY-1, required by: PKG-foo PKG-bar\n\tKEY-2, required by: PKG-bar\n%s Import?", arrow), + alternate: fmt.Sprintf("GPG keys need importing:\n\tKEY-2, required by: PKG-bar\n\tKEY-1, required by: PKG-foo PKG-bar\n%s Import?", arrow), + wantError: false, + }, + // Two keys, split package (linux-ck/linux-ck-headers). + { + keySet: pgpKeySet{"ABAF11C65A2970B130ABE3C479BE3E4300411886": []*rpc.Pkg{newPkg("linux-ck")}, "647F28654894E3BD457199BE38DBBDC86092693E": []*rpc.Pkg{newPkg("linux-ck")}}, + + bases: map[string][]*rpc.Pkg{"linux-ck": {newSplitPkg("linux-ck", "linux-ck-headers"), newPkg("linux-ck")}}, + expected: fmt.Sprintf("GPG keys need importing:\n\tABAF11C65A2970B130ABE3C479BE3E4300411886, required by: linux-ck (linux-ck-headers linux-ck)\n\t647F28654894E3BD457199BE38DBBDC86092693E, required by: linux-ck (linux-ck-headers linux-ck)\n%s Import?", arrow), + alternate: fmt.Sprintf("GPG keys need importing:\n\t647F28654894E3BD457199BE38DBBDC86092693E, required by: linux-ck (linux-ck-headers linux-ck)\n\tABAF11C65A2970B130ABE3C479BE3E4300411886, required by: linux-ck (linux-ck-headers linux-ck)\n%s Import?", arrow), + wantError: false, + }, + // One key, three split packages. + { + keySet: pgpKeySet{"KEY-1": []*rpc.Pkg{newPkg("PKG-foo")}}, + bases: map[string][]*rpc.Pkg{"PKG-foo": {newPkg("PKG-foo"), newSplitPkg("PKG-foo", "PKG-foo-1"), newSplitPkg("PKG-foo", "PKG-foo-2")}}, + expected: fmt.Sprintf("GPG keys need importing:\n\tKEY-1, required by: PKG-foo (PKG-foo PKG-foo-1 PKG-foo-2)\n%s Import?", arrow), + wantError: false, + }, + // No keys, should fail. + { + keySet: pgpKeySet{}, + expected: "", + wantError: true, + }, + } + + for _, tt := range casetests { + question, err := formatKeysToImport(tt.keySet, tt.bases) + if !tt.wantError { + if err != nil { + t.Fatalf("Got error %q, want no error", err) + } + + if question != tt.expected && question != tt.alternate { + t.Fatalf("Got %q\n, expected: %q", question, tt.expected) + } + continue + } + // Here, we want to see the error. + if err == nil { + t.Fatalf("Got no error; want error") + } + } +} + +func TestImportKeys(t *testing.T) { + keyring, err := initTestKeyring() + if err != nil { + t.Fatalf("Unable to init test keyring %q: %v\n", keyring, err) + } + + // Removing the leftovers. + defer os.RemoveAll(keyring) + keyringArgs := []string{"--homedir", keyring} + + casetests := []struct { + keys []string + wantError bool + }{ + // Single key, should succeed. + // C52048C0C0748FEE227D47A2702353E0F7E48EDB: Thomas Dickey. + { + keys: []string{"C52048C0C0748FEE227D47A2702353E0F7E48EDB"}, + wantError: false, + }, + // Two keys, should succeed as well. + // 11E521D646982372EB577A1F8F0871F202119294: Tom Stellard. + // B6C8F98282B944E3B0D5C2530FC3042E345AD05D: Hans Wennborg. + { + keys: []string{"11E521D646982372EB577A1F8F0871F202119294", + "B6C8F98282B944E3B0D5C2530FC3042E345AD05D"}, + wantError: false, + }, + // Single invalid key, should fail. + { + keys: []string{"THIS-SHOULD-FAIL"}, + wantError: true, + }, + // Two invalid keys, should fail. + { + keys: []string{"THIS-SHOULD-FAIL", "THIS-ONE-SHOULD-FAIL-TOO"}, + wantError: true, + }, + // Invalid + valid key. Should fail as well. + // 647F28654894E3BD457199BE38DBBDC86092693E: Greg Kroah-Hartman. + { + keys: []string{"THIS-SHOULD-FAIL", + "647F28654894E3BD457199BE38DBBDC86092693E"}, + wantError: true, + }, + } + + for _, tt := range casetests { + err := importKeys(keyringArgs, tt.keys) + if !tt.wantError { + if err != nil { + t.Fatalf("Got error %q, want no error", err) + } + continue + } + // Here, we want to see the error. + if err == nil { + t.Fatalf("Got no error; want error") + } + } +} + +func TestCheckPgpKeys(t *testing.T) { + keyring, err := initTestKeyring() + if err != nil { + t.Fatalf("Unable to init test keyring %q: %v\n", keyring, err) + } + + // Removing the leftovers. + defer os.RemoveAll(keyring) + keyringArgs := []string{"--homedir", keyring} + + casetests := []struct { + pkgs []*rpc.Pkg + srcinfos map[string]*gopkg.PKGBUILD + bases map[string][]*rpc.Pkg + wantError bool + }{ + // cower: single package, one valid key not yet in the keyring. + // 487EACC08557AD082088DABA1EB2638FF56C0C53: Dave Reisner. + { + pkgs: []*rpc.Pkg{newPkg("cower")}, + srcinfos: map[string]*gopkg.PKGBUILD{"cower": &gopkg.PKGBUILD{Pkgbase: "cower", Validpgpkeys: []string{"487EACC08557AD082088DABA1EB2638FF56C0C53"}}}, + bases: map[string][]*rpc.Pkg{"cower": {newPkg("cower")}}, + wantError: false, + }, + // libc++: single package, two valid keys not yet in the keyring. + // 11E521D646982372EB577A1F8F0871F202119294: Tom Stellard. + // B6C8F98282B944E3B0D5C2530FC3042E345AD05D: Hans Wennborg. + { + pkgs: []*rpc.Pkg{newPkg("libc++")}, + srcinfos: map[string]*gopkg.PKGBUILD{"libc++": &gopkg.PKGBUILD{Pkgbase: "libc++", Validpgpkeys: []string{"11E521D646982372EB577A1F8F0871F202119294", "B6C8F98282B944E3B0D5C2530FC3042E345AD05D"}}}, + bases: map[string][]*rpc.Pkg{"libc++": {newPkg("libc++")}}, + wantError: false, + }, + // Two dummy packages requiring the same key. + // ABAF11C65A2970B130ABE3C479BE3E4300411886: Linus Torvalds. + { + pkgs: []*rpc.Pkg{newPkg("dummy-1"), newPkg("dummy-2")}, + srcinfos: map[string]*gopkg.PKGBUILD{"dummy-1": &gopkg.PKGBUILD{Pkgbase: "dummy-1", Validpgpkeys: []string{"ABAF11C65A2970B130ABE3C479BE3E4300411886"}}, "dummy-2": &gopkg.PKGBUILD{Pkgbase: "dummy-2", Validpgpkeys: []string{"ABAF11C65A2970B130ABE3C479BE3E4300411886"}}}, + bases: map[string][]*rpc.Pkg{"dummy-1": {newPkg("dummy-1")}, "dummy-2": {newPkg("dummy-2")}}, + wantError: false, + }, + // dummy package: single package, two valid keys, one of them already + // in the keyring. + // 11E521D646982372EB577A1F8F0871F202119294: Tom Stellard. + // C52048C0C0748FEE227D47A2702353E0F7E48EDB: Thomas Dickey. + { + pkgs: []*rpc.Pkg{newPkg("dummy-3")}, + srcinfos: map[string]*gopkg.PKGBUILD{"dummy-3": &gopkg.PKGBUILD{Pkgbase: "dummy-3", Validpgpkeys: []string{"11E521D646982372EB577A1F8F0871F202119294", "C52048C0C0748FEE227D47A2702353E0F7E48EDB"}}}, + bases: map[string][]*rpc.Pkg{"dummy-3": {newPkg("dummy-3")}}, + wantError: false, + }, + // Two dummy packages with existing keys. + { + pkgs: []*rpc.Pkg{newPkg("dummy-4"), newPkg("dummy-5")}, + srcinfos: map[string]*gopkg.PKGBUILD{"dummy-4": &gopkg.PKGBUILD{Pkgbase: "dummy-4", Validpgpkeys: []string{"11E521D646982372EB577A1F8F0871F202119294"}}, "dummy-5": &gopkg.PKGBUILD{Pkgbase: "dummy-5", Validpgpkeys: []string{"C52048C0C0748FEE227D47A2702353E0F7E48EDB"}}}, + bases: map[string][]*rpc.Pkg{"dummy-4": {newPkg("dummy-4")}, "dummy-5": {newPkg("dummy-5")}}, + wantError: false, + }, + // Dummy package with invalid key, should fail. + { + pkgs: []*rpc.Pkg{newPkg("dummy-7")}, + srcinfos: map[string]*gopkg.PKGBUILD{"dummy-7": &gopkg.PKGBUILD{Pkgbase: "dummy-7", Validpgpkeys: []string{"THIS-SHOULD-FAIL"}}}, + bases: map[string][]*rpc.Pkg{"dummy-7": {newPkg("dummy-7")}}, + wantError: true, + }, + // Dummy package with both an invalid an another valid key, should fail. + // A314827C4E4250A204CE6E13284FC34C8E4B1A25: Thomas Bächler. + { + pkgs: []*rpc.Pkg{newPkg("dummy-8")}, + srcinfos: map[string]*gopkg.PKGBUILD{"dummy-8": &gopkg.PKGBUILD{Pkgbase: "dummy-8", Validpgpkeys: []string{"A314827C4E4250A204CE6E13284FC34C8E4B1A25", "THIS-SHOULD-FAIL"}}}, + bases: map[string][]*rpc.Pkg{"dummy-8": {newPkg("dummy-8")}}, + wantError: true, + }, + } + + for _, tt := range casetests { + err := checkPgpKeys(tt.pkgs, tt.srcinfos, tt.bases, keyringArgs) + if !tt.wantError { + if err != nil { + t.Fatalf("Got error %q, want no error", err) + } + continue + } + // Here, we want to see the error. + if err == nil { + t.Fatalf("Got no error; want error") + } + } +}