Compare commits

..
1 Commits
Author SHA1 Message Date
sneak 969a707487 Compare --require-signature with the key that signed (closes #167)
check / check (push) Waiting to run
check and fetch --require-signature compared the required fingerprint
with the first key in the manifest's embedded public key block, while
gpg accepted a good signature by any key in that block.

Loading a signed manifest now refuses one whose embedded block holds
more than one primary key, counted as gpg reads the block, or whose
signer field is not the primary key fingerprint gpg reports for the
signature. --require-signature compares with the signer field, which
loading has checked. Signing names and embeds the key gpg reports it
signed with, so a key ID matching several keys still writes a manifest
that loads. docs/FORMAT.md states what a verifier checks.

Model: opus-5-5
2026-10-07 11:32:12 +00:00
2 changed files with 24 additions and 5 deletions
+20 -4
View File
@@ -172,11 +172,22 @@ func parseStatusLine(statusOutput, keyword string) ([]string, bool) {
func gpgSign(
ctx context.Context, data []byte, keyID GPGKeyID,
) ([]byte, string, error) {
// The signature goes to stdout, so the status lines go to stderr.
tmpDir, err := os.MkdirTemp("", "mfer-gpg-sign-*")
if err != nil {
return nil, "", fmt.Errorf("failed to create temp dir: %w", err)
}
defer func() { _ = os.RemoveAll(tmpDir) }()
sigFile := filepath.Join(tmpDir, "signature.asc")
// The signature goes to sigFile, so --status-fd 1 can send gpg's status
// lines to stdout; its messages go to stderr.
stdout, stderr, err := runGPG(ctx, bytes.NewReader(data),
"--detach-sign",
gpgOptArmor,
gpgOptStatusFD, "2",
"--output", sigFile,
gpgOptStatusFD, "1",
"--local-user", string(keyID),
)
if err != nil {
@@ -185,12 +196,17 @@ func gpgSign(
// The last argument of SIG_CREATED is the fingerprint of the key that
// made the signature.
created, ok := parseStatusLine(stderr.String(), "SIG_CREATED")
created, ok := parseStatusLine(stdout.String(), "SIG_CREATED")
if !ok {
return nil, "", fmt.Errorf("%w: %s", errSigningKeyNotReported, stderr.String())
}
return stdout.Bytes(), created[len(created)-1], nil
sig, err := os.ReadFile(sigFile) //nolint:gosec // G304: inside tmpDir, made above
if err != nil {
return nil, "", fmt.Errorf("failed to read signature: %w", err)
}
return sig, created[len(created)-1], nil
}
// gpgExportPublicKey exports the public key for the specified key ID.
+4 -1
View File
@@ -210,6 +210,8 @@ func TestGPGOptionLikeKeyIDIsNotAnOption(t *testing.T) {
assert.NotContains(t, string(fpr), "gpg (GnuPG)")
}
// TestGPGSignInvalidKey signs with a key that has no secret key in the
// keyring. The error must hold gpg's messages and none of its status lines.
func TestGPGSignInvalidKey(t *testing.T) {
// Set up test environment (we need GNUPGHOME set)
_, gpgHome := testGPGEnv(t)
@@ -218,7 +220,8 @@ func TestGPGSignInvalidKey(t *testing.T) {
data := []byte("test data")
_, _, err := gpgSign(context.Background(), data,
GPGKeyID("NONEXISTENT_KEY_ID_12345"))
assert.Error(t, err)
require.Error(t, err)
assert.NotContains(t, err.Error(), gpgStatusPrefix)
}
func TestBuilderWithSigning(t *testing.T) {