From 979dc1a5026f906d52ba9149172abc84734cde43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Tue, 22 Jul 2025 13:44:39 +0200 Subject: [PATCH 1/4] Refactor TestVerifyAndExtractSignature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encapsulate the wanted/recorded tuples a bit more, to prepare for changes to the signatureAcceptanceRules type. Should not change (test) behavior. Signed-off-by: Miloslav Trmač --- signature/simple_test.go | 106 +++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/signature/simple_test.go b/signature/simple_test.go index 06a469c89..99ae25cf5 100644 --- a/signature/simple_test.go +++ b/signature/simple_test.go @@ -275,108 +275,116 @@ func TestVerifyAndExtractSignature(t *testing.T) { require.NoError(t, err) defer mech.Close() - type triple struct { + type tuple struct { keyIdentity string 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) (*tuple, signatureAcceptanceRules) { + recorded := tuple{} + return &recorded, 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 + }, + } } signature, err := os.ReadFile("./fixtures/image.signature") require.NoError(t, err) - signatureData := triple{ + signatureData := tuple{ keyIdentity: TestKeyFingerprint, signedDockerReference: TestImageSignatureReference, signedDockerManifestDigest: TestImageManifestDigest, } // Successful verification - wanted = signatureData - recorded = triple{} + recorded, recordingRules := setupRecording(signatureData) sig, err := verifyAndExtractSignature(mech, signature, recordingRules) require.NoError(t, err) assert.Equal(t, TestImageSignatureReference, sig.DockerReference) assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest) - assert.Equal(t, signatureData, recorded) + assert.Equal(t, signatureData, *recorded) // For extra paranoia, test that we return a nil signature object on error. // Completely invalid signature. - recorded = triple{} + recorded, recordingRules = setupRecording(signatureData) sig, err = verifyAndExtractSignature(mech, []byte{}, recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, triple{}, recorded) + assert.Equal(t, tuple{}, *recorded) - recorded = triple{} + recorded, recordingRules = setupRecording(signatureData) sig, err = verifyAndExtractSignature(mech, []byte("invalid signature"), recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, triple{}, recorded) + assert.Equal(t, tuple{}, *recorded) // Valid signature of non-JSON: asked for keyIdentity, only invalidBlobSignature, err := os.ReadFile("./fixtures/invalid-blob.signature") require.NoError(t, err) - recorded = triple{} + recorded, recordingRules = setupRecording(signatureData) sig, err = verifyAndExtractSignature(mech, invalidBlobSignature, recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, triple{keyIdentity: signatureData.keyIdentity}, recorded) + assert.Equal(t, tuple{keyIdentity: signatureData.keyIdentity}, *recorded) // Valid signature with a wrong key: asked for keyIdentity, only - wanted = signatureData - wanted.keyIdentity = "unexpected fingerprint" - recorded = triple{} + recorded, recordingRules = setupRecording(tuple{ + keyIdentity: "unexpected fingerprint", + signedDockerReference: signatureData.signedDockerReference, + signedDockerManifestDigest: signatureData.signedDockerManifestDigest, + }) sig, err = verifyAndExtractSignature(mech, signature, recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, triple{keyIdentity: signatureData.keyIdentity}, recorded) + assert.Equal(t, tuple{keyIdentity: signatureData.keyIdentity}, *recorded) // Valid signature with a wrong manifest digest: asked for keyIdentity and signedDockerManifestDigest - wanted = signatureData - wanted.signedDockerManifestDigest = "invalid digest" - recorded = triple{} + recorded, recordingRules = setupRecording(tuple{ + keyIdentity: signatureData.keyIdentity, + signedDockerReference: signatureData.signedDockerReference, + signedDockerManifestDigest: "invalid digest", + }) sig, err = verifyAndExtractSignature(mech, signature, recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, triple{ + assert.Equal(t, tuple{ keyIdentity: signatureData.keyIdentity, signedDockerManifestDigest: signatureData.signedDockerManifestDigest, - }, recorded) + }, *recorded) // Valid signature with a wrong image reference - wanted = signatureData - wanted.signedDockerReference = "unexpected docker reference" - recorded = triple{} + recorded, recordingRules = setupRecording(tuple{ + keyIdentity: signatureData.keyIdentity, + signedDockerReference: "unexpected docker reference", + signedDockerManifestDigest: signatureData.signedDockerManifestDigest, + }) sig, err = verifyAndExtractSignature(mech, signature, recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, signatureData, recorded) + assert.Equal(t, signatureData, *recorded) } func TestGetUntrustedSignatureInformationWithoutVerifying(t *testing.T) { From 42276d2cf464afebd696b5253bb7a938690a3abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 21 Jul 2025 20:49:13 +0200 Subject: [PATCH 2/4] Replace signatureAcceptanceRules.validateKeyIdentity by acceptedPrimaryKeyFingerprints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move more code into the centralized function, to allow us to change the implementation in the future. Signed-off-by: Miloslav Trmač --- signature/docker.go | 12 +--- signature/policy_eval_signedby.go | 12 +--- signature/simple.go | 22 +++---- signature/simple_test.go | 99 ++++++++++++++++--------------- 4 files changed, 66 insertions(+), 79 deletions(-) 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/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/simple.go b/signature/simple.go index 94a846593..f68d16a3c 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,38 @@ 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) { + return nil, "", internal.NewInvalidSignatureError(fmt.Sprintf("signature by key %s is not accepted", keyIdentity)) } 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 99ae25cf5..2839d7c97 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, "") @@ -276,23 +271,16 @@ func TestVerifyAndExtractSignature(t *testing.T) { defer mech.Close() type tuple struct { - keyIdentity string signedDockerReference string signedDockerManifestDigest digest.Digest } // 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) (*tuple, signatureAcceptanceRules) { + setupRecording := func(wanted tuple, acceptedKeyIdentities ...string) (*tuple, signatureAcceptanceRules) { recorded := tuple{} return &recorded, signatureAcceptanceRules{ - validateKeyIdentity: func(keyIdentity string) error { - recorded.keyIdentity = keyIdentity - if keyIdentity != wanted.keyIdentity { - return errors.New("keyIdentity mismatch") - } - return nil - }, + acceptedKeyIdentities: acceptedKeyIdentities, validateSignedDockerReference: func(signedDockerReference string) error { recorded.signedDockerReference = signedDockerReference if signedDockerReference != wanted.signedDockerReference { @@ -313,77 +301,90 @@ func TestVerifyAndExtractSignature(t *testing.T) { signature, err := os.ReadFile("./fixtures/image.signature") require.NoError(t, err) signatureData := tuple{ - keyIdentity: TestKeyFingerprint, signedDockerReference: TestImageSignatureReference, signedDockerManifestDigest: TestImageManifestDigest, } // Successful verification - recorded, recordingRules := setupRecording(signatureData) - sig, err := verifyAndExtractSignature(mech, signature, recordingRules) - require.NoError(t, err) - assert.Equal(t, TestImageSignatureReference, sig.DockerReference) - assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest) - assert.Equal(t, signatureData, *recorded) + 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) + } - // 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, recordingRules = setupRecording(signatureData) - 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, "", keyIdentity) assert.Equal(t, tuple{}, *recorded) - recorded, recordingRules = setupRecording(signatureData) - 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, "", keyIdentity) assert.Equal(t, tuple{}, *recorded) - // Valid signature of non-JSON: asked for keyIdentity, only + // 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 of non-JSON: used acceptedKeyIdentities only invalidBlobSignature, err := os.ReadFile("./fixtures/invalid-blob.signature") require.NoError(t, err) - recorded, recordingRules = setupRecording(signatureData) - sig, err = verifyAndExtractSignature(mech, invalidBlobSignature, recordingRules) + recorded, recordingRules = setupRecording(signatureData, TestKeyFingerprint) + sig, keyIdentity, err = verifyAndExtractSignature(mech, invalidBlobSignature, recordingRules) assert.Error(t, err) assert.Nil(t, sig) - assert.Equal(t, tuple{keyIdentity: signatureData.keyIdentity}, *recorded) + assert.Equal(t, "", keyIdentity) + assert.Equal(t, tuple{}, *recorded) - // Valid signature with a wrong key: asked for keyIdentity, only - recorded, recordingRules = setupRecording(tuple{ - keyIdentity: "unexpected fingerprint", - signedDockerReference: signatureData.signedDockerReference, - signedDockerManifestDigest: signatureData.signedDockerManifestDigest, - }) - 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, tuple{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 + // Valid signature with a wrong manifest digest: used acceptedKeyIdentities, asked for signedDockerManifestDigest only recorded, recordingRules = setupRecording(tuple{ - keyIdentity: signatureData.keyIdentity, signedDockerReference: signatureData.signedDockerReference, signedDockerManifestDigest: "invalid digest", - }) - sig, err = verifyAndExtractSignature(mech, signature, recordingRules) + }, TestKeyFingerprint) + sig, keyIdentity, err = verifyAndExtractSignature(mech, signature, recordingRules) assert.Error(t, err) assert.Nil(t, sig) + assert.Equal(t, "", keyIdentity) assert.Equal(t, tuple{ - keyIdentity: signatureData.keyIdentity, signedDockerManifestDigest: signatureData.signedDockerManifestDigest, }, *recorded) // Valid signature with a wrong image reference recorded, recordingRules = setupRecording(tuple{ - keyIdentity: signatureData.keyIdentity, signedDockerReference: "unexpected docker reference", signedDockerManifestDigest: signatureData.signedDockerManifestDigest, - }) - sig, err = verifyAndExtractSignature(mech, signature, recordingRules) + }, TestKeyFingerprint) + sig, keyIdentity, err = verifyAndExtractSignature(mech, signature, recordingRules) assert.Error(t, err) assert.Nil(t, sig) + assert.Equal(t, "", keyIdentity) assert.Equal(t, signatureData, *recorded) } From c85a5c863e0aff22eeb97365581274f42459f98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Tue, 22 Jul 2025 15:05:15 +0200 Subject: [PATCH 3/4] Implement and test subkey matching in SigningMechanism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document how the key identities from NewEphemeralGPGSigningMechanism and SigningMechanism should relate. For the GPGME implementation where the relationship isn't as desired, introduce signingMechanismWithVerificationIdentityLookup . For the openpgp implementation, just fix the returned value to match new expecations. Currently, the simple signing verifyAndExtractSignature rejects subkey signatures for both mechanism implementations, so this does not change behavior for any previously-accepted signatures (but it might affect theoretical external direct callers of SigningMechanism). We will actually use this in the next commit. WARNING: Adding support for subkeys makes subkey revocation relevant. Revocation is always messy - to support long-term signature validity (which is very relevant for container image signing), smooth handling of revocation might require trusted timestamps (not natively supported in OpenPGP). In practice, this library (partially out of necessity) lets the underlying OpenPGP implementations impose their own policy, and that handling DIFFERS between various SigningMechanism implementations: it seems that GnuPG and refuses to use a revoked subkey regardless of revocation time, while Sequoia-PGP decides based on the revocation reason whether the revocation applies to all signatures or only to signatures (supposedly!!) made after the revocation happened. It is STRONGLY recommended to avoid all of this complexity, and to plan for workflows where subkey revocation never happens. Just revoke and reissue the whole key - either way you'll need to distribute an updated key to all consumers. Signed-off-by: Miloslav Trmač --- .../public-key-with-revoked-subkey.gpg | 58 +++++++++++ signature/fixtures/public-key-with-subkey.gpg | 49 +++++++++ signature/fixtures/pubring.gpg | Bin 3581 -> 8319 bytes signature/fixtures/regenerate-keys.sh | 96 ++++++++++++++++++ signature/fixtures/subkey-revoked.signature | Bin 0 -> 658 bytes .../fixtures/subkey-revoked.signature-v3 | Bin 0 -> 649 bytes signature/fixtures/subkey.signature | Bin 0 -> 657 bytes signature/fixtures/subkey.signature-v3 | Bin 0 -> 650 bytes signature/fixtures_info_test.go | 8 ++ signature/mechanism.go | 15 ++- signature/mechanism_gpgme.go | 23 ++++- signature/mechanism_gpgme_test.go | 2 + signature/mechanism_openpgp.go | 7 +- signature/mechanism_test.go | 93 ++++++++++++++++- 14 files changed, 346 insertions(+), 5 deletions(-) create mode 100644 signature/fixtures/public-key-with-revoked-subkey.gpg create mode 100644 signature/fixtures/public-key-with-subkey.gpg create mode 100644 signature/fixtures/regenerate-keys.sh create mode 100644 signature/fixtures/subkey-revoked.signature create mode 100644 signature/fixtures/subkey-revoked.signature-v3 create mode 100644 signature/fixtures/subkey.signature create mode 100644 signature/fixtures/subkey.signature-v3 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) + +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 +=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) + +mQGNBGiBSK4BDADj//Hv7pbme3RnHTt76n8AkI49kLQhHEtkgQwOtPgVJ+RAYEab +oLGnNpXUaxjVDVrKe9lN6Q81Grhp+EfNfoq/mzWsvvCi1D3/f5eaCL8B4jHCMC86 +GYoGmca6bIcspxx3InNpdZLkVuWAU5b+IlmngZcy6V5h10H6Nr8FC6RNXSxdSpFC +sqqFPgIG5GUdDjDRQa/1x4u2hItMLlfJMZtOytRcgpM1Rsv9PqrgADaoyOaDBZfR +cDnc82EYvqu+05wxK/XdY+YVEEXwDFK6/iJ/N95eDgxhgnOojK6eV/PoqV0ED1VY +BqZ4X0tOiggrOWWmh9yHlpM+EF6NY5j69nYFc7rxm8TlxhU/20+xzEXO49RMEgCg +maVI5kpLYnLsc6zvYSJKO2nn6lsruZRsGYKE9dM4yWdRlHn912PE/+FA0dHYHBFM +GjTfyWdxV1KUwoJmm+/m82Zg/xahtTt7evQYoLKLAFgGEdp98+HPW4xraLnccw05 +/m1Z0dMeL59zsUsAEQEAAbQcYy9pbWFnZSB0ZXN0IGtleSB3aXRoIHN1YmtleYkB +uQQTAQgAIwUCaIFIrgIbIQcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJEI1U +qUfuOW8283wMALWRo6PfYEe8UwVsnHzsS6uJ7kE5wY2z15YPfU5Xs7efOG/wZmWM +gmPRxcsnr5CVe5jDCwIGRur7LFsg1xlogki3vCSsGtvHso+n69oZZQZ4gsiEB7yb +rCyQwGSevK7rVUd5jinbXnCyEH5pbZuFb8shi5ifuNxKe4xs2MDcFAaSGviv4OsX +JSjx0Q3Cj20LcU+2xHqS/uhVS7nPznNgiPUiqFl8yfkJ31suetu3ZARCzNBxUfTK +cTVkC/pAaqIoGJp47b3CVkrRM1yUzjPbpYBDVA4Q1lXSWwQQb8l/tJWbTPhFCVfY +8qQ/KgHtGViqXWLp5viUWPK3kKlYgiozrX689PtBxkOcSoxECHjIrFAkgrgYDUcn +y37YkJSKkI336nGfzZIi0EPPjCS3zVF7D0hoBXtpxv2smFr1I4wvksLyG9uEEVi9 +08Lj6/IGsIxCZ/is3S9ITZWc9Ddpf1KE9tL5+ijxmb8H/uXDBNxx52hoUZtm+HVf +lFitliC09wOgHbkBjQRogUiuAQwAuFv3LEesTK4mYSoQO/Cqy1kEFiRI/srXX6HH +41+gsCJvHHi0Pdn9Ybio2faYdHiyt1lO1CuE4Ti32W+aTN73GEJNoywlYIt5FbR0 +1hdm4zjILoZhvM65iiA6MS/lCKmtgfk5+YAKSJv3FM9IVS+/Hn8STSXxxOTafj/5 +7L3M4ictG2IBG0lhtXyfVgchaj+ikhaagSSiCYi3uyKZcq5cbPy8Uo/qsChHTjK2 +DbqnSTI8Wghw40Nb0E3qVEdAaex3wm8hW2CZQ/yQhAic6b/JNmb90B4R8hTXD07z +0TyeU0Kh63GZEGOCLqwc9TkaZ2V0LeoNpcg1Bi2DUTW/GRLsWMvSRxg7VrbKpNWB +OTreJUUIIFuA/y0/Id+odkCMvNyuAB3Sb2h883KrITo8H066xpcwuBU0exZb/ItX +Rd7zvd8SKZ4HgSV9R1+yxDWS2hfoSLGpv1ZQzCKMkWnq30fAdlJoGrrdShS++5UW +JyAc6lMgx83wRFjvJgqTwmyT17upABEBAAGJAz4EGAEIAAkFAmiBSK4CGwIBqQkQ +jVSpR+45bzbA3SAEGQEIAAYFAmiBSK4ACgkQ11sxCbO6rq+ffQv+LbOJ9C+1VZ/b +mTWBbRCttv6SO6S9cJFNsMG6TqijmRa9ck+WVfnmJiSTsBczZwUpitEJZxM0NR6q +PSNoMiHCncplerVFIMNBNYCn0a0nzHXSqwdjf6XRyzPbYlfsV3u9owDzdxHrys/c +lNjy6Up/jXIWgisffLr+gpfaklhtMlWZAR0PttgsUQnizjJHVEEgMBlK3HUNScgH +U1CjqHRG6T5O3K4kWGvczq7gl8Sw+QEhKKmucsdCkCKwnYGKplot9vBnfKhotaPj +kOJHkSmsvyxNWJrhAhLy7EnEZhe50GEeMLcq82bCGA/te/slOMyHRMUsrIjUFnb5 +cViOtf4/m2rR0kjt2yv/VUEZtgHGv2Stnd3qjN5Q73j7VY6qH380zWxKQMqEBOto +k6Z08s2qHoC1kn1/B79HKO8jmAZexZdnfZ8191DBC3A96rxn8qJOZz9O0/rT7Zlo +mfUnPOwmIVTnn/0/GTsSD1FS0HakSl3a8+/D7iqDf23Ux3kV2tYx6k8L/0sEqzmC +DFpH1nNCcWGSXD46cLO0c2MXh5a5k/oqgQbSH14BAW4IqYw48G5gMciK1LiwGrAd +8gTIyb6Go/fu79pZVBh1AbIVRWY9D0Ex0psR9wnuo+HmSGKLRiFo5CUpi8MzB/H0 +sMnFPXrog/1zMgsp6iph802TBsMz+ny3/Ayjogb6802w974UQz5zjllp+aZLC83E +uoefPOJpUpQ7LamC0Db6sjH9mPv1QTJHN+bQXMTr3KxSiAzr/ROA0yyfvmLZkNQ4 +zcxbJDMdoB6hZCKTsWtoebkATgSEox7WT/vJqIJVKu/+Eg/Q5PW+Le2osCMVPPWk +LL93YzLfFc/tBrORd5irvbskzT+/8Sesv71yCaCSEoPZV+9C/jTHK99kH0UVbIEl +bA7x6z0okhKDaav5cJhxwoJRbmQtNXRk3VfFXDoLIdwybaF662fyY9ws3fc2h8Ok +dw3zGw7BCuwbFfgSwKqKFGjjBnbQFpzOg8mThA6KbTPYMeG+yutBLwsxpw== +=1ZyP +-----END PGP PUBLIC KEY BLOCK----- diff --git a/signature/fixtures/pubring.gpg b/signature/fixtures/pubring.gpg index 8cad08aff761cc200306ddeb2e9fe202aad30abf..fa09982a9105870f6b0f50709c9e1c76322633b5 100644 GIT binary patch delta 4783 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 delta 7 OcmezG@K<`nUtRza^aK(B 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 0000000000000000000000000000000000000000..1ef6a59c5ec24ce127f1597991ae040141a9a8ec GIT binary patch literal 658 zcmV;D0&V@H0h_?f%)r5D_u;3Ne1hh5#?uFMlNd4@JvLM;B^PCuWF{x(C|Ol2Wu~O& zm1LGwg4ikf$=Rtzx<#pJsYR)I$*D?KN+qeqC7F5Y`nidDnQ1__RZb#ENU2swDKj@Q zJrzkCOo?tvW;#fTl~Qp=qLHbYm61VGnvr2@iixRtlChXZSvW2;&X`*>j zYNDZ;k(s5XVOmO}nSpVtL5hK4l1Zw0nwf!#k$I}ADaf*t$^wvc5=-)PGm{mP^Ycm) zGxJi56pAy`^Abx+i&8;~@(W5b^Yg&|s;%v0oWsJv$icwI$^`N?0~ep@+;L;f@AT!8_dZg!bCq zu221W&+6u)*ArKrOFp!?=48OKjBht=_X(s0-`VRWu*i|yy(O=K(_`U<;(q+zi>v=N-7?+V+(*L(DVV^aA7#KFZ^sUi4w6-8J{4sGL0 z1#67IUHK-V&EB&1@%G$O;|uBw=NuF4d3xeB_xj))iZyXX!9KFIN6|P z_F(72iuo_S=2o8wtx$VYP}!^IvG0w0b16$|ZAxABo;N3LOtxfQ`*Sz7u}3$wFnyM0 zLG_&pTcw@drdoU|VXlaoSU4^6+;zQww^xgId2}9=h&i$N;`J6)nXUG#rIOcJ&D@%N s_2sq5Ei(-2*tezTedxQqR`k)NohfS#_O*ot?cMw|X7R}j%Gpiv04(lbO8@`> literal 0 HcmV?d00001 diff --git a/signature/fixtures/subkey-revoked.signature-v3 b/signature/fixtures/subkey-revoked.signature-v3 new file mode 100644 index 0000000000000000000000000000000000000000..a971f2726589cb3eb5f0bffb648d608fe6198b58 GIT binary patch literal 649 zcmV;40(SkQ0h_?f%)r5D_u;3Ne1hh5#?uFMlNd4@JvLM;B^PCuWF{x(C|Ol2Wu~O& zm1LGwg4ikf$=Rtzx<#pJsYR)I$*D?KN+qeqC7F5Y`nidDnQ1__RZb#ENU2swDKj@Q zJrzkCOo?tvW;#fTl~Qp=qLHbYm61VGnvr2@iixRtlChXZSvW2;&X`*>j zYNDZ;k(s5XVOmO}nSpVtL5hK4l1Zw0nwf!#k$I}ADaf*t$^wvc5=-)PGm{mP^Ycm) zGxJi56pAy`^Abx+i&8;~@(W5b^Yg&|s;%v0oXX4!_AopMICB5;Ff2NJ!+43&Hc9!i zkZBrQ_qcL3zCJ#+LJ$5s$eySfQ ze)q+!$Trb=b8eM(#Z*hWmd2`G+s$dXR^W}}^<0Zbn!bzxz5 zT6_A0|6)rScFbE7eL#)r)yf~6)TT?Neojx#e7rd9@~mfW=U*RkvRQ66VZ)ZaSKn`q zEw?n)Gk9Uj(EnPaZno#klh3)XKhmF{rIR`@^6KWzzIAit?ylW!<*uk|+aI}o<%cIa zW&3pmFYgvOzVeY&dZf#TEi3o%Ets6$vD|qZ1MlJc*JMhU9yn@}sW0!%=pWfNeRB7K j|F8Z`Iv6qceuTf2aGo+>N{wE-Pc7%8I$bR;)>qj8*IHST literal 0 HcmV?d00001 diff --git a/signature/fixtures/subkey.signature b/signature/fixtures/subkey.signature new file mode 100644 index 0000000000000000000000000000000000000000..d26084fcfe6c79dbdb52a00fdea05e6007725483 GIT binary patch literal 657 zcmV;C0&e}I0h_?f%)r5TJ=&0S^R9L48BZV3O=8Gs^jKf5lw6cql9`;CqhwXBl$ny6 zSCUy$31X+@CugS?=@zA?r52^;C8sJ`DV3xamt^Lp>*prsWu^hyRym0vA*EU!rOe#K z^i(8mFeSPvndu-UR!YSgiAJVoRz?O%X-0;rDJG`oNye527DkrIDM_iR$rk38ritcB zsfmVWMrM|lhG{8@W(LNo1}O%HNhYc0X=Vl{M&_xerXb5oDhoi)Ni50F%}iEE&d)1J z%*;zIQYg+$&r2*RElLF`$}cF%%+CY+tG2e2aSjUuBL@Q;D-+1q3|yQ7u&_95$^AdE zFJ_v4)cwTt_^tg@?abKCFK?$`=VJ`5cUk>Rw$$kOuH@w7yH}o&Sag4;Xk&*|U1Q{fGwXJB zt=fFveUWLVxaNVc?wgW5i+LaaoA}Vk>5@se0@Ew+)atdVrv85qP2W^llFT;Q;JSv> z*2!rSiV<>79LbSEVv=3^l8RoKY`YMD_u_$TbAoQSxSGcvoV{;>0IimwD|x z9rr0{*u*#=aZ^3_eAd=tg_*o8Q2{@t6=tic-S1s6la=#N%DWuT1)r5SYU|o}-~V!T zZ4YPvOWsTORCeb6;V}yS5NhMH`B&dg=MS8R#hTnVPV-QV4^hcjZ}K>6ee6>f$C>S{ rH!As`F?_x7c*?^%uPfRqT6sYyP2bdSnBj6dW!;3%g+AV{vRAYK8zWn? literal 0 HcmV?d00001 diff --git a/signature/fixtures/subkey.signature-v3 b/signature/fixtures/subkey.signature-v3 new file mode 100644 index 0000000000000000000000000000000000000000..a4a184bbdcce823268bc496a8291bc6ed70abb21 GIT binary patch literal 650 zcmV;50(JeP0h_?f%)r5TJ=&0S^R9L48BZV3O=8Gs^jKf5lw6cql9`;CqhwXBl$ny6 zSCUy$31X+@CugS?=@zA?r52^;C8sJ`DV3xamt^Lp>*prsWu^hyRym0vA*EU!rOe#K z^i(8mFeSPvndu-UR!YSgiAJVoRz?O%X-0;rDJG`oNye527DkrIDM_iR$rk38ritcB zsfmVWMrM|lhG{8@W(LNo1}O%HNhYc0X=Vl{M&_xerXb5oDhoi)Ni50F%}iEE&d)1J z%*;zIQYg+$&r2*RElLF`$}cF%%+CY+tG2e2aVj$_*u(H3;COSHhhdxC^{kGjd7Aov zrWd?8H0`hZ^`^i#sb{CnJZ89(V{_wIsauu%^q1MNT(Xg2-p_T>=}VYPk9#P~;&8sj zw@Y`)r#croPWhaeU)Fz{eRZ1Cb=3ACq8YJ3^afv=TlRjy;R9E!tyN@mA`Cfe#AOAP?uh-|#Tex@W7$;U686?Cl zC^);bd-kHj%Kgdd@13~A4@|6@c6Nn{+(wS05B7vk;6CNs#&Md(=vLyQSrO`LZW0ki zKTBRax(Vvv(I~2@+wyS@k9@$j{s*SFo*opr_au1zA03m&7iD_(>`2UA`}4hyUD8f> kzKDxICNJN)NX Date: Tue, 22 Jul 2025 15:05:53 +0200 Subject: [PATCH 4/4] Also accept subkey signatures in simple signing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This does not change behavior for mechanisms which already always return the primary key fingerprint (after a recent change, openpgp); for the others, add support for signingMechanismWithVerificationIdentityLookup . Signed-off-by: Miloslav Trmač --- .../dir-img-valid-subkey/manifest.json | 1 + .../fixtures/dir-img-valid-subkey/signature-1 | 1 + signature/policy_eval_signedby_test.go | 7 +++++ signature/simple.go | 16 ++++++++++- signature/simple_test.go | 28 +++++++++++++++++-- 5 files changed, 50 insertions(+), 3 deletions(-) create mode 120000 signature/fixtures/dir-img-valid-subkey/manifest.json create mode 120000 signature/fixtures/dir-img-valid-subkey/signature-1 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/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 f68d16a3c..3130cfa9a 100644 --- a/signature/simple.go +++ b/signature/simple.go @@ -222,7 +222,21 @@ func verifyAndExtractSignature(mech SigningMechanism, unverifiedSignature []byte return nil, "", err } if !slices.Contains(rules.acceptedKeyIdentities, keyIdentity) { - return nil, "", internal.NewInvalidSignatureError(fmt.Sprintf("signature by key %s is not accepted", 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 diff --git a/signature/simple_test.go b/signature/simple_test.go index 2839d7c97..452e30b5f 100644 --- a/signature/simple_test.go +++ b/signature/simple_test.go @@ -319,12 +319,26 @@ func TestVerifyAndExtractSignature(t *testing.T) { 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) + 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 and a "" key identity on error. // Completely invalid signature. - recorded, recordingRules := setupRecording(signatureData, TestKeyFingerprint) - sig, keyIdentity, 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, "", keyIdentity) @@ -345,6 +359,16 @@ func TestVerifyAndExtractSignature(t *testing.T) { 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, 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, "", keyIdentity) + assert.Equal(t, tuple{}, *recorded) + // Valid signature of non-JSON: used acceptedKeyIdentities only invalidBlobSignature, err := os.ReadFile("./fixtures/invalid-blob.signature") require.NoError(t, err)