diff --git a/pgp/gnupg.go b/pgp/gnupg.go index 674d318f..3afe61a3 100644 --- a/pgp/gnupg.go +++ b/pgp/gnupg.go @@ -20,6 +20,7 @@ var ( // GpgSigner is implementation of Signer interface using gpg as external program type GpgSigner struct { + gpg string keyRef string keyring, secretKeyring string passphrase, passphraseFile string @@ -78,9 +79,50 @@ func (g *GpgSigner) gpgArgs() []string { return args } +func cliVersionCheck(cmd string, marker string) bool { + output, err := exec.Command(cmd, "--version").CombinedOutput() + if err != nil { + return false + } + return strings.Contains(string(output), marker) +} + +func findSuitableCLI(cmds []string, versionMarker string) string { + for _, cmd := range cmds { + if cliVersionCheck(cmd, versionMarker) { + return cmd + } + } + return "" +} + +// We only support gpg1 at this time. Make sure we find a suitable binary. +func findGPG1() (string, error) { + cmd := findSuitableCLI([]string{"gpg", "gpg1"}, "gpg (GnuPG) 1.") + if cmd != "" { + return cmd, nil + } + return "", fmt.Errorf("Couldn't find a suitable gpg executable. Make sure gnupg1 is available as either gpg or gpg1 in $PATH") +} + +// We only support gpgv1 at this time. Make sure we find a suitable binary. +func findGPGV1() (string, error) { + cmd := findSuitableCLI([]string{"gpgv", "gpgv1"}, "gpgv (GnuPG) 1.") + if cmd != "" { + return cmd, nil + } + return "", fmt.Errorf("Couldn't find a suitable gpgv executable. Make sure gpgv1 is available as either gpgv or gpgv1 in $PATH") +} + // Init verifies availability of gpg & presence of keys func (g *GpgSigner) Init() error { - output, err := exec.Command("gpg", "--list-keys", "--dry-run", "--no-auto-check-trustdb").CombinedOutput() + cmd, err := findGPG1() + if err != nil { + return err + } + g.gpg = cmd + + output, err := exec.Command(g.gpg, "--list-keys", "--dry-run", "--no-auto-check-trustdb").CombinedOutput() if err != nil { return fmt.Errorf("unable to execute gpg: %s (is gpg installed?): %s", err, string(output)) } @@ -99,7 +141,7 @@ func (g *GpgSigner) DetachedSign(source string, destination string) error { args := []string{"-o", destination, "--digest-algo", "SHA256", "--armor", "--yes"} args = append(args, g.gpgArgs()...) args = append(args, "--detach-sign", source) - cmd := exec.Command("gpg", args...) + cmd := exec.Command(g.gpg, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -112,7 +154,7 @@ func (g *GpgSigner) ClearSign(source string, destination string) error { args := []string{"-o", destination, "--digest-algo", "SHA256", "--yes"} args = append(args, g.gpgArgs()...) args = append(args, "--clearsign", source) - cmd := exec.Command("gpg", args...) + cmd := exec.Command(g.gpg, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -121,19 +163,28 @@ func (g *GpgSigner) ClearSign(source string, destination string) error { // GpgVerifier is implementation of Verifier interface using gpgv as external program type GpgVerifier struct { + gpg string + gpgv string keyRings []string } // InitKeyring verifies that gpg is installed and some keys are trusted func (g *GpgVerifier) InitKeyring() error { - err := exec.Command("gpgv", "--version").Run() + cmd, err := findGPG1() if err != nil { - return fmt.Errorf("unable to execute gpgv: %s (is gpg installed?)", err) + return err } + g.gpg = cmd + + cmd, err = findGPGV1() + if err != nil { + return err + } + g.gpgv = cmd if len(g.keyRings) == 0 { // using default keyring - output, err := exec.Command("gpg", "--no-default-keyring", "--no-auto-check-trustdb", "--keyring", "trustedkeys.gpg", "--list-keys").Output() + output, err := exec.Command(g.gpg, "--no-default-keyring", "--no-auto-check-trustdb", "--keyring", "trustedkeys.gpg", "--list-keys").Output() if err == nil && len(output) == 0 { fmt.Printf("\nLooks like your keyring with trusted keys is empty. You might consider importing some keys.\n") fmt.Printf("If you're running Debian or Ubuntu, it's a good idea to import current archive keys by running:\n\n") @@ -164,7 +215,7 @@ func (g *GpgVerifier) argsKeyrings() (args []string) { func (g *GpgVerifier) runGpgv(args []string, context string, showKeyTip bool) (*KeyInfo, error) { args = append([]string{"--status-fd", "3"}, args...) - cmd := exec.Command("gpgv", args...) + cmd := exec.Command(g.gpgv, args...) tempf, err := ioutil.TempFile("", "aptly-gpg-status") if err != nil { @@ -327,7 +378,7 @@ func (g *GpgVerifier) ExtractClearsigned(clearsigned io.Reader) (text *os.File, args := []string{"--no-auto-check-trustdb", "--decrypt", "--batch", "--skip-verify", "--output", "-", clearf.Name()} - cmd := exec.Command("gpg", args...) + cmd := exec.Command(g.gpg, args...) stdout, err := cmd.StdoutPipe() if err != nil { return nil, err diff --git a/pgp/gnupg_test.go b/pgp/gnupg_test.go new file mode 100644 index 00000000..5e114d9b --- /dev/null +++ b/pgp/gnupg_test.go @@ -0,0 +1,88 @@ +package pgp + +import ( + "os" + "path/filepath" + "runtime" + + "github.com/stretchr/testify/assert" + . "gopkg.in/check.v1" +) + +type GnupgSuite struct { + verifier Verifier + bins string +} + +var _ = Suite(&GnupgSuite{}) + +func (s *GnupgSuite) SetUpSuite(c *C) { + _, _File, _, _ := runtime.Caller(0) + s.bins = filepath.Join(filepath.Dir(_File), "test-bins") +} + +// If gpg == gpg1 = pick gpg +func (s *GnupgSuite) TestGPG1(c *C) { + origPath := os.Getenv("PATH") + os.Setenv("PATH", filepath.Join(s.bins, "gpg1")) + defer func() { os.Setenv("PATH", origPath) }() + + signer := GpgSigner{} + assert.NoError(c, signer.Init()) + assert.Equal(c, "gpg", signer.gpg) +} + +// gpg(2) + gpg1 installed = pick gpg1 +func (s *GnupgSuite) TestGPG1Not2(c *C) { + origPath := os.Getenv("PATH") + os.Setenv("PATH", filepath.Join(s.bins, "gpg2-and-1")) + defer func() { os.Setenv("PATH", origPath) }() + + signer := GpgSigner{} + assert.NoError(c, signer.Init()) + assert.Equal(c, "gpg1", signer.gpg) +} + +// If gpg == gpg2 and no gpg1 is available = error +func (s *GnupgSuite) TestGPGNothing(c *C) { + origPath := os.Getenv("PATH") + os.Setenv("PATH", filepath.Join(s.bins, "gpg2-only")) + defer func() { os.Setenv("PATH", origPath) }() + + signer := GpgSigner{} + assert.Error(c, signer.Init()) + assert.Equal(c, "", signer.gpg) +} + +// If gpgv == gpgv1 = pick gpgv +func (s *GnupgSuite) TestGPGV1(c *C) { + origPath := os.Getenv("PATH") + os.Setenv("PATH", filepath.Join(s.bins, "gpgv1")+":"+filepath.Join(s.bins, "gpg1")) + defer func() { os.Setenv("PATH", origPath) }() + + verifier := GpgVerifier{} + assert.NoError(c, verifier.InitKeyring()) + assert.Equal(c, "gpgv", verifier.gpgv) +} + +// gpgv(2) + gpgv1 installed = pick gpgv1 +func (s *GnupgSuite) TestGPGV1Not2(c *C) { + origPath := os.Getenv("PATH") + os.Setenv("PATH", filepath.Join(s.bins, "gpgv2-and-1")+":"+filepath.Join(s.bins, "gpg1")) + defer func() { os.Setenv("PATH", origPath) }() + + verifier := GpgVerifier{} + assert.NoError(c, verifier.InitKeyring()) + assert.Equal(c, "gpgv1", verifier.gpgv) +} + +// If gpgv == gpgv2 and no gpgv1 is available = error +func (s *GnupgSuite) TestGPGVNothing(c *C) { + origPath := os.Getenv("PATH") + os.Setenv("PATH", filepath.Join(s.bins, "gpgv2-only")+":"+filepath.Join(s.bins, "gpg1")) + defer func() { os.Setenv("PATH", origPath) }() + + verifier := GpgVerifier{} + assert.Error(c, verifier.InitKeyring()) + assert.Equal(c, "", verifier.gpgv) +} diff --git a/pgp/test-bins/gpg1/gpg b/pgp/test-bins/gpg1/gpg new file mode 120000 index 00000000..1dac9f1a --- /dev/null +++ b/pgp/test-bins/gpg1/gpg @@ -0,0 +1 @@ +../gpg2-and-1/gpg1 \ No newline at end of file diff --git a/pgp/test-bins/gpg2-and-1/gpg b/pgp/test-bins/gpg2-and-1/gpg new file mode 100755 index 00000000..a5f21a77 --- /dev/null +++ b/pgp/test-bins/gpg2-and-1/gpg @@ -0,0 +1,30 @@ +#!/bin/sh + +while [ "$1" != "" ]; do + case $1 in + --version) + /bin/cat <<'OUTPUT' +gpg (GnuPG) 2.2.4 +libgcrypt 1.8.1 +Copyright (C) 2017 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. + +Home: /root/.gnupg +Supported algorithms: +Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA +Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH, + CAMELLIA128, CAMELLIA192, CAMELLIA256 +Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224 +Compression: Uncompressed, ZIP, ZLIB, BZIP2 +OUTPUT + ;; + -?*) + echo "Unknown option: $1" + ;; + *) + break + esac + shift +done diff --git a/pgp/test-bins/gpg2-and-1/gpg1 b/pgp/test-bins/gpg2-and-1/gpg1 new file mode 100755 index 00000000..4a142cb1 --- /dev/null +++ b/pgp/test-bins/gpg2-and-1/gpg1 @@ -0,0 +1,29 @@ +#!/bin/sh + +while [ "$1" != "" ]; do + case $1 in + --version) + /bin/cat <<'OUTPUT' +gpg (GnuPG) 1.4.22 +Copyright (C) 2015 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. + +Home: ~/.gnupg +Supported algorithms: +Pubkey: RSA, RSA-E, RSA-S, ELG-E, DSA +Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH, + CAMELLIA128, CAMELLIA192, CAMELLIA256 +Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224 +Compression: Uncompressed, ZIP, ZLIB, BZIP2 +OUTPUT + ;; + -?*) + echo "Unknown option: $1" + ;; + *) + break + esac + shift +done diff --git a/pgp/test-bins/gpg2-only/gpg b/pgp/test-bins/gpg2-only/gpg new file mode 120000 index 00000000..edac3458 --- /dev/null +++ b/pgp/test-bins/gpg2-only/gpg @@ -0,0 +1 @@ +../gpg2-and-1/gpg \ No newline at end of file diff --git a/pgp/test-bins/gpgv1/gpgv b/pgp/test-bins/gpgv1/gpgv new file mode 120000 index 00000000..6b421475 --- /dev/null +++ b/pgp/test-bins/gpgv1/gpgv @@ -0,0 +1 @@ +../gpgv2-and-1/gpgv1 \ No newline at end of file diff --git a/pgp/test-bins/gpgv2-and-1/gpgv b/pgp/test-bins/gpgv2-and-1/gpgv new file mode 100755 index 00000000..17149142 --- /dev/null +++ b/pgp/test-bins/gpgv2-and-1/gpgv @@ -0,0 +1,22 @@ +#!/bin/sh + +while [ "$1" != "" ]; do + case $1 in + --version) + /bin/cat <<'OUTPUT' +gpgv (GnuPG) 2.2.4 +libgcrypt 1.8.1 +Copyright (C) 2017 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +OUTPUT + ;; + -?*) + echo "Unknown option: $1" + ;; + *) + break + esac + shift +done diff --git a/pgp/test-bins/gpgv2-and-1/gpgv1 b/pgp/test-bins/gpgv2-and-1/gpgv1 new file mode 100755 index 00000000..18f48752 --- /dev/null +++ b/pgp/test-bins/gpgv2-and-1/gpgv1 @@ -0,0 +1,21 @@ +#!/bin/sh + +while [ "$1" != "" ]; do + case $1 in + --version) + /bin/cat <<'OUTPUT' +gpgv (GnuPG) 1.4.22 +Copyright (C) 2015 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. +OUTPUT + ;; + -?*) + echo "Unknown option: $1" + ;; + *) + break + esac + shift +done diff --git a/pgp/test-bins/gpgv2-only/gpgv b/pgp/test-bins/gpgv2-only/gpgv new file mode 120000 index 00000000..6d597975 --- /dev/null +++ b/pgp/test-bins/gpgv2-only/gpgv @@ -0,0 +1 @@ +../gpgv2-and-1/gpgv \ No newline at end of file