diff --git a/context/context.go b/context/context.go index 11436873..eae6f1b5 100644 --- a/context/context.go +++ b/context/context.go @@ -400,7 +400,7 @@ func (context *AptlyContext) GetSigner() pgp.Signer { defer context.Unlock() if context.pgpProvider() == "gpg" { // nolint: goconst - return &pgp.GpgSigner{} + return pgp.NewGpgSigner() } return &pgp.GoSigner{} @@ -412,7 +412,7 @@ func (context *AptlyContext) GetVerifier() pgp.Verifier { defer context.Unlock() if context.pgpProvider() == "gpg" { // nolint: goconst - return &pgp.GpgVerifier{} + return pgp.NewGpgVerifier() } return &pgp.GoVerifier{} diff --git a/pgp/gnupg.go b/pgp/gnupg.go index 674d318f..4f576193 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,53 @@ 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") +} + +// NewGpgSigner creates a new gpg signer +func NewGpgSigner() *GpgSigner { + gpg, err := findGPG1() + if err != nil { + panic(err) + } + return &GpgSigner{gpg: gpg} +} + // 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() + 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 +144,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 +157,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 +166,43 @@ 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 } +// NewGpgVerifier creates a new gpg signer +func NewGpgVerifier() *GpgVerifier { + gpg, err := findGPG1() + if err != nil { + panic(err) + } + + gpgv, err := findGPGV1() + if err != nil { + panic(err) + } + + return &GpgVerifier{gpg: gpg, gpgv: gpgv} +} + // 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 +233,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 +396,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..670ab114 --- /dev/null +++ b/pgp/gnupg_test.go @@ -0,0 +1,79 @@ +package pgp + +import ( + "os" + "path/filepath" + "runtime" + + . "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 := NewGpgSigner() + c.Assert(signer.gpg, Equals, "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 := NewGpgSigner() + c.Assert(signer.gpg, Equals, "gpg1") +} + +// 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) }() + + c.Assert(func() { NewGpgSigner() }, PanicMatches, `Couldn't find a suitable gpg executable.+`) +} + +// 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 := NewGpgVerifier() + c.Assert(verifier.gpgv, Equals, "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 := NewGpgVerifier() + c.Assert(verifier.gpgv, Equals, "gpgv1") +} + +// 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) }() + + c.Assert(func() { NewGpgVerifier() }, PanicMatches, `Couldn't find a suitable gpgv executable.+`) +} 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