From 2ca34684236b13d25d7705d2915db7cbc931c0ce Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 19 Jun 2026 15:00:55 +0200 Subject: [PATCH] Return error for public-only GPG signing keyring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SelectOpenPGPSigningEntity` selects `keyRing[0]` when no key id is supplied and then calls `entity.PrivateKey.Decrypt` directly. For a keyring that contains only public keys — e.g. an armor-exported public key file — `PrivateKey` is `nil` and the call panics with a nil pointer dereference rather than surfacing an actionable error. The keyed branch already guards against this; the default branch did not. Guard the default branch with the same nil check and return an error pointing at `gpg --export-secret-keys` or `--gpg-key-id` so the user knows how to recover. Cover the public-only-keyring case in `TestSelectOpenPGPSigningEntity` so a future regression cannot re-introduce the panic. Assisted-by: claude/opus-4.7 Signed-off-by: Hidde Beydals --- pkg/bootstrap/bootstrap_plain_git.go | 4 ++++ pkg/bootstrap/bootstrap_test.go | 29 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/pkg/bootstrap/bootstrap_plain_git.go b/pkg/bootstrap/bootstrap_plain_git.go index 6ef3c32e..af1f8af3 100644 --- a/pkg/bootstrap/bootstrap_plain_git.go +++ b/pkg/bootstrap/bootstrap_plain_git.go @@ -574,6 +574,10 @@ func SelectOpenPGPSigningEntity(keyRing openpgp.EntityList, passphrase, keyID st } } else { entity = keyRing[0] + if entity.PrivateKey == nil { + return nil, fmt.Errorf("keyring does not contain a private key; " + + "export the secret key with 'gpg --export-secret-keys' or specify --gpg-key-id") + } } err := entity.PrivateKey.Decrypt([]byte(passphrase)) diff --git a/pkg/bootstrap/bootstrap_test.go b/pkg/bootstrap/bootstrap_test.go index 07256602..0bd9deab 100644 --- a/pkg/bootstrap/bootstrap_test.go +++ b/pkg/bootstrap/bootstrap_test.go @@ -542,6 +542,35 @@ func TestPlainGitBootstrapper_resolveSigner(t *testing.T) { }) } +func TestSelectOpenPGPSigningEntity(t *testing.T) { + t.Run("empty key ring errors", func(t *testing.T) { + g := NewWithT(t) + _, err := SelectOpenPGPSigningEntity(openpgp.EntityList{}, "", "") + g.Expect(err).To(MatchError(ContainSubstring("empty GPG key ring"))) + }) + + t.Run("public-only key ring without key id errors instead of panicking", func(t *testing.T) { + g := NewWithT(t) + entity, err := openpgp.NewEntity("Alice", "test", "alice@example.com", nil) + g.Expect(err).ToNot(HaveOccurred()) + entity.PrivateKey = nil + + _, err = SelectOpenPGPSigningEntity(openpgp.EntityList{entity}, "", "") + g.Expect(err).To(MatchError(ContainSubstring("keyring does not contain a private key"))) + }) + + t.Run("public-only key ring with matching key id errors with key id context", func(t *testing.T) { + g := NewWithT(t) + entity, err := openpgp.NewEntity("Alice", "test", "alice@example.com", nil) + g.Expect(err).ToNot(HaveOccurred()) + keyID := entity.PrimaryKey.KeyIdString() + entity.PrivateKey = nil + + _, err = SelectOpenPGPSigningEntity(openpgp.EntityList{entity}, "", keyID) + g.Expect(err).To(MatchError(ContainSubstring("keyring does not contain private key for key id"))) + }) +} + // TestPlainGitBootstrapper_sshSignerProducesVerifiableCommit is an // end-to-end wiring test. resolveSigner already has unit tests for // dispatch behaviour, but nothing in pkg/bootstrap exercises the full