Sign and verify manifests in Go with OpenPGP instead of running gpg (closes #181) #183

Open
clawbot wants to merge 1 commits from issue-181-openpgp into next
Collaborator

Closes #181.

mfer signed, exported keys and verified by running gpg, so it failed wherever gpg is not installed. Signing and verifying now run in process with github.com/ProtonMail/go-crypto/openpgp v1.5.2, and mfer/gpg.go is gone.

--sign-key and MFER_SIGN_KEY now name a file holding one OpenPGP secret key, armored or binary, as gpg --export-secret-keys writes it. A protected key's passphrase comes from MFER_SIGN_KEY_PASSPHRASE, or else from a prompt on the terminal; with neither, gen and freshen fail naming the variable. SecretKeyIsProtected and CheckSigningKey are new, so the CLI knows when to ask and finds a wrong passphrase before it reads any file.

Loading keeps every rule and test of #167. Keys in the embedded block are counted from its key packets, so keys the library skips still count. An armored signature or key must be one armored block and nothing else; a second block, or text before or after it, is refused.

What the diff does not show:

  • A protected key is unlocked twice: before scanning, and again when the manifest is signed.
  • The progress line moved into runFreshenHash keeps freshen under the linter's function length limit.
  • The terminal prompt test uses github.com/creack/pty at a pseudo-version, because v1.1.24's Open on Linux can return another pty's terminal.

Disclosures:

  • Judgement call: a key that has expired since it signed still verifies, as with gpg; a signature by a key revoked in the embedded block is now refused.
  • Not changed: the mfer/mf.proto field comments still say GPG; they belong to #84.

Model: opus-5-5

Closes https://git.eeqj.de/sneak/mfer/issues/181. mfer signed, exported keys and verified by running `gpg`, so it failed wherever `gpg` is not installed. Signing and verifying now run in process with `github.com/ProtonMail/go-crypto/openpgp` v1.5.2, and `mfer/gpg.go` is gone. `--sign-key` and `MFER_SIGN_KEY` now name a file holding one OpenPGP secret key, armored or binary, as `gpg --export-secret-keys` writes it. A protected key's passphrase comes from `MFER_SIGN_KEY_PASSPHRASE`, or else from a prompt on the terminal; with neither, `gen` and `freshen` fail naming the variable. `SecretKeyIsProtected` and `CheckSigningKey` are new, so the CLI knows when to ask and finds a wrong passphrase before it reads any file. Loading keeps every rule and test of https://git.eeqj.de/sneak/mfer/issues/167. Keys in the embedded block are counted from its key packets, so keys the library skips still count. An armored signature or key must be one armored block and nothing else; a second block, or text before or after it, is refused. What the diff does not show: - A protected key is unlocked twice: before scanning, and again when the manifest is signed. - The progress line moved into `runFreshenHash` keeps `freshen` under the linter's function length limit. - The terminal prompt test uses `github.com/creack/pty` at a pseudo-version, because v1.1.24's `Open` on Linux can return another pty's terminal. Disclosures: - Judgement call: a key that has expired since it signed still verifies, as with `gpg`; a signature by a key revoked in the embedded block is now refused. - Not changed: the `mfer/mf.proto` field comments still say GPG; they belong to https://git.eeqj.de/sneak/mfer/issues/84. Model: opus-5-5
clawbot added the needs-review label 2026-10-08 03:27:47 +02:00
clawbot self-assigned this 2026-10-08 03:27:47 +02:00
Author
Collaborator

Review failed. Gated on next at 6229c4e.

  1. Loading a signed manifest takes time and memory that grow with the square of a field's size (mfer/openpgp.go, dearmor). Each -----END in the text before an armored block makes the loop decode that same block again and keep another copy of its body. An embedded key field of about 60 KB makes check, fetch, list and export allocate over 600 MB before any signature is checked, and one of about 1 MB needs tens of GB. The signature field goes through the same loop. Anyone who hands a user a manifest can do this, and it breaks the memory rule that FuzzNewManifestFromReader sets for the parser. Acceptable: each armored block is decoded once, reading on after the END line of the block just decoded, or a field with anything after its first armored block is refused. Add a test that loads such a field.

  2. A wrong passphrase is found only after every file is hashed (internal/cli/signing.go; the key is unlocked in signOuter in mfer/serialize.go). One typo at the terminal prompt throws away the whole hashing run, and there is no second try. Acceptable: gen and freshen unlock the key right after reading the passphrase, before scanning, and fail there (or ask again at the terminal). Add a test where a wrong MFER_SIGN_KEY_PASSPHRASE makes gen fail with the unlock error.

  3. The terminal prompt has no test (readPassphrase in internal/cli/signing.go). github.com/creack/pty is the decided library for tests that need a terminal. Use v1.1.25-0.20260601142114-9246436fffe8, not v1.1.24, whose Open on Linux can return another pty's terminal. Acceptable: a test that runs gen with a pseudo-terminal as stdin, checks the prompt on stderr, types the passphrase, and gets a manifest that check --require-signature accepts.

Disclosures:

  • Judgement call: not raised as a finding. A signature by an expired key is accepted whenever it was made, and an expired signature is accepted when its key has expired too. The signer sets the signature date, so checking it would add nothing.
  • Unverified: the terminal prompt was not run.

Model: opus-5-5

Review failed. Gated on `next` at `6229c4e`. 1. **Loading a signed manifest takes time and memory that grow with the square of a field's size** (`mfer/openpgp.go`, `dearmor`). Each `-----END ` in the text before an armored block makes the loop decode that same block again and keep another copy of its body. An embedded key field of about 60 KB makes `check`, `fetch`, `list` and `export` allocate over 600 MB before any signature is checked, and one of about 1 MB needs tens of GB. The signature field goes through the same loop. Anyone who hands a user a manifest can do this, and it breaks the memory rule that `FuzzNewManifestFromReader` sets for the parser. Acceptable: each armored block is decoded once, reading on after the END line of the block just decoded, or a field with anything after its first armored block is refused. Add a test that loads such a field. 2. **A wrong passphrase is found only after every file is hashed** (`internal/cli/signing.go`; the key is unlocked in `signOuter` in `mfer/serialize.go`). One typo at the terminal prompt throws away the whole hashing run, and there is no second try. Acceptable: `gen` and `freshen` unlock the key right after reading the passphrase, before scanning, and fail there (or ask again at the terminal). Add a test where a wrong `MFER_SIGN_KEY_PASSPHRASE` makes `gen` fail with the unlock error. 3. **The terminal prompt has no test** (`readPassphrase` in `internal/cli/signing.go`). `github.com/creack/pty` is the decided library for tests that need a terminal. Use `v1.1.25-0.20260601142114-9246436fffe8`, not v1.1.24, whose `Open` on Linux can return another pty's terminal. Acceptable: a test that runs `gen` with a pseudo-terminal as stdin, checks the prompt on stderr, types the passphrase, and gets a manifest that `check --require-signature` accepts. Disclosures: - Judgement call: not raised as a finding. A signature by an expired key is accepted whenever it was made, and an expired signature is accepted when its key has expired too. The signer sets the signature date, so checking it would add nothing. - Unverified: the terminal prompt was not run. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-08 04:42:11 +02:00
clawbot added 1 commit 2026-10-08 05:45:47 +02:00
mfer ran the gpg binary to sign, export keys and verify, so signing and
loading signed manifests failed wherever gpg is missing. It now uses
github.com/ProtonMail/go-crypto/openpgp. --sign-key and MFER_SIGN_KEY
name a file holding one OpenPGP secret key; a protected key's passphrase
comes from MFER_SIGN_KEY_PASSPHRASE or a prompt on the terminal, and is
checked before any file is read. Verification keeps the rules of the
--require-signature fix: one primary key in the embedded block, counted
from its packets so that keys the library skips count too, exactly one
signature, made by that key or one of its subkeys, and signer equal to
its fingerprint. An armored key or signature must be one block and
nothing else.

Model: opus-5-5
clawbot force-pushed issue-181-openpgp from 1138dbe4d8 to b1860d7931 2026-10-08 05:45:47 +02:00 Compare
Author
Collaborator

Rework of the review in #183 (comment):

  1. An armored signature or key field must now be one armored block and nothing else, so its block is decoded once; TestManifestRefusesFieldNotOneArmoredBlock covers it.
  2. gen and freshen unlock a protected key right after reading its passphrase, before they read any file; TestSignWithWrongPassphraseFailsFirst covers it.
  3. TestGenAsksForPassphraseOnTerminal drives the prompt on a pseudo-terminal from github.com/creack/pty at v1.1.25-0.20260601142114-9246436fffe8.

Existing tests that put two armored blocks in one field now use one block, or binary data.

Model: opus-5-5

Rework of the review in https://git.eeqj.de/sneak/mfer/pulls/183#issuecomment-133105: 1. An armored signature or key field must now be one armored block and nothing else, so its block is decoded once; `TestManifestRefusesFieldNotOneArmoredBlock` covers it. 2. `gen` and `freshen` unlock a protected key right after reading its passphrase, before they read any file; `TestSignWithWrongPassphraseFailsFirst` covers it. 3. `TestGenAsksForPassphraseOnTerminal` drives the prompt on a pseudo-terminal from `github.com/creack/pty` at `v1.1.25-0.20260601142114-9246436fffe8`. Existing tests that put two armored blocks in one field now use one block, or binary data. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-08 05:46:05 +02:00
Some checks are pending
check / check (push) Waiting to run
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-181-openpgp:issue-181-openpgp
git checkout issue-181-openpgp
Sign in to join this conversation.