Sign and verify manifests in Go with OpenPGP instead of running gpg (closes #181)
check / check (push) Waiting to run

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.
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. Tests make their
keys in process.

Model: opus-5-5
This commit is contained in:
2026-10-08 01:13:48 +00:00
parent 6229c4eca0
commit 1138dbe4d8
25 changed files with 1143 additions and 1174 deletions
+19 -13
View File
@@ -339,7 +339,7 @@ func writeFreshenedManifest(
// newFreshenBuilder constructs the manifest builder configured from CLI
// flags.
func newFreshenBuilder(cmd *cli.Command) *mfer.Builder {
func (mfa *CLIApp) newFreshenBuilder(cmd *cli.Command) (*mfer.Builder, error) {
builder := mfer.NewBuilder()
if cmd.Bool("include-timestamps") {
builder.SetIncludeTimestamps(true)
@@ -347,13 +347,15 @@ func newFreshenBuilder(cmd *cli.Command) *mfer.Builder {
// Set up signing options if sign-key is provided
if signKey := cmd.String("sign-key"); signKey != "" {
builder.SetSigningOptions(&mfer.SigningOptions{
KeyID: mfer.GPGKeyID(signKey),
})
log.Infof("signing manifest with GPG key: %s", signKey)
signing, err := mfa.signingOptions(signKey)
if err != nil {
return nil, err
}
builder.SetSigningOptions(signing)
}
return builder
return builder, nil
}
// freshenScan runs the scan phase against the loaded manifest entries
@@ -433,7 +435,7 @@ func hashTotals(entries []*freshenEntry) (int64, int64) {
}
// runFreshenHash processes every entry through the hasher, aborting if
// the context is canceled.
// the context is canceled, and ends the hasher's progress line.
func runFreshenHash(
ctx context.Context, hasher *freshenHasher, entries []*freshenEntry,
) error {
@@ -450,6 +452,10 @@ func runFreshenHash(
}
}
if hasher.showProgress && hasher.filesToHash > 0 {
log.ProgressDone()
}
return nil
}
@@ -494,7 +500,11 @@ func (mfa *CLIApp) freshenManifestOperation(
return err
}
//nolint:contextcheck // mfer loads a manifest without a context
builder, err := mfa.newFreshenBuilder(cmd)
if err != nil {
return err
}
existingByPath, err := mfa.loadExistingEntries(manifestPath)
if err != nil {
return err
@@ -528,7 +538,7 @@ func (mfa *CLIApp) freshenManifestOperation(
totalHashBytes: totalHashBytes,
filesToHash: filesToHash,
startHash: time.Now(),
builder: newFreshenBuilder(cmd),
builder: builder,
}
err = runFreshenHash(ctx, hasher, scanner.entries)
@@ -536,10 +546,6 @@ func (mfa *CLIApp) freshenManifestOperation(
return err
}
if showProgress && filesToHash > 0 {
log.ProgressDone()
}
// Print summary
log.Infof("freshen complete: %d unchanged, %d changed, %d added, %d removed",
scanner.unchanged, scanner.changed, scanner.added, removed)