1 Commits
Author SHA1 Message Date
clawbot e00ec787e8 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 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 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. A DSA key is refused, and so is an armored field that is
not one well-formed block.

Model: opus-5-5
2026-10-08 05:21:16 +00:00
3 changed files with 9 additions and 72 deletions
+2 -3
View File
@@ -153,9 +153,8 @@ that key (or one of its subkeys), and `signer` is that key's fingerprint. The
reference implementation refuses to load a manifest that fails these checks;
`check` and `fetch` given `--require-signature` then compare the required
fingerprint with `signer`. It also refuses a manifest whose `signingPubKey`
holds a DSA key or subkey, or any secret key or subkey, since checking the
self-signatures of a DSA key or the numbers of a secret key can take hours when
those numbers are very large.
holds a DSA key or subkey, since checking the self-signatures of a DSA key with
very large numbers can take hours.
## Deterministic Serialization
+7 -20
View File
@@ -46,7 +46,6 @@ const (
var (
errKeyCount = errors.New("must hold exactly one key")
errDSAKey = errors.New("must not hold a DSA key")
errSecretKey = errors.New("must not hold a secret key")
errNoSecretKey = errors.New("signing key file holds no secret key")
errNotV4Key = errors.New("signing key must be an OpenPGP version 4 key, " +
"the only kind whose fingerprint --require-signature takes")
@@ -127,7 +126,7 @@ func readSigningKey(opts *SigningOptions) (*openpgp.Entity, error) {
// readSecretKey returns the one key in secretKey, armored or binary,
// which must be a version 4 key and include its secret key.
func readSecretKey(secretKey []byte) (*openpgp.Entity, error) {
key, err := readOneKey(secretKey, "signing key file", false)
key, err := readOneKey(secretKey, "signing key file")
if err != nil {
return nil, err
}
@@ -145,9 +144,9 @@ func readSecretKey(secretKey []byte) (*openpgp.Entity, error) {
}
// readOneKey returns the key in data, armored or binary, which must hold
// exactly one primary key and no DSA key or subkey, and when publicOnly no
// secret key or subkey either. what names data in errors.
func readOneKey(data []byte, what string, publicOnly bool) (*openpgp.Entity, error) {
// exactly one primary key and no DSA key or subkey. what names data in
// errors.
func readOneKey(data []byte, what string) (*openpgp.Entity, error) {
packets, err := dearmor(data)
if err != nil {
return nil, fmt.Errorf("read %s: %w", what, err)
@@ -162,20 +161,8 @@ func readOneKey(data []byte, what string, publicOnly bool) (*openpgp.Entity, err
return nil, fmt.Errorf("%s %w, found %d", what, errKeyCount, keys)
}
// openpgp.ReadKeyRing checks the numbers of every secret key it reads,
// and an ElGamal secret subkey with a very large prime makes that take
// minutes.
secretKeys, err := countPackets(packets, secretKeyPacketTag, secretSubkeyPacketTag)
if err != nil {
return nil, fmt.Errorf("read %s: %w", what, err)
}
if publicOnly && secretKeys != 0 {
return nil, fmt.Errorf("%s %w", what, errSecretKey)
}
// openpgp.ReadKeyRing also checks every self-signature, and a DSA key
// with very large numbers makes each check take seconds to minutes.
// openpgp.ReadKeyRing checks every self-signature, and a DSA key with
// very large numbers makes each check take seconds to minutes.
dsa, err := holdsDSAKey(packets)
if err != nil {
return nil, fmt.Errorf("read %s: %w", what, err)
@@ -247,7 +234,7 @@ func fingerprint(key *openpgp.Entity) string {
// and returns that primary key's fingerprint. signature and pubKey may each
// be armored or binary.
func verifySignature(data, signature, pubKey []byte) (string, error) {
key, err := readOneKey(pubKey, "embedded public key block", true)
key, err := readOneKey(pubKey, "embedded public key block")
if err != nil {
return "", err
}
-49
View File
@@ -17,7 +17,6 @@ import (
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/elgamal"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
@@ -608,54 +607,6 @@ func TestManifestRefusesDSAKey(t *testing.T) {
}
}
// elGamalSecretSubkeyPacket returns a secret subkey packet holding an
// ElGamal key. Its numbers are not a working key: loading must refuse the
// packet before it uses them.
func elGamalSecretSubkeyPacket(t *testing.T) []byte {
t.Helper()
key := packet.NewElGamalPrivateKey(time.Now(), &elgamal.PrivateKey{
P: big.NewInt(23), G: big.NewInt(4), Y: big.NewInt(8), X: big.NewInt(3),
})
key.IsSubkey = true
var buf bytes.Buffer
require.NoError(t, key.Serialize(&buf))
return buf.Bytes()
}
// TestManifestRefusesSecretKey loads manifests whose embedded public key
// block holds a secret key: the key that signed the manifest with its
// secret key, or its public key followed by an ElGamal secret subkey.
// openpgp.ReadKeyRing checks the numbers of every secret key it reads,
// which for an ElGamal key with a very large prime takes minutes. Loading
// must refuse each block before that.
func TestManifestRefusesSecretKey(t *testing.T) {
t.Parallel()
signer := newTestKey(t, nil)
manifest := signedTestManifest(t,
&SigningOptions{SecretKey: armoredSecretKeys(t, signer)})
var signerKey bytes.Buffer
require.NoError(t, signer.Serialize(&signerKey))
for _, block := range [][]byte{
armoredSecretKeys(t, signer),
slices.Concat(signerKey.Bytes(), elGamalSecretSubkeyPacket(t)),
} {
embedded := rewriteOuter(t, manifest, func(outer *MFFileOuter) {
outer.SigningPubKey = block
})
_, err := NewManifestFromReader(bytes.NewReader(embedded))
require.ErrorIs(t, err, errSecretKey)
}
}
// TestManifestRefusesTwoSignatures loads a manifest whose signature field
// holds its good signature twice, not armored. Loading must refuse it.
func TestManifestRefusesTwoSignatures(t *testing.T) {