Skip to content

Commit

Permalink
WIP: Allow choosing between a gpgme and openpgp signature backend usi…
Browse files Browse the repository at this point in the history
…ng a build tag

The default is gpgme; a containers_image_openpgp build tag can be used
to use openpgp instead.

openpgp does not currently support signing, and is based on mfojtik's
implementation (adding GPG home directory support, parsing of unarmored
keys, and fixing ImportKeysFromBytes semantics).

NOTE: The openpgp backend is not really fleshed out yet. Some of the
mechanism_test.go tests may be better mechanism-specific, and openpgp
definitely needs more tests e.g. for optionalDir.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
  • Loading branch information
mtrmac committed Jan 9, 2017
1 parent 93a9f23 commit b35edc2
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 102 deletions.
4 changes: 4 additions & 0 deletions copy/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageRe
if err != nil {
return errors.Wrap(err, "Error initializing GPG")
}
if err := mech.SupportsSigning(); err != nil {
return errors.Wrap(err, "Signing not supported")
}

dockerReference := dest.Reference().DockerReference()
if dockerReference == nil {
return errors.Errorf("Cannot determine canonical Docker reference for destination %s", transports.ImageName(dest.Reference()))
Expand Down
5 changes: 5 additions & 0 deletions signature/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
func TestSignDockerManifest(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)

if err := mech.SupportsSigning(); err != nil {
t.Skipf("Signing not supported: %v", err)
}

manifest, err := ioutil.ReadFile("fixtures/image.manifest.json")
require.NoError(t, err)

Expand Down
101 changes: 2 additions & 99 deletions signature/mechanism.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@

package signature

import (
"bytes"
"fmt"

"github.com/mtrmac/gpgme"
)

// SigningMechanism abstracts a way to sign binary blobs and verify their signatures.
// FIXME: Eventually expand on keyIdentity (namespace them between mechanisms to
// eliminate ambiguities, support CA signatures and perhaps other key properties)
Expand All @@ -17,105 +10,15 @@ type SigningMechanism interface {
// The blob is assumed to have an appropriate format (the caller is expected to know which one).
// NOTE: This may modify long-term state (e.g. key storage in a directory underlying the mechanism).
ImportKeysFromBytes(blob []byte) ([]string, error)
// SupportsSigning returns nil if the mechanism supports signing, or an error message.
SupportsSigning() error
// Sign creates a (non-detached) signature of input using keyidentity
Sign(input []byte, keyIdentity string) ([]byte, error)
// Verify parses unverifiedSignature and returns the content and the signer's identity
Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error)
}

// A GPG/OpenPGP signing mechanism.
type gpgSigningMechanism struct {
ctx *gpgme.Context
}

// NewGPGSigningMechanism returns a new GPG/OpenPGP signing mechanism.
func NewGPGSigningMechanism() (SigningMechanism, error) {
return newGPGSigningMechanismInDirectory("")
}

// newGPGSigningMechanismInDirectory returns a new GPG/OpenPGP signing mechanism, using optionalDir if not empty.
func newGPGSigningMechanismInDirectory(optionalDir string) (SigningMechanism, error) {
ctx, err := gpgme.New()
if err != nil {
return nil, err
}
if err = ctx.SetProtocol(gpgme.ProtocolOpenPGP); err != nil {
return nil, err
}
if optionalDir != "" {
err := ctx.SetEngineInfo(gpgme.ProtocolOpenPGP, "", optionalDir)
if err != nil {
return nil, err
}
}
ctx.SetArmor(false)
ctx.SetTextMode(false)
return gpgSigningMechanism{ctx: ctx}, nil
}

// ImportKeysFromBytes implements SigningMechanism.ImportKeysFromBytes
func (m gpgSigningMechanism) ImportKeysFromBytes(blob []byte) ([]string, error) {
inputData, err := gpgme.NewDataBytes(blob)
if err != nil {
return nil, err
}
res, err := m.ctx.Import(inputData)
if err != nil {
return nil, err
}
keyIdentities := []string{}
for _, i := range res.Imports {
if i.Result == nil {
keyIdentities = append(keyIdentities, i.Fingerprint)
}
}
return keyIdentities, nil
}

// Sign implements SigningMechanism.Sign
func (m gpgSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte, error) {
key, err := m.ctx.GetKey(keyIdentity, true)
if err != nil {
return nil, err
}
inputData, err := gpgme.NewDataBytes(input)
if err != nil {
return nil, err
}
var sigBuffer bytes.Buffer
sigData, err := gpgme.NewDataWriter(&sigBuffer)
if err != nil {
return nil, err
}
if err = m.ctx.Sign([]*gpgme.Key{key}, inputData, sigData, gpgme.SigModeNormal); err != nil {
return nil, err
}
return sigBuffer.Bytes(), nil
}

// Verify implements SigningMechanism.Verify
func (m gpgSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) {
signedBuffer := bytes.Buffer{}
signedData, err := gpgme.NewDataWriter(&signedBuffer)
if err != nil {
return nil, "", err
}
unverifiedSignatureData, err := gpgme.NewDataBytes(unverifiedSignature)
if err != nil {
return nil, "", err
}
_, sigs, err := m.ctx.Verify(unverifiedSignatureData, nil, signedData)
if err != nil {
return nil, "", err
}
if len(sigs) != 1 {
return nil, "", InvalidSignatureError{msg: fmt.Sprintf("Unexpected GPG signature count %d", len(sigs))}
}
sig := sigs[0]
// This is sig.Summary == gpgme.SigSumValid except for key trust, which we handle ourselves
if sig.Status != nil || sig.Validity == gpgme.ValidityNever || sig.ValidityReason != nil || sig.WrongKeyUsage {
// FIXME: Better error reporting eventually
return nil, "", InvalidSignatureError{msg: fmt.Sprintf("Invalid GPG signature: %#v", sig)}
}
return signedBuffer.Bytes(), sig.Fingerprint, nil
}
107 changes: 107 additions & 0 deletions signature/mechanism_gpgme.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// +build !containers_image_openpgp

package signature

import (
"bytes"
"fmt"

"github.com/mtrmac/gpgme"
)

// A GPG/OpenPGP signing mechanism, implemented using gpgme.
type gpgmeSigningMechanism struct {
ctx *gpgme.Context
}

// newGPGSigningMechanismInDirectory returns a new GPG/OpenPGP signing mechanism, using optionalDir if not empty.
func newGPGSigningMechanismInDirectory(optionalDir string) (SigningMechanism, error) {
ctx, err := gpgme.New()
if err != nil {
return nil, err
}
if err = ctx.SetProtocol(gpgme.ProtocolOpenPGP); err != nil {
return nil, err
}
if optionalDir != "" {
err := ctx.SetEngineInfo(gpgme.ProtocolOpenPGP, "", optionalDir)
if err != nil {
return nil, err
}
}
ctx.SetArmor(false)
ctx.SetTextMode(false)
return gpgmeSigningMechanism{ctx: ctx}, nil
}

// ImportKeysFromBytes implements SigningMechanism.ImportKeysFromBytes
func (m gpgmeSigningMechanism) ImportKeysFromBytes(blob []byte) ([]string, error) {
inputData, err := gpgme.NewDataBytes(blob)
if err != nil {
return nil, err
}
res, err := m.ctx.Import(inputData)
if err != nil {
return nil, err
}
keyIdentities := []string{}
for _, i := range res.Imports {
if i.Result == nil {
keyIdentities = append(keyIdentities, i.Fingerprint)
}
}
return keyIdentities, nil
}

// SupportsSigning returns nil if the mechanism supports signing, or an error message.
func (m gpgmeSigningMechanism) SupportsSigning() error {
return nil
}

// Sign implements SigningMechanism.Sign
func (m gpgmeSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte, error) {
key, err := m.ctx.GetKey(keyIdentity, true)
if err != nil {
return nil, err
}
inputData, err := gpgme.NewDataBytes(input)
if err != nil {
return nil, err
}
var sigBuffer bytes.Buffer
sigData, err := gpgme.NewDataWriter(&sigBuffer)
if err != nil {
return nil, err
}
if err = m.ctx.Sign([]*gpgme.Key{key}, inputData, sigData, gpgme.SigModeNormal); err != nil {
return nil, err
}
return sigBuffer.Bytes(), nil
}

// Verify implements SigningMechanism.Verify
func (m gpgmeSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) {
signedBuffer := bytes.Buffer{}
signedData, err := gpgme.NewDataWriter(&signedBuffer)
if err != nil {
return nil, "", err
}
unverifiedSignatureData, err := gpgme.NewDataBytes(unverifiedSignature)
if err != nil {
return nil, "", err
}
_, sigs, err := m.ctx.Verify(unverifiedSignatureData, nil, signedData)
if err != nil {
return nil, "", err
}
if len(sigs) != 1 {
return nil, "", InvalidSignatureError{msg: fmt.Sprintf("Unexpected GPG signature count %d", len(sigs))}
}
sig := sigs[0]
// This is sig.Summary == gpgme.SigSumValid except for key trust, which we handle ourselves
if sig.Status != nil || sig.Validity == gpgme.ValidityNever || sig.ValidityReason != nil || sig.WrongKeyUsage {
// FIXME: Better error reporting eventually
return nil, "", InvalidSignatureError{msg: fmt.Sprintf("Invalid GPG signature: %#v", sig)}
}
return signedBuffer.Bytes(), sig.Fingerprint, nil
}
113 changes: 113 additions & 0 deletions signature/mechanism_openpgp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// +build containers_image_openpgp

package signature

import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"time"

"github.com/containers/storage/pkg/homedir"
"golang.org/x/crypto/openpgp"
)

// A GPG/OpenPGP signing mechanism, implemented using x/crypto/openpgp.
type openpgpSigningMechanism struct {
keyring openpgp.EntityList
}

// newGPGSigningMechanismInDirectory returns a new GPG/OpenPGP signing mechanism, using optionalDir if not empty.
func newGPGSigningMechanismInDirectory(optionalDir string) (SigningMechanism, error) {
m := &openpgpSigningMechanism{
keyring: openpgp.EntityList{},
}

gpgHome := optionalDir
if gpgHome == "" {
gpgHome = os.Getenv("GNUPGHOME")
if gpgHome == "" {
gpgHome = path.Join(homedir.Get(), ".gnupg")
}
}

pubring, err := ioutil.ReadFile(path.Join(gpgHome, "pubring.gpg"))
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else {
_, err := m.ImportKeysFromBytes(pubring)
if err != nil {
return nil, err
}
}
return m, nil
}

func (m *openpgpSigningMechanism) ImportKeysFromBytes(blob []byte) ([]string, error) {
keyring, err := openpgp.ReadKeyRing(bytes.NewReader(blob))
if err != nil {
k, e2 := openpgp.ReadArmoredKeyRing(bytes.NewReader(blob))
if e2 != nil {
return nil, err // The original error -- FIXME: is this better?
}
keyring = k
}

keyIdentities := []string{}
for _, entity := range keyring {
if entity.PrimaryKey == nil {
continue
}
// Uppercase the fingerprint to be compatible with gpgme
keyIdentities = append(keyIdentities, strings.ToUpper(fmt.Sprintf("%x", entity.PrimaryKey.Fingerprint)))
m.keyring = append(m.keyring, entity)
}
return keyIdentities, nil
}

// SupportsSigning returns nil if the mechanism supports signing, or an error message.
func (m *openpgpSigningMechanism) SupportsSigning() error {
return errors.New("signing is not supported in github.com/containers/image built with the containers_image_openpgp build tag")
}

func (m *openpgpSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte, error) {
return nil, errors.New("signing is not supported in github.com/containers/image built with the containers_image_openpgp build tag")
}

func (m *openpgpSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) {
if len(m.keyring) == 0 {
return nil, "", errors.New("no public keys imported")
}
md, err := openpgp.ReadMessage(bytes.NewReader(unverifiedSignature), m.keyring, nil, nil)
if err != nil {
return nil, "", err
}
if !md.IsSigned {
return nil, "", errors.New("not signed")
}
content, err := ioutil.ReadAll(md.UnverifiedBody)
if err != nil {
return nil, "", err
}
if md.SignatureError != nil {
return nil, "", fmt.Errorf("signature error: %v", md.SignatureError)
}
if md.SignedBy == nil {
return nil, "", InvalidSignatureError{msg: fmt.Sprintf("Invalid GPG signature: %#v", md.Signature)}
}
if md.Signature.SigLifetimeSecs != nil {
expiry := md.Signature.CreationTime.Add(time.Duration(*md.Signature.SigLifetimeSecs) * time.Second)
if time.Now().After(expiry) {
return nil, "", InvalidSignatureError{msg: fmt.Sprintf("Signature expired on %s", expiry)}
}
}

// Uppercase the fingerprint to be compatible with gpgme
return content, strings.ToUpper(fmt.Sprintf("%x", md.SignedBy.PublicKey.Fingerprint)), nil
}
Loading

0 comments on commit b35edc2

Please sign in to comment.