Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions image/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ go 1.25.7
require (
dario.cat/mergo v1.0.2
github.com/BurntSushi/toml v1.6.0
github.com/ProtonMail/go-crypto v1.4.1
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01
github.com/containers/ocicrypt v1.3.2
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467
Expand Down Expand Up @@ -39,7 +40,6 @@ require (
go.etcd.io/bbolt v1.5.0
go.podman.io/storage v1.63.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.54.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.22.0
golang.org/x/term v0.45.0
Expand All @@ -48,7 +48,6 @@ require (
require (
cyphar.com/go-pathrs v0.2.5 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down Expand Up @@ -98,6 +97,7 @@ require (
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
Expand Down
7 changes: 1 addition & 6 deletions image/signature/mechanism.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@ import (
"io"
"strings"

// This code is used only to parse the data in an explicitly-untrusted
// code path, where cryptography is not relevant. For now, continue to
// use this frozen deprecated implementation. When mechanism_openpgp.go
// migrates to another implementation, this should migrate as well.
//lint:ignore SA1019 See above
"golang.org/x/crypto/openpgp" //nolint:staticcheck
"github.com/ProtonMail/go-crypto/openpgp"
)

// SigningMechanism abstracts a way to sign binary blobs and verify their signatures.
Expand Down
25 changes: 2 additions & 23 deletions image/signature/mechanism_openpgp.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,13 @@ import (
"os"
"path"
"strings"
"time"

"github.com/ProtonMail/go-crypto/openpgp"
"go.podman.io/image/v5/signature/internal"
"go.podman.io/storage/pkg/homedir"

// This is a fallback code; the primary recommendation is to use the gpgme mechanism
// implementation, which is out-of-process and more appropriate for handling long-term private key material
// than any Go implementation.
// For this verify-only fallback, we haven't reviewed any of the
// existing alternatives to choose; so, for now, continue to
// use this frozen deprecated implementation.
//lint:ignore SA1019 See above
"golang.org/x/crypto/openpgp" //nolint:staticcheck
)

// A GPG/OpenPGP signing mechanism, implemented using x/crypto/openpgp.
// A GPG/OpenPGP signing mechanism, implemented using github.com/ProtonMail/go-crypto/openpgp.
type openpgpSigningMechanism struct {
keyring openpgp.EntityList
}
Expand Down Expand Up @@ -155,18 +146,6 @@ func (m *openpgpSigningMechanism) Verify(unverifiedSignature []byte) (contents [
if md.SignedBy == nil {
return nil, "", internal.NewInvalidSignatureError(fmt.Sprintf("Key not found for key ID %x in signature", md.SignedByKeyId))
}
if md.Signature != nil {
if md.Signature.SigLifetimeSecs != nil {
expiry := md.Signature.CreationTime.Add(time.Duration(*md.Signature.SigLifetimeSecs) * time.Second)
if time.Now().After(expiry) {
return nil, "", internal.NewInvalidSignatureError(fmt.Sprintf("Signature expired on %s", expiry))
}
}
} else if md.SignatureV3 == nil {
// Coverage: If md.SignedBy != nil, the final md.UnverifiedBody.Read() either sets one of md.Signature or md.SignatureV3,
// or sets md.SignatureError.
return nil, "", internal.NewInvalidSignatureError("Unexpected openpgp.MessageDetails: neither Signature nor SignatureV3 is set")
}

// Uppercase the fingerprint to be compatible with gpgme
return content, strings.ToUpper(fmt.Sprintf("%x", md.SignedBy.Entity.PrimaryKey.Fingerprint)), nil
Expand Down
172 changes: 101 additions & 71 deletions image/signature/mechanism_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,26 @@ const (
testGPGHomeDirectory = "./fixtures"
)

// Many of the tests use two fixtures: V4 signature packets (*.signature), and V3 signature packets (*.signature-v3)
// Many of the tests use two fixtures: V4 signature packets (*.signature), and V3 signature packets (*.signature-v3).
// Note that V3 signature packets are not supported by openpgpSigningMechanism.

// fixtureVariants loads V3 and V4 signature fixture variants based on the v4 fixture path, and returns a map which makes it easy to test both.
func fixtureVariants(t *testing.T, v4Path string) map[string][]byte {
type fixtureVariant struct {
path string
bytes []byte
isV3 bool
}

// fixtureVariants loads V3 and V4 signature fixture variants based on the v4 fixture path.
func fixtureVariants(t *testing.T, v4Path string) []fixtureVariant {
v4, err := os.ReadFile(v4Path)
require.NoError(t, err)
v3Path := v4Path + "-v3"
v3, err := os.ReadFile(v3Path)
require.NoError(t, err)
return map[string][]byte{v4Path: v4, v3Path: v3}
return []fixtureVariant{
{path: v4Path, bytes: v4, isV3: false},
{path: v3Path, bytes: v3, isV3: true},
}
}

func TestSigningNotSupportedError(t *testing.T) {
Expand Down Expand Up @@ -56,9 +66,9 @@ func TestNewGPGSigningMechanismInDirectory(t *testing.T) {
mech, err = newGPGSigningMechanismInDirectory("")
require.NoError(t, err)
defer mech.Close()
for version, signature := range signatures {
_, _, err := mech.Verify(signature)
assert.Error(t, err, version)
for _, variant := range signatures {
_, _, err := mech.Verify(variant.bytes)
assert.Error(t, err, variant.path)
}

// Similarly, using a newly created empty directory makes TestKeyFingerprint
Expand All @@ -67,9 +77,9 @@ func TestNewGPGSigningMechanismInDirectory(t *testing.T) {
mech, err = newGPGSigningMechanismInDirectory(emptyDir)
require.NoError(t, err)
defer mech.Close()
for version, signature := range signatures {
_, _, err := mech.Verify(signature)
assert.Error(t, err, version)
for _, variant := range signatures {
_, _, err := mech.Verify(variant.bytes)
assert.Error(t, err, variant.path)
}

// If pubring.gpg is unreadable in the directory, either initializing
Expand All @@ -82,29 +92,33 @@ func TestNewGPGSigningMechanismInDirectory(t *testing.T) {
mech, err = newGPGSigningMechanismInDirectory(unreadableDir)
if err == nil {
defer mech.Close()
for version, signature := range signatures {
_, _, err := mech.Verify(signature)
assert.Error(t, err, version)
for _, variant := range signatures {
_, _, err := mech.Verify(variant.bytes)
assert.Error(t, err, variant.path)
}
}

// Setting the directory parameter to testGPGHomeDirectory makes the key available.
mech, err = newGPGSigningMechanismInDirectory(testGPGHomeDirectory)
require.NoError(t, err)
defer mech.Close()
for version, signature := range signatures {
_, _, err := mech.Verify(signature)
assert.NoError(t, err, version)
for _, variant := range signatures {
_, _, err := mech.Verify(variant.bytes)
if !variant.isV3 { // V3 signatures might be entirely unsupported and rejected.
assert.NoError(t, err, variant.path)
}
}

// If we use the default directory mechanism, GNUPGHOME is respected.
t.Setenv("GNUPGHOME", testGPGHomeDirectory)
mech, err = newGPGSigningMechanismInDirectory("")
require.NoError(t, err)
defer mech.Close()
for version, signature := range signatures {
_, _, err := mech.Verify(signature)
assert.NoError(t, err, version)
for _, variant := range signatures {
_, _, err := mech.Verify(variant.bytes)
if !variant.isV3 { // V3 signatures might be entirely unsupported and rejected.
assert.NoError(t, err, variant.path)
}
}
}

Expand All @@ -116,9 +130,9 @@ func TestNewEphemeralGPGSigningMechanism(t *testing.T) {
assert.Empty(t, keyIdentities)
// Try validating a signature when the key is unknown.
signatures := fixtureVariants(t, "./fixtures/invalid-blob.signature")
for version, signature := range signatures {
_, _, err := mech.Verify(signature)
require.Error(t, err, version)
for _, variant := range signatures {
_, _, err := mech.Verify(variant.bytes)
require.Error(t, err, variant.path)
}

// Successful import
Expand All @@ -129,11 +143,15 @@ func TestNewEphemeralGPGSigningMechanism(t *testing.T) {
defer mech.Close()
assert.Equal(t, []string{TestKeyFingerprint}, keyIdentities)
// After import, the signature should validate.
for version, signature := range signatures {
content, signingFingerprint, err := mech.Verify(signature)
require.NoError(t, err, version)
assert.Equal(t, []byte("This is not JSON\n"), content, version)
assert.Equal(t, TestKeyFingerprint, signingFingerprint, version)
for _, variant := range signatures {
content, signingFingerprint, err := mech.Verify(variant.bytes)
if !variant.isV3 { // V3 signatures might be entirely unsupported and rejected.
require.NoError(t, err, variant.path)
}
if err == nil {
assert.Equal(t, []byte("This is not JSON\n"), content, variant.path)
assert.Equal(t, TestKeyFingerprint, signingFingerprint, variant.path)
}
}

// Import of a key with a subkey
Expand Down Expand Up @@ -239,34 +257,46 @@ func TestGPGSigningMechanismVerify(t *testing.T) {
require.NoError(t, err)
defer mech.Close()

// For extra paranoia, test that we return nil data on error.

// Successful verification
signatures := fixtureVariants(t, "./fixtures/invalid-blob.signature")
for variant, signature := range signatures {
content, signingFingerprint, err := mech.Verify(signature)
require.NoError(t, err, variant)
assert.Equal(t, []byte("This is not JSON\n"), content, variant)
assert.Equal(t, TestKeyFingerprint, signingFingerprint, variant)
for _, variant := range signatures {
content, signingFingerprint, err := mech.Verify(variant.bytes)
if !variant.isV3 { // V3 signatures might be entirely unsupported and rejected.
require.NoError(t, err, variant.path)
}
if err == nil {
assert.Equal(t, []byte("This is not JSON\n"), content, variant.path)
assert.Equal(t, TestKeyFingerprint, signingFingerprint, variant.path)
} else {
assertSigningError(t, content, signingFingerprint, err)
}
}
// Successful verification of a signature using a subkey
signatures = fixtureVariants(t, "./fixtures/subkey.signature")
for variant, signature := range signatures {
content, signingFingerprint, err := mech.Verify(signature)
require.NoError(t, err, variant)
assert.Equal(t, []byte(`{"critical":{"identity":{"docker-reference":"testing/manifest:latest"},"image":{"docker-manifest-digest":"sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"},"type":"atomic container signature"},"optional":{}}`), content, variant)
if signingFingerprint != TestKeyFingerprintPrimaryWithSubkey {
assert.Equal(t, TestKeyFingerprintSubkeyWithSubkey, signingFingerprint, variant)
withLookup, ok := mech.(signingMechanismWithVerificationIdentityLookup)
require.True(t, ok, variant)

primaryKey, err := withLookup.keyIdentityForVerificationKeyIdentity(signingFingerprint)
require.NoError(t, err, variant)
signingFingerprint = primaryKey
for _, variant := range signatures {
content, signingFingerprint, err := mech.Verify(variant.bytes)
if !variant.isV3 { // V3 signatures might be entirely unsupported and rejected.
require.NoError(t, err, variant.path)
}
if err == nil {
assert.Equal(t, []byte(`{"critical":{"identity":{"docker-reference":"testing/manifest:latest"},"image":{"docker-manifest-digest":"sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"},"type":"atomic container signature"},"optional":{}}`), content, variant.path)
if signingFingerprint != TestKeyFingerprintPrimaryWithSubkey {
assert.Equal(t, TestKeyFingerprintSubkeyWithSubkey, signingFingerprint, variant.path)
withLookup, ok := mech.(signingMechanismWithVerificationIdentityLookup)
require.True(t, ok, variant.path)

primaryKey, err := withLookup.keyIdentityForVerificationKeyIdentity(signingFingerprint)
require.NoError(t, err, variant.path)
signingFingerprint = primaryKey
}
assert.Equal(t, TestKeyFingerprintPrimaryWithSubkey, signingFingerprint, variant.path)
} else {
assertSigningError(t, content, signingFingerprint, err)
}
assert.Equal(t, TestKeyFingerprintPrimaryWithSubkey, signingFingerprint, variant)
}

// For extra paranoia, test that we return nil data on error.

// Completely invalid signature.
content, signingFingerprint, err := mech.Verify([]byte{})
assertSigningError(t, content, signingFingerprint, err)
Expand Down Expand Up @@ -296,23 +326,23 @@ func TestGPGSigningMechanismVerify(t *testing.T) {

// Corrupt signature
signatures = fixtureVariants(t, "./fixtures/corrupt.signature")
for version, signature := range signatures {
content, signingFingerprint, err := mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err, version)
for _, variant := range signatures {
content, signingFingerprint, err := mech.Verify(variant.bytes)
assertSigningError(t, content, signingFingerprint, err, variant.path)
}

// Valid signature with an unknown key
signatures = fixtureVariants(t, "./fixtures/unknown-key.signature")
for version, signature := range signatures {
content, signingFingerprint, err := mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err, version)
for _, variant := range signatures {
content, signingFingerprint, err := mech.Verify(variant.bytes)
assertSigningError(t, content, signingFingerprint, err, variant.path)
}

// Valid signature with a revoked subkey
signatures = fixtureVariants(t, "./fixtures/subkey-revoked.signature")
for version, signature := range signatures {
content, signingFingerprint, err := mech.Verify(signature)
assertSigningError(t, content, signingFingerprint, err, version)
for _, variant := range signatures {
content, signingFingerprint, err := mech.Verify(variant.bytes)
assertSigningError(t, content, signingFingerprint, err, variant.path)
}

// The various GPG/GPGME failures cases are not obviously easy to reach.
Expand Down Expand Up @@ -367,11 +397,11 @@ func TestGPGSigningMechanismUntrustedSignatureContents(t *testing.T) {

// A valid signature
signatures := fixtureVariants(t, "./fixtures/invalid-blob.signature")
for version, signature := range signatures {
content, shortKeyID, err := mech.UntrustedSignatureContents(signature)
require.NoError(t, err, version)
assert.Equal(t, []byte("This is not JSON\n"), content, version)
assert.Equal(t, TestKeyShortID, shortKeyID, version)
for _, variant := range signatures {
content, shortKeyID, err := mech.UntrustedSignatureContents(variant.bytes)
require.NoError(t, err, variant.path)
assert.Equal(t, []byte("This is not JSON\n"), content, variant.path)
assert.Equal(t, TestKeyShortID, shortKeyID, variant.path)
}

// Completely invalid signature.
Expand Down Expand Up @@ -403,19 +433,19 @@ func TestGPGSigningMechanismUntrustedSignatureContents(t *testing.T) {

// Corrupt signature
signatures = fixtureVariants(t, "./fixtures/corrupt.signature")
for version, signature := range signatures {
content, shortKeyID, err := mech.UntrustedSignatureContents(signature)
require.NoError(t, err, version)
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, version)
assert.Equal(t, TestKeyShortID, shortKeyID, version)
for _, variant := range signatures {
content, shortKeyID, err := mech.UntrustedSignatureContents(variant.bytes)
require.NoError(t, err, variant.path)
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, variant.path)
assert.Equal(t, TestKeyShortID, shortKeyID, variant.path)
}

// Valid signature with an unknown key
signatures = fixtureVariants(t, "./fixtures/unknown-key.signature")
for version, signature := range signatures {
content, shortKeyID, err := mech.UntrustedSignatureContents(signature)
require.NoError(t, err, version)
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, version)
assert.Equal(t, "5F9470E3BC6C3B55", shortKeyID, version)
for _, variant := range signatures {
content, shortKeyID, err := mech.UntrustedSignatureContents(variant.bytes)
require.NoError(t, err, variant.path)
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, variant.path)
assert.Equal(t, "5F9470E3BC6C3B55", shortKeyID, variant.path)
}
}
Loading
Loading