Skip to content

Commit

Permalink
Add signature.GetUntrustedSignatureInformationWithoutVerifying
Browse files Browse the repository at this point in the history
Add signature.GetUntrustedSignatureInformationWithoutVerifying, which
returns the contents of a signature without doing any cryptographic
verification.

Expected uses are 1) debugging verification failures, and
(CAREFULLY!) 2) listing which signatures are stored in a registry along
with an image.  It is STRONGLY recommended to do cryptographic
verification in the latter case, and to clearly indicate untrusted
signatures in the UI.

Signed-off-by: Miloslav Trmač <[email protected]>
  • Loading branch information
mtrmac committed Jan 17, 2017
1 parent 8dd5570 commit 626c43f
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 0 deletions.
Binary file added signature/fixtures/no-optional-fields.signature
Binary file not shown.
2 changes: 2 additions & 0 deletions signature/fixtures_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ const (
TestImageSignatureReference = "testing/manifest"
// TestKeyFingerprint is the fingerprint of the private key in this directory.
TestKeyFingerprint = "1D8230F6CDB6A06716E414C1DB72F2188BB46CC8"
// TestKeyShortID is the short ID of the private key in this directory.
TestKeyShortID = "DB72F2188BB46CC8"
)
38 changes: 38 additions & 0 deletions signature/mechanism.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ package signature

import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"strings"

"github.com/mtrmac/gpgme"
"golang.org/x/crypto/openpgp"
)

// SigningMechanism abstracts a way to sign binary blobs and verify their signatures.
Expand All @@ -21,6 +25,12 @@ type SigningMechanism interface {
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)
// UntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION,
// along with a short identifier of the key used for signing.
// WARNING: The short key identifier (which correponds to "Key ID" for OpenPGP keys)
// is NOT the same as a "key identity" used in other calls ot this interface, and
// the values may have no recognizable relationship if the public key is not available.
UntrustedSignatureContents(untrustedSignature []byte) (untrustedContents []byte, shortKeyIdentifier string, err error)
}

// A GPG/OpenPGP signing mechanism.
Expand Down Expand Up @@ -119,3 +129,31 @@ func (m gpgSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte
}
return signedBuffer.Bytes(), sig.Fingerprint, nil
}

// UntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION,
// along with a short identifier of the key used for signing.
// WARNING: The short key identifier (which correponds to "Key ID" for OpenPGP keys)
// is NOT the same as a "key identity" used in other calls ot this interface, and
// the values may have no recognizable relationship if the public key is not available.
func (m gpgSigningMechanism) UntrustedSignatureContents(untrustedSignature []byte) (untrustedContents []byte, shortKeyIdentifier string, err error) {
// This uses the Golang-native OpenPGP implementation instead of gpgme because we are not doing any cryptography.
md, err := openpgp.ReadMessage(bytes.NewReader(untrustedSignature), openpgp.EntityList{}, nil, nil)
if err != nil {
return nil, "", err
}
if !md.IsSigned {
return nil, "", errors.New("The input is not a signature")
}
content, err := ioutil.ReadAll(md.UnverifiedBody)
if err != nil {
// Coverage: An error during reading the body can happen only if
// 1) the message is encrypted, which is not our case (and we don’t give ReadMessage the key
// to decrypt the contents anyway), or
// 2) the message is signed AND we give ReadMessage a correspnding public key, which we don’t.
return nil, "", err
}

// Uppercase the key ID for minimal consistency with the gpgme-returned fingerprints
// (but note that key ID is a suffix of the fingerprint only for V4 keys, not V3)!
return content, strings.ToUpper(fmt.Sprintf("%016X", md.SignedByKeyId)), nil
}
56 changes: 56 additions & 0 deletions signature/mechanism_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,59 @@ func TestGPGSigningMechanismVerify(t *testing.T) {

// The various GPG/GPGME failures cases are not obviously easy to reach.
}

func TestGPGSigningMechanismUntrustedSignatureContents(t *testing.T) {
mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)

// A valid signature
signature, err := ioutil.ReadFile("./fixtures/invalid-blob.signature")
require.NoError(t, err)
content, shortKeyID, err := mech.UntrustedSignatureContents(signature)
require.NoError(t, err)
assert.Equal(t, []byte("This is not JSON\n"), content)
assert.Equal(t, TestKeyShortID, shortKeyID)

// Completely invalid signature.
_, _, err = mech.UntrustedSignatureContents([]byte{})
assert.Error(t, err)

_, _, err = mech.UntrustedSignatureContents([]byte("invalid signature"))
assert.Error(t, err)

// Literal packet, not a signature
signature, err = ioutil.ReadFile("./fixtures/unsigned-literal.signature")
require.NoError(t, err)
content, shortKeyID, err = mech.UntrustedSignatureContents(signature)
assert.Error(t, err)

// Encrypted data, not a signature.
signature, err = ioutil.ReadFile("./fixtures/unsigned-encrypted.signature")
require.NoError(t, err)
content, shortKeyID, err = mech.UntrustedSignatureContents(signature)
assert.Error(t, err)

// Expired signature
signature, err = ioutil.ReadFile("./fixtures/expired.signature")
require.NoError(t, err)
content, shortKeyID, err = mech.UntrustedSignatureContents(signature)
require.NoError(t, err)
assert.Equal(t, []byte("This signature is expired.\n"), content)
assert.Equal(t, TestKeyShortID, shortKeyID)

// Corrupt signature
signature, err = ioutil.ReadFile("./fixtures/corrupt.signature")
require.NoError(t, err)
content, shortKeyID, err = mech.UntrustedSignatureContents(signature)
require.NoError(t, err)
assert.Equal(t, []byte(`{"critical":{"identity":{"docker-reference":"testing/manifest"},"image":{"docker-manifest-digest":"sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"},"type":"atomic container signature"},"optional":{"creator":"atomic ","timestamp":1458239713}}`), content)
assert.Equal(t, TestKeyShortID, shortKeyID)

// Valid signature with an unknown key
signature, err = ioutil.ReadFile("./fixtures/unknown-key.signature")
require.NoError(t, err)
content, shortKeyID, err = mech.UntrustedSignatureContents(signature)
require.NoError(t, err)
assert.Equal(t, []byte(`{"critical":{"identity":{"docker-reference":"testing/manifest"},"image":{"docker-manifest-digest":"sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"},"type":"atomic container signature"},"optional":{"creator":"atomic 0.1.13-dev","timestamp":1464633474}}`), content)
assert.Equal(t, "E5476D1110D07803", shortKeyID)
}
55 changes: 55 additions & 0 deletions signature/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ type untrustedSignature struct {
UntrustedTimestamp *int64
}

// UntrustedSignatureInformation is information available in an untrusted signature.
// This may be useful when debugging signature verification failures,
// or when managing a set of signatures on a single image.
//
// WARNING: Do not use the contents of this for ANY security decisions,
// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable.
// There is NO REASON to expect the values to be correct, or not intentionally misleading
// (including things like “✅ Verified by $authority”)
type UntrustedSignatureInformation struct {
UntrustedDockerManifestDigest digest.Digest
UntrustedDockerReference string // FIXME: more precise type?
UntrustedCreatorID *string
UntrustedTimestamp *time.Time
UntrustedShortKeyIdentifier string
}

// newUntrustedSignature returns an untrustedSignature object with
// the specified primary contents and appropriate metadata.
func newUntrustedSignature(dockerManifestDigest digest.Digest, dockerReference string) untrustedSignature {
Expand Down Expand Up @@ -233,3 +249,42 @@ func verifyAndExtractSignature(mech SigningMechanism, unverifiedSignature []byte
DockerReference: unmatchedSignature.UntrustedDockerReference,
}, nil
}

// GetUntrustedSignatureInformationWithoutVerifying extracts information available in an untrusted signature,
// WITHOUT doing any cryptographic verification.
// This may be useful when debugging signature verification failures,
// or when managing a set of signatures on a single image.
//
// WARNING: Do not use the contents of this for ANY security decisions,
// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable.
// There is NO REASON to expect the values to be correct, or not intentionally misleading
// (including things like “✅ Verified by $authority”)
func GetUntrustedSignatureInformationWithoutVerifying(untrustedSignatureBytes []byte) (*UntrustedSignatureInformation, error) {
// NOTE: This should eventualy do format autodetection.
mech, err := NewGPGSigningMechanism()
if err != nil {
return nil, err
}

untrustedContents, shortKeyIdentifier, err := mech.UntrustedSignatureContents(untrustedSignatureBytes)
if err != nil {
return nil, err
}
var untrustedDecodedContents untrustedSignature
if err := json.Unmarshal(untrustedContents, &untrustedDecodedContents); err != nil {
return nil, InvalidSignatureError{msg: err.Error()}
}

var timestamp *time.Time // = nil
if untrustedDecodedContents.UntrustedTimestamp != nil {
ts := time.Unix(*untrustedDecodedContents.UntrustedTimestamp, 0)
timestamp = &ts
}
return &UntrustedSignatureInformation{
UntrustedDockerManifestDigest: untrustedDecodedContents.UntrustedDockerManifestDigest,
UntrustedDockerReference: untrustedDecodedContents.UntrustedDockerReference,
UntrustedCreatorID: untrustedDecodedContents.UntrustedCreatorID,
UntrustedTimestamp: timestamp,
UntrustedShortKeyIdentifier: shortKeyIdentifier,
}, nil
}
39 changes: 39 additions & 0 deletions signature/signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,42 @@ func TestVerifyAndExtractSignature(t *testing.T) {
assert.Nil(t, sig)
assert.Equal(t, signatureData, recorded)
}

func TestGetUntrustedSignatureInformationWithoutVerifying(t *testing.T) {
signature, err := ioutil.ReadFile("./fixtures/image.signature")
require.NoError(t, err)
// Successful parsing, all optional fields present
info, err := GetUntrustedSignatureInformationWithoutVerifying(signature)
require.NoError(t, err)
assert.Equal(t, TestImageSignatureReference, info.UntrustedDockerReference)
assert.Equal(t, TestImageManifestDigest, info.UntrustedDockerManifestDigest)
assert.NotNil(t, info.UntrustedCreatorID)
assert.Equal(t, "atomic ", *info.UntrustedCreatorID)
assert.NotNil(t, info.UntrustedTimestamp)
assert.Equal(t, time.Unix(1458239713, 0), *info.UntrustedTimestamp)
assert.Equal(t, TestKeyShortID, info.UntrustedShortKeyIdentifier)
// Successful parsing, no optional fields present
signature, err = ioutil.ReadFile("./fixtures/no-optional-fields.signature")
require.NoError(t, err)
// Successful parsing
info, err = GetUntrustedSignatureInformationWithoutVerifying(signature)
require.NoError(t, err)
assert.Equal(t, TestImageSignatureReference, info.UntrustedDockerReference)
assert.Equal(t, TestImageManifestDigest, info.UntrustedDockerManifestDigest)
assert.Nil(t, info.UntrustedCreatorID)
assert.Nil(t, info.UntrustedTimestamp)
assert.Equal(t, TestKeyShortID, info.UntrustedShortKeyIdentifier)

// Completely invalid signature.
_, err = GetUntrustedSignatureInformationWithoutVerifying([]byte{})
assert.Error(t, err)

_, err = GetUntrustedSignatureInformationWithoutVerifying([]byte("invalid signature"))
assert.Error(t, err)

// Valid signature of non-JSON
invalidBlobSignature, err := ioutil.ReadFile("./fixtures/invalid-blob.signature")
require.NoError(t, err)
_, err = GetUntrustedSignatureInformationWithoutVerifying(invalidBlobSignature)
assert.Error(t, err)
}

0 comments on commit 626c43f

Please sign in to comment.