diff --git a/signature/docker.go b/signature/docker.go index b313231a8..c9a2a3747 100644 --- a/signature/docker.go +++ b/signature/docker.go @@ -5,7 +5,6 @@ package signature import ( "errors" "fmt" - "slices" "strings" "github.com/containers/image/v5/docker/reference" @@ -64,15 +63,8 @@ func VerifyImageManifestSignatureUsingKeyIdentityList(unverifiedSignature, unver if err != nil { return nil, "", err } - var matchedKeyIdentity string - sig, err := verifyAndExtractSignature(mech, unverifiedSignature, signatureAcceptanceRules{ - validateKeyIdentity: func(keyIdentity string) error { - if !slices.Contains(expectedKeyIdentities, keyIdentity) { - return internal.NewInvalidSignatureError(fmt.Sprintf("Signature by %s does not match expected fingerprints %v", keyIdentity, expectedKeyIdentities)) - } - matchedKeyIdentity = keyIdentity - return nil - }, + sig, matchedKeyIdentity, err := verifyAndExtractSignature(mech, unverifiedSignature, signatureAcceptanceRules{ + acceptedKeyIdentities: expectedKeyIdentities, validateSignedDockerReference: func(signedDockerReference string) error { signedRef, err := reference.ParseNormalizedNamed(signedDockerReference) if err != nil { diff --git a/signature/fixtures/dir-img-valid-subkey/manifest.json b/signature/fixtures/dir-img-valid-subkey/manifest.json new file mode 120000 index 000000000..c5bd25431 --- /dev/null +++ b/signature/fixtures/dir-img-valid-subkey/manifest.json @@ -0,0 +1 @@ +../image.manifest.json \ No newline at end of file diff --git a/signature/fixtures/dir-img-valid-subkey/signature-1 b/signature/fixtures/dir-img-valid-subkey/signature-1 new file mode 120000 index 000000000..5af2f7809 --- /dev/null +++ b/signature/fixtures/dir-img-valid-subkey/signature-1 @@ -0,0 +1 @@ +../subkey.signature \ No newline at end of file diff --git a/signature/fixtures/public-key-with-revoked-subkey.gpg b/signature/fixtures/public-key-with-revoked-subkey.gpg new file mode 100644 index 000000000..54361770b --- /dev/null +++ b/signature/fixtures/public-key-with-revoked-subkey.gpg @@ -0,0 +1,58 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v2.0.22 (GNU/Linux) + +mQGNBGiBSK8BDAC6debGjwAv5hBns4gmeY54Er/U3gFcLpa6Wn/a426T1+5PBfl9 +Gdk9ZxCKv8SEG6rkSFPbG1CQ9abahWQXrjl7jxthG99DSAjLq6x/oR/9y/th8YDM +oFVzARMHNpSfP5mbM7GAfKoIMmcg9xRPtNNpRi3utm7P8Fhr1STK6YqazXf95DdU +EXvnbD2pFDQe3UJT6YYfTq3jjVNH3wJhELvriux9n7vCdep7v9te12DsYmxY8k/5 +juBzJSLsH/VKCeWmZPa0cy4FsQZvFNYhOfUAUxIZ70hgfrb5BD5I6fR+xbWB8DJ8 +u8B8u93pKVVJzIstUKVHbBU8OqAnfWpg07a6bpJNCIFiU8CTfs2ocCP15stUaaJP +tyyhMIH3tTZNk6o7oI1r4YS8p1ndBk1gFY9uxzLFgWRehsd0YWAYuEJgU5eVOc3S +0eqQivk15mqJktknvTThxoyR7ifeOpmTFlLfZHgrzrlWwd34az1fHwoyJ6U0kQbI +cxc4qq+1VJflSNkAEQEAAbQmYy9pbWFnZSB0ZXN0IGtleSB3aXRoIGEgUkVWT0tF +RCBzdWJrZXmJAbkEEwEIACMFAmiBSK8CGyEHCwkIBwMCAQYVCAIJCgsEFgIDAQIe +AQIXgAAKCRBrhJmvYziNY7B2C/0TY00nN0YKKclWuwlg/XX+NDD6mDWw4tUm0on6 +zKZJ1tFTEIimXjsy9qbsi+wj6KMWoPozIG22+g/JZmot4llUSYbHLp8qi/egB7LM +ySMMq56BbfXVZCzDl5TuU+vaCozWlev4F1OXdJSoezEqnKfkcPZPGl6/X09VOlT2 +GlGLzSB4hhQUm6now28VaPmz2k6pcjuTfpykUtFuzA+psCltEYYUfv2eZaWUChJo +naxRuq+3nmVZoHq5yMmjCr4COA4cvsKCL0bkXplXx8e7wqjc1jEi7xFfBwCZfLER +egMq+jKwNSXqPIeV+BCvlxhQRgCt80sgWpCbw6hdY33qtoYadnOJLPkcgwBRHoTc +kj4BuKcDPJ7fBzrGHErr2UGP+ZNDlkYY3ynle18XMh7UX2NhbWDhKmIwdQJ7KsM/ +ibcSAjfJ4Mm42Tx458FOkUkGy/pfF9427aW0tr7ShFWozGLHNBmcIeJ4dk0sDD2f +uLRs4x0uyup8NuI7oS2YepHtZXe5AY0EaIFIrwEMALezb6rHjiNOfJMSNwNN6xx1 +yUo6oDoV/cfh9KBTHTA9NChnfIqPt0sqi2PZXzyzJmd+wTTe+ROViz6HrIReWFpS +4TCouvWsBrJz9pn5Bjg2dJZcHRovd2BqAuIGM1DndqkV0DhjxcNhy2OP8tVajlJA +o+KtRJgioerF+Z278BfzTRXtI9V5L5D9Ak+3K0HJC0c2AwwCF+vLQzHGPWxHaM39 +Z9rMJD9+91DPy7ZNdP5kGSTyL3S8TdDj7jaNQarjcSgoeA+QBW+az4/n+2P8HcaO +UqKpkUsfsFZipo1TvCuWkerbYcRP22yVW6j6SRjNzd/z6C1BNEJ+Kr7CZhbLc7IQ +/Jwv95ZMcLaT08Z9tcLqRlsEKCG8Q06asy1G0KOh89i7RjPoNON3mI795NAiJ/6g +hjUUksPLIrnaSkz6RnFayEfycLMMnXdy1Im3AYD4vULKdPrbk9YZB7N1olMBWuO8 +GjOzI1EdjfRzaFQ08mzv7PlzmCcKHVCZtvUFjCnlXwARAQABiQGfBCgBCAAJBQJo +gUi3Ah0CAAoJEGuEma9jOI1jI5cL/jPFGFa5Ot70Ih8sh5MdVXnHgqypYpo0DlM0 +a1sB1w/v1zLWgdQzjCXYAHgUPqyIixD2RkFccS7k/W2oK9vLqWXM/ES3jOZdGZ2f +OFoRmWtU3tOhP58W4lqpNd61lUqkN86DgFAG1OT3D0JW1lXjtEsMd1G3ijdo5ZhS +pjB14Jb67MTibENC4sIiV+N0EoHviTonajgqe5oT+BL10fYBlNLoh44mcaHiNFwd +FNugBUz5bBB9ax6ixADNxceORUnvsuuRj5L+/Z8152+RopVQlF94lb9bla5NnjVD +365mmDK7jOoqrZs/zANMzyd05BPLkLsW2RKIZvUFPp35KzL5RXZJ0nmRhMM02NlX +x9ktLc3MsgWbSf1NnKZrsh9iGxwv7EiaPsSA9gI7CpHI5NBvrAApq2wIvrSqK6Gz +FHzyFJq8bBivw09GVJvpSBY3jNpkTikel+aTBgIHDkq+MstqP9aa5rSlGdaEJfK8 +I4WOdny7ASnAZKG7pbxOhjSefKd9HYkDPgQYAQgACQUCaIFIrwIbAgGpCRBrhJmv +YziNY8DdIAQZAQgABgUCaIFIrwAKCRA+8Pk6H2Apl2WyC/0WzEM8kSzdni2unynT +WDvpnhIfPgMaAG152+sPbFotZeLUpQYBZO/j/ecE0mGf4gQHBfRKac5L+sfphhbJ +vbCXQ0BQ6NdTmyJP+VPxD5AkbbEPFSa7Xz1Grmon6AfNyrZS3uvm8BuY4e+gmDJt +IXXofULC3WI7SAa5Betudjtu4eQgGgnvG2y5y+U1lS6GNm6Uvagn/RlJX1zTK16U +NJhew9ipN45u/B1L/7KdwvnRu748DGCPiZxEU2M2YHDeAvzGB2bszUuA9wh+lVBd +rATE11poKp5vaDnuQY/BJOASio2rpxfYYnYHXYfJNnptaERfy85BVZBOVTLr5TH7 +/WiKMwNQLSFClsuiGc8vwQ8Iw9tmp8Pl5O7l1geLyTCfLbxbwIgsle1kE8jfcYzn +7/5vckaLF/44CrndSp31xbEYE+5KzXRHIYjF8KvaRq3nsNk5CGMuRdWNLIOkWFCZ +dgw9aKmw0bLL70DeHZmmjzrCcYOVLzNT6IBAsEMxLI91eB8MjAwAqI5txMVBGcIw +xi4/4E/eq8hvWLh0ENjCTf1LPx8PcBfImNgFPSLXJnRGr+ovOfJslYxxH3FzFl1n +uSY7rCZ4n6FWjoVE2uonh0IKVuFmWaIolKLSArV709NbucSSZ8YBIt55gpABj72I +cjPet4RM0oIal1m9/exrP0ePpmqz++yx+t5/sqHLhlASnRd13IqExp5x27uSP+8z +m6VRDxB4jJ2f2OamXjGWQR54USingwFJWgO6NPXOFvSrxBQYySAgSqFwpXVPdy7K +hs2iXLwUPCuWygCzcwYQe1PZG7uOVSWnydCRbrA/W+BrHo2/ZHeZamuyn+a9idOX +C2PIwuWWegNtZurAattWSeTML+YqvbwsriaZMLLyOaYTjNvB8ueiFi2iIJczDq9E +awpsDdxsdcmqEMqBXoON+8eAokh3CC2GfBdbRrySPRBk5qOSVtREjXxRbEm76C4P +w3CRx47n3m2O1iNrbb7B/1sMqT4LlVqZrTREeTzWNmc2EYlYANjo +=k/PT +-----END PGP PUBLIC KEY BLOCK----- diff --git a/signature/fixtures/public-key-with-subkey.gpg b/signature/fixtures/public-key-with-subkey.gpg new file mode 100644 index 000000000..28e288a81 --- /dev/null +++ b/signature/fixtures/public-key-with-subkey.gpg @@ -0,0 +1,49 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v2.0.22 (GNU/Linux) + +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 +=1ZyP +-----END PGP PUBLIC KEY BLOCK----- diff --git a/signature/fixtures/pubring.gpg b/signature/fixtures/pubring.gpg index 8cad08aff..fa09982a9 100644 Binary files a/signature/fixtures/pubring.gpg and b/signature/fixtures/pubring.gpg differ diff --git a/signature/fixtures/regenerate-keys.sh b/signature/fixtures/regenerate-keys.sh new file mode 100644 index 000000000..3fd484f99 --- /dev/null +++ b/signature/fixtures/regenerate-keys.sh @@ -0,0 +1,96 @@ +#! /bin/bash + +# NOTE: To generate v3 signatures, this MUST be run on a system with GPG < 2.1, e.g. a RHEL 7. +# WARNING: This lazily writes to $(pwd). It is best run on a short-term VM. + +# This is only a fragment of the ideal script; as you regenerate any other keys, please work on improving it. + +set -x + +dest=$(pwd)/new-fixtures +mkdir -p "$dest" + +function resign() { + local key_id=$1 + local signature=$2 + local other_opts=$3 + + (GNUPGHOME= gpg -d "signature/fixtures/$signature"; true) | gpg --sign --digest-algo SHA256 --default-key "$key_id" $other_opts - > "$dest/$signature" +} + +export GNUPGHOME=$(mktemp -d -t regenerate-keys.XXXXXX) +echo "GNUPGHOME: $GNUPGHOME" # Don't set up trap(1) to delete it, to allow inspection / debugging. + +# Key-Usage: auth is used because "cert" is implied, and the only one we want, but an empty value is not accepted +# by gpg. +cat >batch-input < fixtures_info +echo "TestKeyFingerprintSubkeyWithSubkey = \"$subkey_fingerprint\"" >> fixtures_info + +resign $subkey_fingerprint subkey.signature +resign $subkey_fingerprint subkey.signature-v3 --force-v3-sigs +gpg --export --armor "$fingerprint" > $dest/public-key-with-subkey.gpg + +# Key-Usage: auth is used because "cert" is implied, and the only one we want, but an empty value is not accepted +# by gpg. +cat >batch-input <> fixtures_info +echo "TestKeyFingerprintSubkeyWithRevokedSubkey = \"$subkey_fingerprint\"" >> fixtures_info + +resign $subkey_fingerprint subkey-revoked.signature +resign $subkey_fingerprint subkey-revoked.signature-v3 --force-v3-sigs + +# FIXME? Can this be fully automated? --batch alone doesn't work, --yes seems to be ignored. +# Answer "yes", "key is compromised" (NOT "no longer used", to break the subkey-revoked.signature* files created above), +# an empty message, and finally, "save" +gpg --yes --cert-digest-algo SHA256 --edit-key "$fingerprint" 'key 1' 'revkey' + +gpg --export --armor "$fingerprint" > $dest/public-key-with-revoked-subkey.gpg + + + + +# EVENTUALLY, rebuild signature/fixtures/pubring.gpg from all keys (currently impossible because this script +# does not regenerate all keys that should be present there): +# GNUPGHOME=$dest gpg --import "$dest/public-key-with-subkey.gpg" + +# === We are done. Show how the regenerated files differ. +for i in "$dest"/*; do + (echo "==== $i"; diff -u <(gpg --list-packets < "signature/fixtures/${i#$dest/}") <(gpg --list-packets < "$i")) |& less +done + +cat fixtures_info \ No newline at end of file diff --git a/signature/fixtures/subkey-revoked.signature b/signature/fixtures/subkey-revoked.signature new file mode 100644 index 000000000..1ef6a59c5 Binary files /dev/null and b/signature/fixtures/subkey-revoked.signature differ diff --git a/signature/fixtures/subkey-revoked.signature-v3 b/signature/fixtures/subkey-revoked.signature-v3 new file mode 100644 index 000000000..a971f2726 Binary files /dev/null and b/signature/fixtures/subkey-revoked.signature-v3 differ diff --git a/signature/fixtures/subkey.signature b/signature/fixtures/subkey.signature new file mode 100644 index 000000000..d26084fcf Binary files /dev/null and b/signature/fixtures/subkey.signature differ diff --git a/signature/fixtures/subkey.signature-v3 b/signature/fixtures/subkey.signature-v3 new file mode 100644 index 000000000..a4a184bbd Binary files /dev/null and b/signature/fixtures/subkey.signature-v3 differ diff --git a/signature/fixtures_info_test.go b/signature/fixtures_info_test.go index 2ab4ed05a..7053fd508 100644 --- a/signature/fixtures_info_test.go +++ b/signature/fixtures_info_test.go @@ -17,6 +17,14 @@ const ( TestKeyShortID = "E932F44B23E8DD43" // TestKeyFingerprintWithPassphrase is the fingerprint of the private key with passphrase in this directory. TestKeyFingerprintWithPassphrase = "F2B501009F78B0B340221A12A3CD242DA6028093" + // TestKeyFingerprintPrimaryWithSubkey is the primary key fingerprint of signature/fixtures/public-key-with-subkey.gpg. + TestKeyFingerprintPrimaryWithSubkey = "B9F4CDB9FD8C475BFA340AC38D54A947EE396F36" + // TestKeyFingerprintSubkeyWithSubkey is the subkey fingerprint of signature/fixtures/public-key-with-subkey.gpg. + TestKeyFingerprintSubkeyWithSubkey = "57D1D95BBC53BA0EAFF0718CD75B3109B3BAAEAF" + // TestKeyFingerprintPrimaryWithRevokedSubkey is the primary key fingerprint of signature/fixtures/public-key-with-revoked-subkey.gpg. + TestKeyFingerprintPrimaryWithRevokedSubkey = "6D88C5A1993648A17B2BB0DC6B8499AF63388D63" + // TestKeyFingerprintSubkeyWithRevokedSubkey is the subkey fingerprint of signature/fixtures/public-key-with-revoked-subkey.gpg. + TestKeyFingerprintSubkeyWithRevokedSubkey = "4D33404E00B470050709F3233EF0F93A1F602997" // TestPassphrase is the passphrase for TestKeyFingerprintWithPassphrase. TestPassphrase = "WithPassphrase123" ) diff --git a/signature/mechanism.go b/signature/mechanism.go index 1d3fe0fdc..897fc4997 100644 --- a/signature/mechanism.go +++ b/signature/mechanism.go @@ -27,7 +27,10 @@ type SigningMechanism interface { // Sign creates a (non-detached) signature of input using keyIdentity. // Fails with a SigningNotSupportedError if the mechanism does not support signing. Sign(input []byte, keyIdentity string) ([]byte, error) - // Verify parses unverifiedSignature and returns the content and the signer's identity + // Verify parses unverifiedSignature and returns the content and the signer's identity. + // For mechanisms created using NewEphemeralGPGSigningMechanism, the returned key identity + // is expected to be one of the values returned by NewEphemeralGPGSigningMechanism, + // or the mechanism should implement signingMechanismWithVerificationIdentityLookup. 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. @@ -46,6 +49,16 @@ type signingMechanismWithPassphrase interface { SignWithPassphrase(input []byte, keyIdentity string, passphrase string) ([]byte, error) } +// signingMechanismWithVerificationIdentityLookup is an internal extension of SigningMechanism. +type signingMechanismWithVerificationIdentityLookup interface { + SigningMechanism + // keyIdentityForVerificationKeyIdentity re-checks the key identity returned by Verify + // if it doesn't match an identity returned by NewEphemeralGPGSigningMechanism, trying to match it. + // (To be more specific, for mechanisms which return a subkey fingerprint from Verify, + // this converts the subkey fingerprint into the corresponding primary key fingerprint.) + keyIdentityForVerificationKeyIdentity(keyIdentity string) (string, error) +} + // SigningNotSupportedError is returned when trying to sign using a mechanism which does not support that. type SigningNotSupportedError string diff --git a/signature/mechanism_gpgme.go b/signature/mechanism_gpgme.go index 8a8c5878a..53106030f 100644 --- a/signature/mechanism_gpgme.go +++ b/signature/mechanism_gpgme.go @@ -169,7 +169,10 @@ func (m *gpgmeSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte, return m.SignWithPassphrase(input, keyIdentity, "") } -// Verify parses unverifiedSignature and returns the content and the signer's identity +// Verify parses unverifiedSignature and returns the content and the signer's identity. +// For mechanisms created using NewEphemeralGPGSigningMechanism, the returned key identity +// is expected to be one of the values returned by NewEphemeralGPGSigningMechanism, +// or the mechanism should implement signingMechanismWithVerificationIdentityLookup. func (m *gpgmeSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) { signedBuffer := bytes.Buffer{} signedData, err := gpgme.NewDataWriter(&signedBuffer) @@ -196,6 +199,24 @@ func (m *gpgmeSigningMechanism) Verify(unverifiedSignature []byte) (contents []b return signedBuffer.Bytes(), sig.Fingerprint, nil } +// keyIdentityForVerificationKeyIdentity re-checks the key identity returned by Verify +// if it doesn't match an identity returned by NewEphemeralGPGSigningMechanism, trying to match it. +// (To be more specific, for mechanisms which return a subkey fingerprint from Verify, +// this converts the subkey fingerprint into the corresponding primary key fingerprint.) +func (m *gpgmeSigningMechanism) keyIdentityForVerificationKeyIdentity(keyIdentity string) (string, error) { + // In theory, if keyIdentity refers to a subkey, the same subkey could be attached to different primary keys; + // in that case, GetKey fails with “ambiguous name”. + // We _could_ handle that, by using KeyList* (GetKey is internally just a helper for KeyList*), but sharing + // a subkey that way is very unexpected, so, for now, prefer the much simpler implementation. + key, err := m.ctx.GetKey(keyIdentity, false) + if err != nil { + return "", err + } + // In theory this value could be nil if (gpg --list-keys --with-colons) misses a "pub:" line + // or a "fpr:" line, but gpg (in recent enough versions) prints that unconditionally. // codespell:ignore fpr + return key.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 corresponds to "Key ID" for OpenPGP keys) diff --git a/signature/mechanism_gpgme_test.go b/signature/mechanism_gpgme_test.go index c5fc77308..2f8e77337 100644 --- a/signature/mechanism_gpgme_test.go +++ b/signature/mechanism_gpgme_test.go @@ -21,6 +21,8 @@ func TestMain(m *testing.M) { os.Exit(code) } +var _ signingMechanismWithVerificationIdentityLookup = &gpgmeSigningMechanism{} + func TestGPGMESigningMechanismClose(t *testing.T) { // Closing an ephemeral mechanism removes the directory. // (The non-ephemeral case is tested in the common TestGPGSigningMechanismClose) diff --git a/signature/mechanism_openpgp.go b/signature/mechanism_openpgp.go index fea8590e1..3b01df671 100644 --- a/signature/mechanism_openpgp.go +++ b/signature/mechanism_openpgp.go @@ -127,7 +127,10 @@ func (m *openpgpSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte return m.SignWithPassphrase(input, keyIdentity, "") } -// Verify parses unverifiedSignature and returns the content and the signer's identity +// Verify parses unverifiedSignature and returns the content and the signer's identity. +// For mechanisms created using NewEphemeralGPGSigningMechanism, the returned key identity +// is expected to be one of the values returned by NewEphemeralGPGSigningMechanism, +// or the mechanism should implement signingMechanismWithVerificationIdentityLookup. func (m *openpgpSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) { md, err := openpgp.ReadMessage(bytes.NewReader(unverifiedSignature), m.keyring, nil, nil) if err != nil { @@ -166,7 +169,7 @@ func (m *openpgpSigningMechanism) Verify(unverifiedSignature []byte) (contents [ } // Uppercase the fingerprint to be compatible with gpgme - return content, strings.ToUpper(fmt.Sprintf("%x", md.SignedBy.PublicKey.Fingerprint)), nil + return content, strings.ToUpper(fmt.Sprintf("%x", md.SignedBy.Entity.PrimaryKey.Fingerprint)), nil } // UntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION, diff --git a/signature/mechanism_test.go b/signature/mechanism_test.go index 566993e73..9baedb120 100644 --- a/signature/mechanism_test.go +++ b/signature/mechanism_test.go @@ -6,6 +6,7 @@ import ( "bytes" "os" "path/filepath" + "slices" "testing" "github.com/stretchr/testify/assert" @@ -135,6 +136,29 @@ func TestNewEphemeralGPGSigningMechanism(t *testing.T) { assert.Equal(t, TestKeyFingerprint, signingFingerprint, version) } + // Import of a key with a subkey + for _, c := range []struct { + fixtureName string + expectedKeyIdentity string + }{ + { + fixtureName: "public-key-with-subkey.gpg", + expectedKeyIdentity: TestKeyFingerprintPrimaryWithSubkey, + }, + { + fixtureName: "public-key-with-revoked-subkey.gpg", + expectedKeyIdentity: TestKeyFingerprintPrimaryWithRevokedSubkey, + }, + } { + keyBlob, err := os.ReadFile(filepath.Join("fixtures", c.fixtureName)) + require.NoError(t, err) + mech, keyIdentities, err := NewEphemeralGPGSigningMechanism(keyBlob) + require.NoError(t, err) + defer mech.Close() + // The primary key’s fingerprint should be returned, not any subkey. + assert.Equal(t, []string{c.expectedKeyIdentity}, keyIdentities) + } + // Two keys in a keyring: Read the binary-format pubring.gpg, and concatenate it twice. // (Using two copies of public-key.gpg, in the ASCII-armored format, works with // gpgmeSigningMechanism but not openpgpSigningMechanism.) @@ -143,7 +167,8 @@ func TestNewEphemeralGPGSigningMechanism(t *testing.T) { mech, keyIdentities, err = NewEphemeralGPGSigningMechanism(bytes.Join([][]byte{keyBlob, keyBlob}, nil)) require.NoError(t, err) defer mech.Close() - assert.Equal(t, []string{TestKeyFingerprintWithPassphrase, TestKeyFingerprint, TestKeyFingerprintWithPassphrase, TestKeyFingerprint}, keyIdentities) + expected := []string{TestKeyFingerprintWithPassphrase, TestKeyFingerprint, TestKeyFingerprintPrimaryWithSubkey, TestKeyFingerprintPrimaryWithRevokedSubkey} + assert.Equal(t, slices.Concat(expected, expected), keyIdentities) // Two keys from two blobs: keyBlob1, err := os.ReadFile("./fixtures/public-key-1.gpg") @@ -222,6 +247,23 @@ func TestGPGSigningMechanismVerify(t *testing.T) { assert.Equal(t, []byte("This is not JSON\n"), content, variant) assert.Equal(t, TestKeyFingerprint, signingFingerprint, variant) } + // 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 + } + assert.Equal(t, TestKeyFingerprintPrimaryWithSubkey, signingFingerprint, variant) + } // For extra paranoia, test that we return nil data on error. @@ -266,9 +308,58 @@ func TestGPGSigningMechanismVerify(t *testing.T) { assertSigningError(t, content, signingFingerprint, err, version) } + // 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) + } + // The various GPG/GPGME failures cases are not obviously easy to reach. } +func TestGPGSigningMechanismKeyIdentityForVerificationKeyIdentity(t *testing.T) { + mech_, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + defer mech_.Close() + mech, ok := mech_.(signingMechanismWithVerificationIdentityLookup) + if !ok { + t.Skip("SigningMechanism does not implement signingMechanismWithVerificationIdentityLookup") + } + + // Success + for _, c := range []struct { + primary, subkey string + }{ + { + primary: TestKeyFingerprintPrimaryWithSubkey, + subkey: TestKeyFingerprintSubkeyWithSubkey, + }, + { + primary: TestKeyFingerprintPrimaryWithRevokedSubkey, + subkey: TestKeyFingerprintSubkeyWithRevokedSubkey, + }, + } { + // Primary fingerprint is mapped to itself. + res, err := mech.keyIdentityForVerificationKeyIdentity(c.primary) + require.NoError(t, err) + assert.Equal(t, c.primary, res) + + // Subkey fingerprint is mapped to the primary fingerprint. + res, err = mech.keyIdentityForVerificationKeyIdentity(c.subkey) + require.NoError(t, err) + assert.Equal(t, c.primary, res) + } + // A no-subkey key is mapped to itself. + res, err := mech.keyIdentityForVerificationKeyIdentity(TestKeyFingerprint) + require.NoError(t, err) + assert.Equal(t, TestKeyFingerprint, res) + + // Key identity not found + _, err = mech.keyIdentityForVerificationKeyIdentity("unexpected fingerprint") + assert.Error(t, err) +} + func TestGPGSigningMechanismUntrustedSignatureContents(t *testing.T) { mech, _, err := NewEphemeralGPGSigningMechanism([]byte{}) require.NoError(t, err) diff --git a/signature/policy_eval_signedby.go b/signature/policy_eval_signedby.go index e5c932918..18124a613 100644 --- a/signature/policy_eval_signedby.go +++ b/signature/policy_eval_signedby.go @@ -6,7 +6,6 @@ import ( "context" "errors" "fmt" - "slices" "github.com/containers/image/v5/internal/multierr" "github.com/containers/image/v5/internal/private" @@ -50,15 +49,8 @@ func (pr *prSignedBy) isSignatureAuthorAccepted(ctx context.Context, image priva return sarRejected, nil, PolicyRequirementError("No public keys imported") } - signature, err := verifyAndExtractSignature(mech, sig, signatureAcceptanceRules{ - validateKeyIdentity: func(keyIdentity string) error { - if slices.Contains(trustedIdentities, keyIdentity) { - return nil - } - // Coverage: We use a private GPG home directory and only import trusted keys, so this should - // not be reachable. - return PolicyRequirementError(fmt.Sprintf("Signature by key %s is not accepted", keyIdentity)) - }, + signature, _, err := verifyAndExtractSignature(mech, sig, signatureAcceptanceRules{ + acceptedKeyIdentities: trustedIdentities, validateSignedDockerReference: func(ref string) error { if !pr.SignedIdentity.matchesDockerReference(image, ref) { return PolicyRequirementError(fmt.Sprintf("Signature for identity %q is not accepted", ref)) diff --git a/signature/policy_eval_signedby_test.go b/signature/policy_eval_signedby_test.go index bca2e722c..f6fa84aca 100644 --- a/signature/policy_eval_signedby_test.go +++ b/signature/policy_eval_signedby_test.go @@ -277,4 +277,11 @@ func TestPRSignedByIsRunningImageAllowed(t *testing.T) { require.NoError(t, err) allowed, err = pr.isRunningImageAllowed(context.Background(), image) assertRunningRejectedPolicyRequirement(t, allowed, err) + + // A valid signature using a subkey + image = dirImageMock(t, "fixtures/dir-img-valid-subkey", "testing/manifest:latest") + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key-with-subkey.gpg", prm) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningAllowed(t, allowed, err) } diff --git a/signature/simple.go b/signature/simple.go index 94a846593..3130cfa9a 100644 --- a/signature/simple.go +++ b/signature/simple.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "time" "github.com/containers/image/v5/signature/internal" @@ -207,37 +208,52 @@ func (s untrustedSignature) sign(mech SigningMechanism, keyIdentity string, pass // because the functions have the same or similar types, so there is a risk of exchanging the functions; // named members of this struct are more explicit. type signatureAcceptanceRules struct { - validateKeyIdentity func(string) error + acceptedKeyIdentities []string validateSignedDockerReference func(string) error validateSignedDockerManifestDigest func(digest.Digest) error } // verifyAndExtractSignature verifies that unverifiedSignature has been signed, and that its principal components -// match expected values, both as specified by rules, and returns it -func verifyAndExtractSignature(mech SigningMechanism, unverifiedSignature []byte, rules signatureAcceptanceRules) (*Signature, error) { +// match expected values, both as specified by rules. +// Returns the signature, and an identity of the key that signed it. +func verifyAndExtractSignature(mech SigningMechanism, unverifiedSignature []byte, rules signatureAcceptanceRules) (*Signature, string, error) { signed, keyIdentity, err := mech.Verify(unverifiedSignature) if err != nil { - return nil, err + return nil, "", err } - if err := rules.validateKeyIdentity(keyIdentity); err != nil { - return nil, err + if !slices.Contains(rules.acceptedKeyIdentities, keyIdentity) { + withLookup, ok := mech.(signingMechanismWithVerificationIdentityLookup) + if !ok { + return nil, "", internal.NewInvalidSignatureError(fmt.Sprintf("signature by key %s is not accepted", keyIdentity)) + } + + primaryKey, err := withLookup.keyIdentityForVerificationKeyIdentity(keyIdentity) + if err != nil { + // Coverage: This only fails if lookup by keyIdentity fails, but we just found and used that key. + // Or maybe on some unexpected I/O error. + return nil, "", err + } + if !slices.Contains(rules.acceptedKeyIdentities, primaryKey) { + return nil, "", internal.NewInvalidSignatureError(fmt.Sprintf("signature by key %s of %s is not accepted", keyIdentity, primaryKey)) + } + keyIdentity = primaryKey } var unmatchedSignature untrustedSignature if err := json.Unmarshal(signed, &unmatchedSignature); err != nil { - return nil, internal.NewInvalidSignatureError(err.Error()) + return nil, "", internal.NewInvalidSignatureError(err.Error()) } if err := rules.validateSignedDockerManifestDigest(unmatchedSignature.untrustedDockerManifestDigest); err != nil { - return nil, err + return nil, "", err } if err := rules.validateSignedDockerReference(unmatchedSignature.untrustedDockerReference); err != nil { - return nil, err + return nil, "", err } // signatureAcceptanceRules have accepted this value. return &Signature{ DockerManifestDigest: unmatchedSignature.untrustedDockerManifestDigest, DockerReference: unmatchedSignature.untrustedDockerReference, - }, nil + }, keyIdentity, nil } // GetUntrustedSignatureInformationWithoutVerifying extracts information available in an untrusted signature, diff --git a/signature/simple_test.go b/signature/simple_test.go index 06a469c89..452e30b5f 100644 --- a/signature/simple_test.go +++ b/signature/simple_test.go @@ -236,13 +236,8 @@ func TestSign(t *testing.T) { signature, err := sig.sign(mech, TestKeyFingerprint, "") require.NoError(t, err) - verified, err := verifyAndExtractSignature(mech, signature, signatureAcceptanceRules{ - validateKeyIdentity: func(keyIdentity string) error { - if keyIdentity != TestKeyFingerprint { - return errors.New("Unexpected keyIdentity") - } - return nil - }, + verified, keyIdentity, err := verifyAndExtractSignature(mech, signature, signatureAcceptanceRules{ + acceptedKeyIdentities: []string{TestKeyFingerprint}, validateSignedDockerReference: func(signedDockerReference string) error { if signedDockerReference != sig.untrustedDockerReference { return errors.New("Unexpected signedDockerReference") @@ -257,9 +252,9 @@ func TestSign(t *testing.T) { }, }) require.NoError(t, err) - assert.Equal(t, sig.untrustedDockerManifestDigest, verified.DockerManifestDigest) assert.Equal(t, sig.untrustedDockerReference, verified.DockerReference) + assert.Equal(t, TestKeyFingerprint, keyIdentity) // Error creating blob to sign _, err = untrustedSignature{}.sign(mech, TestKeyFingerprint, "") @@ -275,108 +270,146 @@ func TestVerifyAndExtractSignature(t *testing.T) { require.NoError(t, err) defer mech.Close() - type triple struct { - keyIdentity string + type tuple struct { signedDockerReference string signedDockerManifestDigest digest.Digest } - var wanted, recorded triple - // recordingRules are a plausible signatureAcceptanceRules implementations, but equally - // importantly record that we are passing the correct values to the rule callbacks. - recordingRules := signatureAcceptanceRules{ - validateKeyIdentity: func(keyIdentity string) error { - recorded.keyIdentity = keyIdentity - if keyIdentity != wanted.keyIdentity { - return errors.New("keyIdentity mismatch") - } - return nil - }, - validateSignedDockerReference: func(signedDockerReference string) error { - recorded.signedDockerReference = signedDockerReference - if signedDockerReference != wanted.signedDockerReference { - return errors.New("signedDockerReference mismatch") - } - return nil - }, - validateSignedDockerManifestDigest: func(signedDockerManifestDigest digest.Digest) error { - recorded.signedDockerManifestDigest = signedDockerManifestDigest - if signedDockerManifestDigest != wanted.signedDockerManifestDigest { - return errors.New("signedDockerManifestDigest mismatch") - } - return nil - }, + // setupRecording returns a signatureAcceptanceRules implementations for wanted, which also + // records the values passed to rule callbacks into the returned triple, to validate + // that we are passing the correct values to the rule callbacks. + setupRecording := func(wanted tuple, acceptedKeyIdentities ...string) (*tuple, signatureAcceptanceRules) { + recorded := tuple{} + return &recorded, signatureAcceptanceRules{ + acceptedKeyIdentities: acceptedKeyIdentities, + validateSignedDockerReference: func(signedDockerReference string) error { + recorded.signedDockerReference = signedDockerReference + if signedDockerReference != wanted.signedDockerReference { + return errors.New("signedDockerReference mismatch") + } + return nil + }, + validateSignedDockerManifestDigest: func(signedDockerManifestDigest digest.Digest) error { + recorded.signedDockerManifestDigest = signedDockerManifestDigest + if signedDockerManifestDigest != wanted.signedDockerManifestDigest { + return errors.New("signedDockerManifestDigest mismatch") + } + return nil + }, + } } signature, err := os.ReadFile("./fixtures/image.signature") require.NoError(t, err) - signatureData := triple{ - keyIdentity: TestKeyFingerprint, + signatureData := tuple{ signedDockerReference: TestImageSignatureReference, signedDockerManifestDigest: TestImageManifestDigest, } // Successful verification - wanted = signatureData - recorded = triple{} - sig, err := verifyAndExtractSignature(mech, signature, recordingRules) + for _, acceptedIdentities := range [][]string{ + {TestKeyFingerprint}, + {TestKeyFingerprint, "some other fingerprint"}, + {"some other fingerprint", TestKeyFingerprint}, + } { + recorded, recordingRules := setupRecording(signatureData, acceptedIdentities...) + sig, keyIdentity, err := verifyAndExtractSignature(mech, signature, recordingRules) + require.NoError(t, err) + assert.Equal(t, TestImageSignatureReference, sig.DockerReference) + assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest) + assert.Equal(t, TestKeyFingerprint, keyIdentity) + assert.Equal(t, signatureData, *recorded) + } + // Successful verification using a subkey + sig2, err := os.ReadFile("./fixtures/subkey.signature") require.NoError(t, err) - assert.Equal(t, TestImageSignatureReference, sig.DockerReference) - assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest) - assert.Equal(t, signatureData, recorded) + sig2Data := tuple{ + signedDockerReference: "testing/manifest:latest", + signedDockerManifestDigest: TestImageManifestDigest, + } + recorded, recordingRules := setupRecording(sig2Data, TestKeyFingerprintPrimaryWithSubkey) + sig, keyIdentity, err := verifyAndExtractSignature(mech, sig2, recordingRules) + require.NoError(t, err) + assert.Equal(t, sig2Data.signedDockerReference, sig.DockerReference) + assert.Equal(t, sig2Data.signedDockerManifestDigest, sig.DockerManifestDigest) + assert.Equal(t, TestKeyFingerprintPrimaryWithSubkey, keyIdentity) + assert.Equal(t, sig2Data, *recorded) - // For extra paranoia, test that we return a nil signature object on error. + // For extra paranoia, test that we return a nil signature object and a "" key identity on error. // Completely invalid signature. - recorded = triple{} - sig, err = verifyAndExtractSignature(mech, []byte{}, recordingRules) + recorded, recordingRules = setupRecording(signatureData, TestKeyFingerprint) + sig, keyIdentity, err = verifyAndExtractSignature(mech, []byte{}, recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, triple{}, recorded) + assert.Equal(t, "", keyIdentity) + assert.Equal(t, tuple{}, *recorded) - recorded = triple{} - sig, err = verifyAndExtractSignature(mech, []byte("invalid signature"), recordingRules) + recorded, recordingRules = setupRecording(signatureData, TestKeyFingerprint) + sig, keyIdentity, err = verifyAndExtractSignature(mech, []byte("invalid signature"), recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, triple{}, recorded) + assert.Equal(t, "", keyIdentity) + assert.Equal(t, tuple{}, *recorded) - // Valid signature of non-JSON: asked for keyIdentity, only - invalidBlobSignature, err := os.ReadFile("./fixtures/invalid-blob.signature") + // No key accepted. + recorded, recordingRules = setupRecording(signatureData /*, nothing */) + sig, keyIdentity, err = verifyAndExtractSignature(mech, signature, recordingRules) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, "", keyIdentity) + assert.Equal(t, tuple{}, *recorded) + + // Valid signature with a revoked subkey + sig2, err = os.ReadFile("./fixtures/subkey-revoked.signature") require.NoError(t, err) - recorded = triple{} - sig, err = verifyAndExtractSignature(mech, invalidBlobSignature, recordingRules) + recorded, recordingRules = setupRecording(sig2Data, TestKeyFingerprintPrimaryWithRevokedSubkey) // sig2Data describes subkey-revoked.signature as well. + sig, keyIdentity, err = verifyAndExtractSignature(mech, sig2, recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, triple{keyIdentity: signatureData.keyIdentity}, recorded) + assert.Equal(t, "", keyIdentity) + assert.Equal(t, tuple{}, *recorded) - // Valid signature with a wrong key: asked for keyIdentity, only - wanted = signatureData - wanted.keyIdentity = "unexpected fingerprint" - recorded = triple{} - sig, err = verifyAndExtractSignature(mech, signature, recordingRules) + // Valid signature of non-JSON: used acceptedKeyIdentities only + invalidBlobSignature, err := os.ReadFile("./fixtures/invalid-blob.signature") + require.NoError(t, err) + recorded, recordingRules = setupRecording(signatureData, TestKeyFingerprint) + sig, keyIdentity, err = verifyAndExtractSignature(mech, invalidBlobSignature, recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, triple{keyIdentity: signatureData.keyIdentity}, recorded) + assert.Equal(t, "", keyIdentity) + assert.Equal(t, tuple{}, *recorded) - // Valid signature with a wrong manifest digest: asked for keyIdentity and signedDockerManifestDigest - wanted = signatureData - wanted.signedDockerManifestDigest = "invalid digest" - recorded = triple{} - sig, err = verifyAndExtractSignature(mech, signature, recordingRules) + // Valid signature with a wrong key: used acceptedKeyIdentities only + recorded, recordingRules = setupRecording(signatureData, "unexpected fingerprint") + sig, keyIdentity, err = verifyAndExtractSignature(mech, signature, recordingRules) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, "", keyIdentity) + assert.Equal(t, tuple{}, *recorded) + + // Valid signature with a wrong manifest digest: used acceptedKeyIdentities, asked for signedDockerManifestDigest only + recorded, recordingRules = setupRecording(tuple{ + signedDockerReference: signatureData.signedDockerReference, + signedDockerManifestDigest: "invalid digest", + }, TestKeyFingerprint) + sig, keyIdentity, err = verifyAndExtractSignature(mech, signature, recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, triple{ - keyIdentity: signatureData.keyIdentity, + assert.Equal(t, "", keyIdentity) + assert.Equal(t, tuple{ signedDockerManifestDigest: signatureData.signedDockerManifestDigest, - }, recorded) + }, *recorded) // Valid signature with a wrong image reference - wanted = signatureData - wanted.signedDockerReference = "unexpected docker reference" - recorded = triple{} - sig, err = verifyAndExtractSignature(mech, signature, recordingRules) + recorded, recordingRules = setupRecording(tuple{ + signedDockerReference: "unexpected docker reference", + signedDockerManifestDigest: signatureData.signedDockerManifestDigest, + }, TestKeyFingerprint) + sig, keyIdentity, err = verifyAndExtractSignature(mech, signature, recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, signatureData, recorded) + assert.Equal(t, "", keyIdentity) + assert.Equal(t, signatureData, *recorded) } func TestGetUntrustedSignatureInformationWithoutVerifying(t *testing.T) {