Refactor GPG signer/verifier

Goal is to make it easier to plug in another implementation.
pull/575/head
Andrey Smirnov 2017-05-23 02:54:56 +03:00
parent c026106352
commit 1be8d39105
21 changed files with 135 additions and 110 deletions

View File

@ -1,6 +1,6 @@
GOVERSION=$(shell go version | awk '{print $$3;}')
VERSION=$(shell git describe --tags | sed 's@^v@@' | sed 's@-@+@g')
PACKAGES=context database deb files http query swift s3 utils
PACKAGES=context database deb files gpg http query swift s3 utils
PYTHON?=python
TESTS?=
BINPATH?=$(GOPATH)/bin

View File

@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/smira/aptly/deb"
"github.com/smira/aptly/pgp"
"github.com/smira/aptly/utils"
)
@ -20,12 +21,12 @@ type SigningOptions struct {
PassphraseFile string
}
func getSigner(options *SigningOptions) (utils.Signer, error) {
func getSigner(options *SigningOptions) (pgp.Signer, error) {
if options.Skip {
return nil, nil
}
signer := &utils.GpgSigner{}
signer := &pgp.GpgSigner{}
signer.SetKey(options.GpgKey)
signer.SetKeyRing(options.Keyring, options.SecretKeyring)
signer.SetPassphrase(options.Passphrase, options.PassphraseFile)

View File

@ -9,6 +9,7 @@ import (
"github.com/smira/aptly/aptly"
"github.com/smira/aptly/database"
"github.com/smira/aptly/deb"
"github.com/smira/aptly/pgp"
"github.com/smira/aptly/utils"
)
@ -296,7 +297,7 @@ func apiReposPackageFromDir(c *gin.Context) {
return
}
verifier := &utils.GpgVerifier{}
verifier := &pgp.GpgVerifier{}
var (
sources []string

View File

@ -3,19 +3,19 @@ package cmd
import (
"strings"
"github.com/smira/aptly/utils"
"github.com/smira/aptly/pgp"
"github.com/smira/commander"
"github.com/smira/flag"
)
func getVerifier(flags *flag.FlagSet) (utils.Verifier, error) {
func getVerifier(flags *flag.FlagSet) (pgp.Verifier, error) {
if LookupOption(context.Config().GpgDisableVerify, flags, "ignore-signatures") {
return nil, nil
}
keyRings := flags.Lookup("keyring").Value.Get().([]string)
verifier := &utils.GpgVerifier{}
verifier := &pgp.GpgVerifier{}
for _, keyRing := range keyRings {
verifier.AddKeyring(keyRing)
}

View File

@ -1,17 +1,17 @@
package cmd
import (
"github.com/smira/aptly/utils"
"github.com/smira/aptly/pgp"
"github.com/smira/commander"
"github.com/smira/flag"
)
func getSigner(flags *flag.FlagSet) (utils.Signer, error) {
func getSigner(flags *flag.FlagSet) (pgp.Signer, error) {
if LookupOption(context.Config().GpgDisableSign, flags, "skip-signing") {
return nil, nil
}
signer := &utils.GpgSigner{}
signer := &pgp.GpgSigner{}
signer.SetKey(flags.Lookup("gpg-key").Value.String())
signer.SetKeyRing(flags.Lookup("keyring").Value.String(), flags.Lookup("secret-keyring").Value.String())
signer.SetPassphrase(flags.Lookup("passphrase").Value.String(), flags.Lookup("passphrase-file").Value.String())

View File

@ -6,6 +6,7 @@ import (
"github.com/smira/aptly/aptly"
"github.com/smira/aptly/deb"
"github.com/smira/aptly/pgp"
"github.com/smira/aptly/utils"
"github.com/smira/commander"
"github.com/smira/flag"
@ -20,7 +21,7 @@ func aptlyRepoAdd(cmd *commander.Command, args []string) error {
name := args[0]
verifier := &utils.GpgVerifier{}
verifier := &pgp.GpgVerifier{}
repo, err := context.CollectionFactory().LocalRepoCollection().ByName(name)
if err != nil {

View File

@ -9,6 +9,7 @@ import (
"github.com/smira/aptly/aptly"
"github.com/smira/aptly/deb"
"github.com/smira/aptly/pgp"
"github.com/smira/aptly/query"
"github.com/smira/aptly/utils"
"github.com/smira/commander"
@ -28,7 +29,7 @@ func aptlyRepoInclude(cmd *commander.Command, args []string) error {
}
if verifier == nil {
verifier = &utils.GpgVerifier{}
verifier = &pgp.GpgVerifier{}
}
forceReplace := context.Flags().Lookup("force-replace").Value.Get().(bool)

View File

@ -9,6 +9,7 @@ import (
"strings"
"github.com/smira/aptly/aptly"
"github.com/smira/aptly/pgp"
"github.com/smira/aptly/utils"
)
@ -23,7 +24,7 @@ type Changes struct {
Binary []string
Architectures []string
Stanza Stanza
SignatureKeys []utils.GpgKey
SignatureKeys []pgp.Key
}
// NewChanges moves .changes file into temporary directory and creates Changes structure
@ -50,7 +51,7 @@ func NewChanges(path string) (*Changes, error) {
}
// VerifyAndParse does optional signature verification and parses changes files
func (c *Changes) VerifyAndParse(acceptUnsigned, ignoreSignature bool, verifier utils.Verifier) error {
func (c *Changes) VerifyAndParse(acceptUnsigned, ignoreSignature bool, verifier pgp.Verifier) error {
input, err := os.Open(filepath.Join(c.TempDir, c.ChangesName))
if err != nil {
return err
@ -69,7 +70,7 @@ func (c *Changes) VerifyAndParse(acceptUnsigned, ignoreSignature bool, verifier
}
if isClearSigned && !ignoreSignature {
var keyInfo *utils.GpgKeyInfo
var keyInfo *pgp.KeyInfo
keyInfo, err = verifier.VerifyClearsigned(input, false)
if err != nil {
return err

View File

@ -14,7 +14,7 @@ import (
"github.com/mkrautz/goar"
"github.com/pkg/errors"
"github.com/smira/aptly/utils"
"github.com/smira/aptly/pgp"
"github.com/smira/go-xz"
"github.com/smira/lzma"
)
@ -76,7 +76,7 @@ func GetControlFileFromDeb(packageFile string) (Stanza, error) {
}
// GetControlFileFromDsc reads control file from dsc package
func GetControlFileFromDsc(dscFile string, verifier utils.Verifier) (Stanza, error) {
func GetControlFileFromDsc(dscFile string, verifier pgp.Verifier) (Stanza, error) {
file, err := os.Open(dscFile)
if err != nil {
return nil, err

View File

@ -5,7 +5,7 @@ import (
"path/filepath"
"runtime"
"github.com/smira/aptly/utils"
"github.com/smira/aptly/pgp"
. "gopkg.in/check.v1"
)
@ -39,7 +39,7 @@ func (s *DebSuite) TestGetControlFileFromDeb(c *C) {
}
func (s *DebSuite) TestGetControlFileFromDsc(c *C) {
verifier := &utils.GpgVerifier{}
verifier := &pgp.GpgVerifier{}
_, err := GetControlFileFromDsc("/no/such/file", verifier)
c.Check(err, ErrorMatches, ".*no such file or directory")

View File

@ -7,6 +7,7 @@ import (
"strings"
"github.com/smira/aptly/aptly"
"github.com/smira/aptly/pgp"
"github.com/smira/aptly/utils"
)
@ -59,7 +60,7 @@ func CollectPackageFiles(locations []string, reporter aptly.ResultReporter) (pac
}
// ImportPackageFiles imports files into local repository
func ImportPackageFiles(list *PackageList, packageFiles []string, forceReplace bool, verifier utils.Verifier,
func ImportPackageFiles(list *PackageList, packageFiles []string, forceReplace bool, verifier pgp.Verifier,
pool aptly.PackagePool, collection *PackageCollection, reporter aptly.ResultReporter, restriction PackageQuery,
checksumStorage aptly.ChecksumStorage) (processedFiles []string, failedFiles []string, err error) {
if forceReplace {

View File

@ -8,6 +8,7 @@ import (
"strings"
"github.com/smira/aptly/aptly"
"github.com/smira/aptly/pgp"
"github.com/smira/aptly/utils"
)
@ -48,7 +49,7 @@ func (file *indexFile) BufWriter() (*bufio.Writer, error) {
return file.w, nil
}
func (file *indexFile) Finalize(signer utils.Signer) error {
func (file *indexFile) Finalize(signer pgp.Signer) error {
if file.w == nil {
if file.discardable {
return nil

View File

@ -19,6 +19,7 @@ import (
"github.com/smira/aptly/aptly"
"github.com/smira/aptly/database"
"github.com/smira/aptly/pgp"
"github.com/smira/aptly/utils"
)
@ -447,7 +448,7 @@ func (p *PublishedRepo) GetLabel() string {
// Publish publishes snapshot (repository) contents, links package files, generates Packages & Release files, signs them
func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageProvider aptly.PublishedStorageProvider,
collectionFactory *CollectionFactory, signer utils.Signer, progress aptly.Progress, forceOverwrite bool) error {
collectionFactory *CollectionFactory, signer pgp.Signer, progress aptly.Progress, forceOverwrite bool) error {
publishedStorage := publishedStorageProvider.GetPublishedStorage(p.Storage)
err := publishedStorage.MkDir(filepath.Join(p.Prefix, "pool"))

View File

@ -18,6 +18,7 @@ import (
"github.com/smira/aptly/aptly"
"github.com/smira/aptly/database"
"github.com/smira/aptly/http"
"github.com/smira/aptly/pgp"
"github.com/smira/aptly/utils"
"github.com/smira/go-uuid/uuid"
"github.com/ugorji/go/codec"
@ -244,7 +245,7 @@ func (repo *RemoteRepo) PackageURL(filename string) *url.URL {
}
// Fetch updates information about repository
func (repo *RemoteRepo) Fetch(d aptly.Downloader, verifier utils.Verifier) error {
func (repo *RemoteRepo) Fetch(d aptly.Downloader, verifier pgp.Verifier) error {
var (
release, inrelease, releasesig *os.File
err error

View File

@ -12,6 +12,7 @@ import (
"github.com/smira/aptly/database"
"github.com/smira/aptly/files"
"github.com/smira/aptly/http"
"github.com/smira/aptly/pgp"
"github.com/smira/aptly/utils"
. "gopkg.in/check.v1"
@ -31,7 +32,7 @@ func (n *NullVerifier) VerifyDetachedSignature(signature, cleartext io.Reader) e
return nil
}
func (n *NullVerifier) VerifyClearsigned(clearsigned io.Reader, hint bool) (*utils.GpgKeyInfo, error) {
func (n *NullVerifier) VerifyClearsigned(clearsigned io.Reader, hint bool) (*pgp.KeyInfo, error) {
return nil, nil
}

View File

@ -6,6 +6,7 @@ import (
"os"
"github.com/DisposaBoy/JsonConfigReader"
"github.com/smira/aptly/pgp"
"github.com/smira/aptly/utils"
)
@ -85,7 +86,7 @@ func (u *Uploaders) IsAllowed(changes *Changes) error {
deny := u.ExpandGroups(rule.Deny)
for _, key := range changes.SignatureKeys {
for _, item := range deny {
if item == "*" || key.Matches(utils.GpgKey(item)) {
if item == "*" || key.Matches(pgp.Key(item)) {
return fmt.Errorf("denied according to rule: %s", rule)
}
}
@ -94,7 +95,7 @@ func (u *Uploaders) IsAllowed(changes *Changes) error {
allow := u.ExpandGroups(rule.Allow)
for _, key := range changes.SignatureKeys {
for _, item := range allow {
if item == "*" || key.Matches(utils.GpgKey(item)) {
if item == "*" || key.Matches(pgp.Key(item)) {
return nil
}
}

View File

@ -1,7 +1,7 @@
package deb
import (
"github.com/smira/aptly/utils"
"github.com/smira/aptly/pgp"
. "gopkg.in/check.v1"
)
@ -58,24 +58,24 @@ func (s *UploadersSuite) TestIsAllowed(c *C) {
}
// no keys - not allowed
c.Check(u.IsAllowed(&Changes{SignatureKeys: []utils.GpgKey{}, Stanza: Stanza{"Source": "calamares"}}), ErrorMatches, "denied as no rule matches")
c.Check(u.IsAllowed(&Changes{SignatureKeys: []pgp.Key{}, Stanza: Stanza{"Source": "calamares"}}), ErrorMatches, "denied as no rule matches")
// no rule - not allowed
c.Check(u.IsAllowed(&Changes{SignatureKeys: []utils.GpgKey{"37E1C17570096AD1", "EC4B033C70096AD1"}, Stanza: Stanza{"Source": "unknown-calamares"}}), ErrorMatches, "denied as no rule matches")
c.Check(u.IsAllowed(&Changes{SignatureKeys: []pgp.Key{"37E1C17570096AD1", "EC4B033C70096AD1"}, Stanza: Stanza{"Source": "unknown-calamares"}}), ErrorMatches, "denied as no rule matches")
// first rule: allow anyone do stuff with calamares
c.Check(u.IsAllowed(&Changes{SignatureKeys: []utils.GpgKey{"ABCD1234", "1234ABCD"}, Stanza: Stanza{"Source": "calamares"}}), IsNil)
c.Check(u.IsAllowed(&Changes{SignatureKeys: []pgp.Key{"ABCD1234", "1234ABCD"}, Stanza: Stanza{"Source": "calamares"}}), IsNil)
// second rule: nobody is allowed to do stuff with never-calamares
c.Check(u.IsAllowed(&Changes{SignatureKeys: []utils.GpgKey{"ABCD1234", "1234ABCD"}, Stanza: Stanza{"Source": "never-calamares"}}),
c.Check(u.IsAllowed(&Changes{SignatureKeys: []pgp.Key{"ABCD1234", "1234ABCD"}, Stanza: Stanza{"Source": "never-calamares"}}),
ErrorMatches, "denied according to rule: {\"condition\":\"\",\"allow\":null,\"deny\":\\[\"\\*\"\\]}")
// third rule: anyone from the group or explicit key
c.Check(u.IsAllowed(&Changes{SignatureKeys: []utils.GpgKey{"45678901", "12345678"}, Stanza: Stanza{"Source": "some-calamares"}}), IsNil)
c.Check(u.IsAllowed(&Changes{SignatureKeys: []utils.GpgKey{"37E1C17570096AD1"}, Stanza: Stanza{"Source": "some-calamares"}}), IsNil)
c.Check(u.IsAllowed(&Changes{SignatureKeys: []utils.GpgKey{"70096AD1"}, Stanza: Stanza{"Source": "some-calamares"}}), IsNil)
c.Check(u.IsAllowed(&Changes{SignatureKeys: []pgp.Key{"45678901", "12345678"}, Stanza: Stanza{"Source": "some-calamares"}}), IsNil)
c.Check(u.IsAllowed(&Changes{SignatureKeys: []pgp.Key{"37E1C17570096AD1"}, Stanza: Stanza{"Source": "some-calamares"}}), IsNil)
c.Check(u.IsAllowed(&Changes{SignatureKeys: []pgp.Key{"70096AD1"}, Stanza: Stanza{"Source": "some-calamares"}}), IsNil)
// fourth rule: some are not allowed
c.Check(u.IsAllowed(&Changes{SignatureKeys: []utils.GpgKey{"ABCD1234", "45678901"}, Stanza: Stanza{"Source": "some-calamares"}}),
c.Check(u.IsAllowed(&Changes{SignatureKeys: []pgp.Key{"ABCD1234", "45678901"}, Stanza: Stanza{"Source": "some-calamares"}}),
ErrorMatches, "denied according to rule: {\"condition\":\"\",\"allow\":null,\"deny\":\\[\"45678901\",\"12345678\"\\]}")
}

View File

@ -1,4 +1,4 @@
package utils
package pgp
import (
"bufio"
@ -12,60 +12,13 @@ import (
"strings"
)
// Signer interface describes facility implementing signing of files
type Signer interface {
Init() error
SetKey(keyRef string)
SetKeyRing(keyring, secretKeyring string)
SetPassphrase(passphrase, passphraseFile string)
SetBatch(batch bool)
DetachedSign(source string, destination string) error
ClearSign(source string, destination string) error
}
// Verifier interface describes signature verification factility
type Verifier interface {
InitKeyring() error
AddKeyring(keyring string)
VerifyDetachedSignature(signature, cleartext io.Reader) error
IsClearSigned(clearsigned io.Reader) (bool, error)
VerifyClearsigned(clearsigned io.Reader, showKeyTip bool) (*GpgKeyInfo, error)
ExtractClearsigned(clearsigned io.Reader) (text *os.File, err error)
}
// Test interface
var (
_ Signer = &GpgSigner{}
_ Verifier = &GpgVerifier{}
)
// GpgKey is key in GPG representation
type GpgKey string
// Matches checks two keys for equality
func (key1 GpgKey) Matches(key2 GpgKey) bool {
if key1 == key2 {
return true
}
if len(key1) == 8 && len(key2) == 16 {
return key1 == key2[8:]
}
if len(key1) == 16 && len(key2) == 8 {
return key1[8:] == key2
}
return false
}
// GpgKeyInfo is response from signature verification
type GpgKeyInfo struct {
GoodKeys []GpgKey
MissingKeys []GpgKey
}
// GpgSigner is implementation of Signer interface using gpg
// GpgSigner is implementation of Signer interface using gpg as external program
type GpgSigner struct {
keyRef string
keyring, secretKeyring string
@ -166,7 +119,7 @@ func (g *GpgSigner) ClearSign(source string, destination string) error {
return cmd.Run()
}
// GpgVerifier is implementation of Verifier interface using gpgv
// GpgVerifier is implementation of Verifier interface using gpgv as external program
type GpgVerifier struct {
keyRings []string
}
@ -209,7 +162,7 @@ func (g *GpgVerifier) argsKeyrings() (args []string) {
return
}
func (g *GpgVerifier) runGpgv(args []string, context string, showKeyTip bool) (*GpgKeyInfo, error) {
func (g *GpgVerifier) runGpgv(args []string, context string, showKeyTip bool) (*KeyInfo, error) {
args = append([]string{"--status-fd", "3"}, args...)
cmd := exec.Command("gpgv", args...)
@ -250,15 +203,15 @@ func (g *GpgVerifier) runGpgv(args []string, context string, showKeyTip bool) (*
statusr := bufio.NewScanner(tempf)
result := &GpgKeyInfo{}
result := &KeyInfo{}
for statusr.Scan() {
line := strings.TrimSpace(statusr.Text())
if strings.HasPrefix(line, "[GNUPG:] GOODSIG ") {
result.GoodKeys = append(result.GoodKeys, GpgKey(strings.Fields(line)[2]))
result.GoodKeys = append(result.GoodKeys, Key(strings.Fields(line)[2]))
} else if strings.HasPrefix(line, "[GNUPG:] NO_PUBKEY ") {
result.MissingKeys = append(result.MissingKeys, GpgKey(strings.Fields(line)[2]))
result.MissingKeys = append(result.MissingKeys, Key(strings.Fields(line)[2]))
}
}
@ -333,7 +286,7 @@ func (g *GpgVerifier) IsClearSigned(clearsigned io.Reader) (bool, error) {
}
// VerifyClearsigned verifies clearsigned file using gpgv
func (g *GpgVerifier) VerifyClearsigned(clearsigned io.Reader, showKeyTip bool) (*GpgKeyInfo, error) {
func (g *GpgVerifier) VerifyClearsigned(clearsigned io.Reader, showKeyTip bool) (*KeyInfo, error) {
args := g.argsKeyrings()
clearf, err := ioutil.TempFile("", "aptly-gpg")

54
pgp/pgp.go Normal file
View File

@ -0,0 +1,54 @@
// Package pgp provides interface to signature generation and validation
package pgp
import (
"io"
"os"
)
// Key is key in PGP representation
type Key string
// Matches checks two keys for equality
func (key1 Key) Matches(key2 Key) bool {
if key1 == key2 {
return true
}
if len(key1) == 8 && len(key2) == 16 {
return key1 == key2[8:]
}
if len(key1) == 16 && len(key2) == 8 {
return key1[8:] == key2
}
return false
}
// KeyInfo is response from signature verification
type KeyInfo struct {
GoodKeys []Key
MissingKeys []Key
}
// Signer interface describes facility implementing signing of files
type Signer interface {
Init() error
SetKey(keyRef string)
SetKeyRing(keyring, secretKeyring string)
SetPassphrase(passphrase, passphraseFile string)
SetBatch(batch bool)
DetachedSign(source string, destination string) error
ClearSign(source string, destination string) error
}
// Verifier interface describes signature verification factility
type Verifier interface {
InitKeyring() error
AddKeyring(keyring string)
VerifyDetachedSignature(signature, cleartext io.Reader) error
IsClearSigned(clearsigned io.Reader) (bool, error)
VerifyClearsigned(clearsigned io.Reader, showKeyTip bool) (*KeyInfo, error)
ExtractClearsigned(clearsigned io.Reader) (text *os.File, err error)
}

27
pgp/pgp_test.go Normal file
View File

@ -0,0 +1,27 @@
package pgp
import (
"testing"
. "gopkg.in/check.v1"
)
// Launch gocheck tests
func Test(t *testing.T) {
TestingT(t)
}
type PGPSuite struct{}
var _ = Suite(&PGPSuite{})
func (s *PGPSuite) TestKeyMatch(c *C) {
c.Check(Key("EC4B033C70096AD1").Matches(Key("EC4B033C70096AD1")), Equals, true)
c.Check(Key("37E1C17570096AD1").Matches(Key("EC4B033C70096AD1")), Equals, false)
c.Check(Key("70096AD1").Matches(Key("70096AD1")), Equals, true)
c.Check(Key("70096AD1").Matches(Key("EC4B033C")), Equals, false)
c.Check(Key("37E1C17570096AD1").Matches(Key("70096AD1")), Equals, true)
c.Check(Key("70096AD1").Matches(Key("EC4B033C70096AD1")), Equals, true)
}

View File

@ -1,20 +0,0 @@
package utils
import (
. "gopkg.in/check.v1"
)
type GpgSuite struct{}
var _ = Suite(&GpgSuite{})
func (s *GpgSuite) TestGpgKeyMatch(c *C) {
c.Check(GpgKey("EC4B033C70096AD1").Matches(GpgKey("EC4B033C70096AD1")), Equals, true)
c.Check(GpgKey("37E1C17570096AD1").Matches(GpgKey("EC4B033C70096AD1")), Equals, false)
c.Check(GpgKey("70096AD1").Matches(GpgKey("70096AD1")), Equals, true)
c.Check(GpgKey("70096AD1").Matches(GpgKey("EC4B033C")), Equals, false)
c.Check(GpgKey("37E1C17570096AD1").Matches(GpgKey("70096AD1")), Equals, true)
c.Check(GpgKey("70096AD1").Matches(GpgKey("EC4B033C70096AD1")), Equals, true)
}