Sign and verify manifests in Go with OpenPGP instead of running gpg (closes #181)
check / check (push) Canceled after 0s

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
+20 -14
View File
@@ -7,9 +7,11 @@ import (
"errors"
"fmt"
"math"
"strings"
"time"
"uuid"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/klauspost/compress/zstd"
"google.golang.org/protobuf/proto"
)
@@ -133,44 +135,48 @@ func (m *manifest) generateOuter(ctx context.Context) error {
}
// Sign the manifest if signing options are provided
if m.signingOptions != nil && m.signingOptions.KeyID != "" {
if m.signingOptions != nil {
return m.signOuter(ctx)
}
return nil
}
// signOuter signs the outer message with the configured GPG key and
// embeds the signature, signer fingerprint, and public key. The signer
// and public key are those of the key gpg reports it signed with, so that
// a key ID matching more than one key cannot name or embed another key.
// signOuter signs the outer message with the secret key in the signing
// options and embeds the signature, the key's fingerprint and its public
// key.
func (m *manifest) signOuter(ctx context.Context) error {
// Unlocking a protected key can take a while; do not start once ctx
// has ended.
err := ctx.Err()
if err != nil {
return err
}
sigString, err := m.signatureString()
if err != nil {
return fmt.Errorf("build signature string: %w", err)
}
sig, signingKey, err := gpgSign(ctx, []byte(sigString), m.signingOptions.KeyID)
key, err := readSigningKey(m.signingOptions)
if err != nil {
return err
}
m.pbOuter.Signature = sig
var sig bytes.Buffer
// Listing the signing key, a subkey's included, puts its primary key's
// fingerprint first.
fingerprint, err := gpgGetKeyFingerprint(ctx, GPGKeyID(signingKey))
err = openpgp.ArmoredDetachSign(&sig, key, strings.NewReader(sigString), nil)
if err != nil {
return err
return fmt.Errorf("sign manifest: %w", err)
}
m.pbOuter.Signer = fingerprint
pubKey, err := gpgExportPublicKey(ctx, GPGKeyID(fingerprint))
pubKey, err := armoredPublicKey(key)
if err != nil {
return err
}
m.pbOuter.Signature = sig.Bytes()
m.pbOuter.Signer = []byte(fingerprint(key))
m.pbOuter.SigningPubKey = pubKey
return nil