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 it 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 version 4 OpenPGP
secret key; a protected key's passphrase comes from
MFER_SIGN_KEY_PASSPHRASE or a terminal prompt. gen and freshen check that
the key can sign before they read any file. Verification keeps the rules
of the --require-signature fix: one primary key in the embedded block,
counted from its packets, exactly one signature, made by that key or a
subkey, and signer equal to its fingerprint. The embedded block may hold
no DSA key and no secret key, and an armored field must be one
well-formed block.

Model: opus-5-5
This commit was merged in pull request #183.
This commit is contained in:
2026-10-08 09:42:45 +02:00
parent c23367c216
commit 0fbb3da0a5
25 changed files with 1648 additions and 1178 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
@@ -441,7 +443,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 {
@@ -458,6 +460,10 @@ func runFreshenHash(
}
}
if hasher.showProgress && hasher.filesToHash > 0 {
log.ProgressDone()
}
return nil
}
@@ -502,7 +508,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
@@ -536,7 +546,7 @@ func (mfa *CLIApp) freshenManifestOperation(
totalHashBytes: totalHashBytes,
filesToHash: filesToHash,
startHash: time.Now(),
builder: newFreshenBuilder(cmd),
builder: builder,
}
err = runFreshenHash(ctx, hasher, scanner.entries)
@@ -544,10 +554,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)