瀏覽代碼

Merge pull request #179 from qrwteyrutiyoup/import-keys

Ask if yay should try to import missing PGP keys
Morgana 7 年之前
父節點
當前提交
e6c47bee32
共有 4 個文件被更改,包括 398 次插入2 次删除
  1. 2 0
      config.go
  2. 7 2
      install.go
  3. 121 0
      keys.go
  4. 268 0
      keys_test.go

+ 2 - 0
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"

+ 7 - 2
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
 		}

+ 121 - 0
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
+}

+ 268 - 0
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")
+		}
+	}
+}