Compare --require-signature with the key that signed (closes #167) #171

Merged
clawbot merged 1 commits from issue-167-require-signature-signing-key into next 2026-10-07 13:59:18 +02:00
Collaborator

Fixes #167.

check and fetch with --require-signature compared the required fingerprint with the first key listed in the manifest's embedded public key block, while gpg --verify accepted a good signature by any key in that block. A manifest carrying the required signer's public key followed by the attacker's own key, and signed by the latter, passed.

Loading a signed manifest (package mfer, so every caller gets it) now refuses it unless the embedded block holds exactly one primary key, counted by gpg as it reads the block (keys it then skips included), and the signer field equals the primary key fingerprint gpg reports on its VALIDSIG status line. --require-signature compares with Checker.Signer(), which loading has checked. Signing passes --sign-key to gpg as given, then names and embeds the key gpg reports on its SIG_CREATED line, resolved to its primary key. docs/FORMAT.md states what a verifier checks.

What the diff does not show:

  • A manifest whose embedded block holds two keys, or whose signer is not the signing key, is now refused on load even without --require-signature.
  • Checker.ExtractEmbeddedSigningKeyFP is removed; it was the key listing the check relied on.
  • The --require-signature mismatch message is unchanged.

Disclosures:

  • Judgement call: a signature holding more than one good signature is refused.
  • Judgement call: Checker.Signer() returns nil for an unsigned manifest, as its doc comment already said; it returned the raw field.
  • Rule suppressed: gosec G304 where gpgSign reads the signature back from the temporary directory it made.

Model: opus-5-5

Fixes https://git.eeqj.de/sneak/mfer/issues/167. `check` and `fetch` with `--require-signature` compared the required fingerprint with the first key listed in the manifest's embedded public key block, while `gpg --verify` accepted a good signature by any key in that block. A manifest carrying the required signer's public key followed by the attacker's own key, and signed by the latter, passed. Loading a signed manifest (package `mfer`, so every caller gets it) now refuses it unless the embedded block holds exactly one primary key, counted by gpg as it reads the block (keys it then skips included), and the `signer` field equals the primary key fingerprint gpg reports on its `VALIDSIG` status line. `--require-signature` compares with `Checker.Signer()`, which loading has checked. Signing passes `--sign-key` to gpg as given, then names and embeds the key gpg reports on its `SIG_CREATED` line, resolved to its primary key. `docs/FORMAT.md` states what a verifier checks. What the diff does not show: - A manifest whose embedded block holds two keys, or whose `signer` is not the signing key, is now refused on load even without `--require-signature`. - `Checker.ExtractEmbeddedSigningKeyFP` is removed; it was the key listing the check relied on. - The `--require-signature` mismatch message is unchanged. Disclosures: - Judgement call: a `signature` holding more than one good signature is refused. - Judgement call: `Checker.Signer()` returns nil for an unsigned manifest, as its doc comment already said; it returned the raw field. - Rule suppressed: gosec G304 where `gpgSign` reads the signature back from the temporary directory it made. Model: opus-5-5
clawbot added the needs-reviewcritical labels 2026-10-07 11:34:08 +02:00
clawbot self-assigned this 2026-10-07 11:34:08 +02:00
Author
Collaborator

Review failed.

  1. mfer/serialize.go, signOuter: signing now takes the first key gpg lists for the --sign-key value in the public keyring and signs with that key's fingerprint. When the first key listed for that name cannot sign (an older revoked or expired key with the same user ID, or another person's public key with the same address and no secret key here), gen and freshen with --sign-key now fail with "Unusable secret key" or "No secret key", where next signed with the key gpg picks itself. It also replaces a KEYID! subkey choice with the default signing subkey. Acceptable: sign with the --sign-key value as given, then name and embed the key gpg reports it signed with (the fingerprint on the SIG_CREATED status line, resolved to its primary key), with a test where the first key listed for the user ID is revoked.

  2. mfer/gpg.go, gpgImportOneKey: the keys are counted in the temporary keyring after import, not in the embedded block. A block holding two primary keys, one of which gpg skips on import (for example a key with no user ID), loads: a block whose first key is the required signer's key with its user ID removed, followed by the key that signed, is accepted. That misses the issue's "refuses a signed manifest whose embedded block holds more than one primary key", and the new docs/FORMAT.md sentence that signingPubKey "holds exactly one primary key" is not true of the code. Acceptable: count the keys in the block itself (for example the processed count on gpg's IMPORT_RES status line), with a test for a block whose extra key gpg skips on import.

  3. Tests: no test signs with a subkey. parseSigningKey must take the last field of the VALIDSIG line (the primary key), not the first (the subkey that signed); the test keys sign with their primary key, where the two are the same, so taking the wrong field passes every test. Nothing tests the refusal of a signature holding two good signatures either. Acceptable: a test with a certify-only primary key and a signing subkey whose manifest loads with signer equal to the primary fingerprint, and a test that two good signatures in signature are refused.

Reviewed against next at 2a174e3.

Model: opus-5-5

Review failed. 1. `mfer/serialize.go`, `signOuter`: signing now takes the first key gpg lists for the `--sign-key` value in the public keyring and signs with that key's fingerprint. When the first key listed for that name cannot sign (an older revoked or expired key with the same user ID, or another person's public key with the same address and no secret key here), `gen` and `freshen` with `--sign-key` now fail with "Unusable secret key" or "No secret key", where `next` signed with the key gpg picks itself. It also replaces a `KEYID!` subkey choice with the default signing subkey. Acceptable: sign with the `--sign-key` value as given, then name and embed the key gpg reports it signed with (the fingerprint on the `SIG_CREATED` status line, resolved to its primary key), with a test where the first key listed for the user ID is revoked. 2. `mfer/gpg.go`, `gpgImportOneKey`: the keys are counted in the temporary keyring after import, not in the embedded block. A block holding two primary keys, one of which gpg skips on import (for example a key with no user ID), loads: a block whose first key is the required signer's key with its user ID removed, followed by the key that signed, is accepted. That misses the issue's "refuses a signed manifest whose embedded block holds more than one primary key", and the new `docs/FORMAT.md` sentence that `signingPubKey` "holds exactly one primary key" is not true of the code. Acceptable: count the keys in the block itself (for example the processed count on gpg's `IMPORT_RES` status line), with a test for a block whose extra key gpg skips on import. 3. Tests: no test signs with a subkey. `parseSigningKey` must take the last field of the `VALIDSIG` line (the primary key), not the first (the subkey that signed); the test keys sign with their primary key, where the two are the same, so taking the wrong field passes every test. Nothing tests the refusal of a `signature` holding two good signatures either. Acceptable: a test with a certify-only primary key and a signing subkey whose manifest loads with `signer` equal to the primary fingerprint, and a test that two good signatures in `signature` are refused. Reviewed against `next` at `2a174e3`. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-07 12:36:05 +02:00
clawbot force-pushed issue-167-require-signature-signing-key from 7f0bcfb228 to 96f879912c 2026-10-07 13:07:10 +02:00 Compare
Author
Collaborator

Rework for the review above:

  1. signOuter signs with the --sign-key value as given, then names and embeds the key on gpg's SIG_CREATED line, resolved to its primary key, so a KEYID! subkey choice is kept. The test makes the first key for the user ID expired rather than revoked: gpg makes an expired key from key generation parameters, while revoking one in batch mode means editing its stored revocation certificate.
  2. Keys are counted from the first field of gpg's IMPORT_RES line, which includes keys gpg skips; new test with an extra key whose user ID was removed.
  3. New tests: a certify-only primary key with a signing subkey, where the first and last VALIDSIG fields differ, loads with signer equal to the primary fingerprint; a signature holding its good signature twice is refused.

Model: opus-5-5

Rework for the review above: 1. `signOuter` signs with the `--sign-key` value as given, then names and embeds the key on gpg's `SIG_CREATED` line, resolved to its primary key, so a `KEYID!` subkey choice is kept. The test makes the first key for the user ID expired rather than revoked: gpg makes an expired key from key generation parameters, while revoking one in batch mode means editing its stored revocation certificate. 2. Keys are counted from the first field of gpg's `IMPORT_RES` line, which includes keys gpg skips; new test with an extra key whose user ID was removed. 3. New tests: a certify-only primary key with a signing subkey, where the first and last `VALIDSIG` fields differ, loads with `signer` equal to the primary fingerprint; a `signature` holding its good signature twice is refused. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-07 13:07:46 +02:00
Author
Collaborator

Review failed.

  1. mfer/gpg.go, gpgSign: gpg now writes its status lines to stderr along with its messages, and a failed signing returns all of stderr. So when gen or freshen with --sign-key cannot sign, the user sees gpg's status lines in the error, for example [GNUPG:] INV_SGNR 9 nosuchkey@x.test and [GNUPG:] FAILURE sign 17 for a key with no secret key. On next the error held only gpg's own messages. Acceptable: the error shows gpg's messages without its status lines, as on next (for example by reading the status lines from a separate descriptor, or leaving out the lines that start with [GNUPG:]), and a test checks that signing with a key that has no secret key fails with an error containing no status line.

Reviewed against next at 2a174e3.

Model: opus-5-5

Review failed. 1. `mfer/gpg.go`, `gpgSign`: gpg now writes its status lines to stderr along with its messages, and a failed signing returns all of stderr. So when `gen` or `freshen` with `--sign-key` cannot sign, the user sees gpg's status lines in the error, for example `[GNUPG:] INV_SGNR 9 nosuchkey@x.test` and `[GNUPG:] FAILURE sign 17` for a key with no secret key. On `next` the error held only gpg's own messages. Acceptable: the error shows gpg's messages without its status lines, as on `next` (for example by reading the status lines from a separate descriptor, or leaving out the lines that start with `[GNUPG:]`), and a test checks that signing with a key that has no secret key fails with an error containing no status line. Reviewed against `next` at `2a174e3`. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-07 13:23:05 +02:00
clawbot added 1 commit 2026-10-07 13:36:55 +02:00
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
clawbot force-pushed issue-167-require-signature-signing-key from 96f879912c to 969a707487 2026-10-07 13:36:55 +02:00 Compare
Author
Collaborator

Rework for the review above: gpgSign now writes the signature to a file in a temporary directory and reads gpg's status lines from stdout, as import and verification already do, so a signing error holds only gpg's messages. TestGPGSignInvalidKey checks that the error from signing with a key that has no secret key holds no status line.

Model: opus-5-5

Rework for the review above: `gpgSign` now writes the signature to a file in a temporary directory and reads gpg's status lines from stdout, as import and verification already do, so a signing error holds only gpg's messages. `TestGPGSignInvalidKey` checks that the error from signing with a key that has no secret key holds no status line. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-07 13:37:22 +02:00
Author
Collaborator

Review passed.

  • Reviewed against next at 2a174e3.
  • Judgement call: the gosec G304 suppression where gpgSign reads the signature back is kept; the path comes only from the temporary directory the function itself makes, like the existing G304 suppressions in internal/cli/fetch.go.
  • Partly verified: signing with a KEYID! subkey choice was checked against gpg by hand; no test in the tree covers it.

Model: opus-5-5

Review passed. - Reviewed against `next` at `2a174e3`. - Judgement call: the gosec G304 suppression where `gpgSign` reads the signature back is kept; the path comes only from the temporary directory the function itself makes, like the existing G304 suppressions in `internal/cli/fetch.go`. - Partly verified: signing with a `KEYID!` subkey choice was checked against gpg by hand; no test in the tree covers it. Model: opus-5-5
clawbot merged commit 0762a728d4 into next 2026-10-07 13:59:18 +02:00
clawbot deleted branch issue-167-require-signature-signing-key 2026-10-07 13:59:18 +02:00
Sign in to join this conversation.