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
804 lines
25 KiB
Go
804 lines
25 KiB
Go
//nolint:testpackage // white-box tests exercise unexported internals
|
|
package mfer
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/dsa" //nolint:staticcheck // SA1019: tests need a DSA key to refuse
|
|
"io"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// newTestKey returns a new Ed25519 key, which is quick to make, for
|
|
// "MFER Test Key <test@mfer.test>". config may set when it is made and how
|
|
// long it lasts.
|
|
func newTestKey(t *testing.T, config *packet.Config) *openpgp.Entity {
|
|
t.Helper()
|
|
|
|
if config == nil {
|
|
config = &packet.Config{}
|
|
}
|
|
|
|
config.Algorithm = packet.PubKeyAlgoEdDSA
|
|
|
|
key, err := openpgp.NewEntity("MFER Test Key", "", "test@mfer.test", config)
|
|
require.NoError(t, err)
|
|
|
|
return key
|
|
}
|
|
|
|
// armoredSecretKeys returns keys with their secret keys in one armored
|
|
// block, as gpg --export-secret-keys --armor writes them.
|
|
func armoredSecretKeys(t *testing.T, keys ...*openpgp.Entity) []byte {
|
|
t.Helper()
|
|
|
|
var buf bytes.Buffer
|
|
|
|
w, err := armor.Encode(&buf, openpgp.PrivateKeyType, nil)
|
|
require.NoError(t, err)
|
|
|
|
for _, key := range keys {
|
|
require.NoError(t, key.SerializePrivateWithoutSigning(w, nil))
|
|
}
|
|
|
|
require.NoError(t, w.Close())
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// armoredPublicKeys returns the public parts of keys in one armored block,
|
|
// as gpg --export --armor writes them.
|
|
func armoredPublicKeys(t *testing.T, keys ...*openpgp.Entity) []byte {
|
|
t.Helper()
|
|
|
|
var buf bytes.Buffer
|
|
|
|
w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil)
|
|
require.NoError(t, err)
|
|
|
|
for _, key := range keys {
|
|
require.NoError(t, key.Serialize(w))
|
|
}
|
|
|
|
require.NoError(t, w.Close())
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// testSigningOptions returns signing options for a new key with no
|
|
// passphrase.
|
|
func testSigningOptions(t *testing.T) *SigningOptions {
|
|
t.Helper()
|
|
|
|
return &SigningOptions{SecretKey: armoredSecretKeys(t, newTestKey(t, nil))}
|
|
}
|
|
|
|
// joinArmored returns the armored block first followed by the armored
|
|
// block second on the next line. armor.Encode ends a block without a
|
|
// newline, unlike gpg, and a block only starts at the start of a line.
|
|
func joinArmored(first, second []byte) []byte {
|
|
return slices.Concat(first, []byte("\n"), second)
|
|
}
|
|
|
|
// signedTestManifest returns a manifest of one file signed with opts.
|
|
func signedTestManifest(t *testing.T, opts *SigningOptions) []byte {
|
|
t.Helper()
|
|
|
|
b := NewBuilder()
|
|
b.SetSigningOptions(opts)
|
|
|
|
content := []byte("signed file content")
|
|
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, 0,
|
|
bytes.NewReader(content), nil)
|
|
require.NoError(t, err)
|
|
|
|
var buf bytes.Buffer
|
|
|
|
require.NoError(t, b.Build(context.Background(), &buf))
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// rewriteOuter returns manifest with its outer message changed by edit.
|
|
// A signature stays good as long as edit leaves the UUID and hash alone.
|
|
func rewriteOuter(t *testing.T, manifest []byte, edit func(*MFFileOuter)) []byte {
|
|
t.Helper()
|
|
|
|
outer := new(MFFileOuter)
|
|
require.NoError(t, proto.Unmarshal(manifest[len(MAGIC):], outer))
|
|
|
|
edit(outer)
|
|
|
|
data, err := proto.Marshal(outer)
|
|
require.NoError(t, err)
|
|
|
|
return append([]byte(MAGIC), data...)
|
|
}
|
|
|
|
func TestBuilderWithSigning(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key := newTestKey(t, nil)
|
|
|
|
// Create a builder with signing options
|
|
b := NewBuilder()
|
|
b.SetSigningOptions(&SigningOptions{SecretKey: armoredSecretKeys(t, key)})
|
|
|
|
// Add a test file
|
|
content := []byte("test file content")
|
|
reader := bytes.NewReader(content)
|
|
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, 0, reader, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Build the manifest
|
|
var buf bytes.Buffer
|
|
|
|
err = b.Build(context.Background(), &buf)
|
|
require.NoError(t, err)
|
|
|
|
// Parse the manifest and verify signature fields are populated
|
|
manifest, err := NewManifestFromReader(&buf)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, manifest.pbOuter)
|
|
|
|
assert.NotEmpty(t, manifest.pbOuter.GetSignature(),
|
|
"signature should be populated")
|
|
assert.NotEmpty(t, manifest.pbOuter.GetSigningPubKey(),
|
|
"signing public key should be populated")
|
|
|
|
// The signer is the key's fingerprint in 40 upper-case hex characters.
|
|
assert.Equal(t, fingerprint(key), string(manifest.pbOuter.GetSigner()))
|
|
assert.Regexp(t, "^[0-9A-F]{40}$", string(manifest.pbOuter.GetSigner()))
|
|
|
|
// Verify signature is a valid PGP signature
|
|
assert.Contains(t, string(manifest.pbOuter.GetSignature()),
|
|
"-----BEGIN PGP SIGNATURE-----")
|
|
|
|
// Verify public key is a valid PGP public key block
|
|
assert.Contains(t, string(manifest.pbOuter.GetSigningPubKey()),
|
|
"-----BEGIN PGP PUBLIC KEY BLOCK-----")
|
|
}
|
|
|
|
func TestScannerWithSigning(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create in-memory filesystem with test files
|
|
fs := afero.NewMemMapFs()
|
|
require.NoError(t, fs.MkdirAll("/testdir", 0o755))
|
|
require.NoError(t,
|
|
afero.WriteFile(fs, "/testdir/file1.txt", []byte("content1"), 0o644))
|
|
require.NoError(t,
|
|
afero.WriteFile(fs, "/testdir/file2.txt", []byte("content2"), 0o644))
|
|
|
|
// Create scanner with signing options
|
|
opts := &ScannerOptions{
|
|
Fs: fs,
|
|
SigningOptions: testSigningOptions(t),
|
|
}
|
|
s := NewScannerWithOptions(opts)
|
|
|
|
// Enumerate files
|
|
require.NoError(t, s.EnumeratePath("/testdir", nil))
|
|
assert.Equal(t, FileCount(2), s.FileCount())
|
|
|
|
// Generate signed manifest
|
|
var buf bytes.Buffer
|
|
require.NoError(t, s.ToManifest(context.Background(), &buf, nil))
|
|
|
|
// Parse and verify
|
|
manifest, err := NewManifestFromReader(&buf)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEmpty(t, manifest.pbOuter.GetSignature())
|
|
assert.NotEmpty(t, manifest.pbOuter.GetSigner())
|
|
assert.NotEmpty(t, manifest.pbOuter.GetSigningPubKey())
|
|
}
|
|
|
|
// TestSigningWithBinarySecretKey signs with a secret key that is not
|
|
// armored, as gpg --export-secret-keys writes it without --armor.
|
|
func TestSigningWithBinarySecretKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var secretKey bytes.Buffer
|
|
|
|
require.NoError(t, newTestKey(t, nil).SerializePrivateWithoutSigning(&secretKey, nil))
|
|
|
|
_, err := NewManifestFromReader(bytes.NewReader(
|
|
signedTestManifest(t, &SigningOptions{SecretKey: secretKey.Bytes()})))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestSigningWithProtectedKey signs with a key protected by a passphrase:
|
|
// with the passphrase, without one, and with a wrong one.
|
|
func TestSigningWithProtectedKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key := newTestKey(t, nil)
|
|
require.NoError(t, key.EncryptPrivateKeys([]byte("right"), nil))
|
|
secretKey := armoredSecretKeys(t, key)
|
|
|
|
protected, err := SecretKeyIsProtected(secretKey)
|
|
require.NoError(t, err)
|
|
assert.True(t, protected)
|
|
|
|
_, err = NewManifestFromReader(bytes.NewReader(signedTestManifest(t,
|
|
&SigningOptions{SecretKey: secretKey, Passphrase: []byte("right")})))
|
|
require.NoError(t, err)
|
|
|
|
b := NewBuilder()
|
|
b.SetSigningOptions(&SigningOptions{SecretKey: secretKey})
|
|
require.ErrorIs(t, b.Build(context.Background(), io.Discard), errNoPassphrase)
|
|
|
|
b.SetSigningOptions(&SigningOptions{
|
|
SecretKey: secretKey, Passphrase: []byte("wrong"),
|
|
})
|
|
assert.ErrorContains(t, b.Build(context.Background(), io.Discard),
|
|
"unlock signing key")
|
|
}
|
|
|
|
func TestSecretKeyIsProtectedWithoutPassphrase(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
protected, err := SecretKeyIsProtected(testSigningOptions(t).SecretKey)
|
|
require.NoError(t, err)
|
|
assert.False(t, protected)
|
|
}
|
|
|
|
// TestSigningKeyFileWithTwoKeys signs with a key file that holds two keys,
|
|
// as gpg writes it for a user ID that two keys have. It names no one key
|
|
// to sign with, so signing must fail.
|
|
func TestSigningKeyFileWithTwoKeys(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
b := NewBuilder()
|
|
b.SetSigningOptions(&SigningOptions{SecretKey: armoredSecretKeys(t,
|
|
newTestKey(t, nil), newTestKey(t, nil))})
|
|
|
|
err := b.Build(context.Background(), io.Discard)
|
|
require.ErrorIs(t, err, errKeyCount)
|
|
assert.EqualError(t, err, "signing key file must hold exactly one key, found 2")
|
|
}
|
|
|
|
// TestSigningKeyFileWithoutSecretKey signs with a file that holds only a
|
|
// public key.
|
|
func TestSigningKeyFileWithoutSecretKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
b := NewBuilder()
|
|
b.SetSigningOptions(&SigningOptions{
|
|
SecretKey: armoredPublicKeys(t, newTestKey(t, nil)),
|
|
})
|
|
require.ErrorIs(t, b.Build(context.Background(), io.Discard), errNoSecretKey)
|
|
}
|
|
|
|
// TestCheckSigningKeyRefusesKeyThatCannotSign checks a key that can sign,
|
|
// and keys that read and need no passphrase but cannot sign now: one that
|
|
// expired in 2020 and one that has been revoked. CheckSigningKey must
|
|
// refuse each of the two, as signing a manifest with it would fail.
|
|
func TestCheckSigningKeyRefusesKeyThatCannotSign(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.NoError(t, CheckSigningKey(testSigningOptions(t)))
|
|
|
|
made := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
expired := newTestKey(t, &packet.Config{
|
|
Time: func() time.Time { return made },
|
|
KeyLifetimeSecs: uint32((24 * time.Hour).Seconds()),
|
|
})
|
|
|
|
revoked := newTestKey(t, nil)
|
|
require.NoError(t, revoked.RevokeKey(packet.KeyRetired, "", nil))
|
|
|
|
for name, key := range map[string]*openpgp.Entity{
|
|
"expired": expired,
|
|
"revoked": revoked,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assert.ErrorContains(t, CheckSigningKey(
|
|
&SigningOptions{SecretKey: armoredSecretKeys(t, key)}),
|
|
"signing key cannot sign")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSigningRefusesVersion6Key signs with an OpenPGP version 6 key, whose
|
|
// fingerprint is 64 hex characters. mfer signs only with version 4 keys.
|
|
func TestSigningRefusesVersion6Key(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key, err := openpgp.NewEntity("MFER Test Key", "", "test@mfer.test",
|
|
&packet.Config{V6Keys: true, Algorithm: packet.PubKeyAlgoEd25519})
|
|
require.NoError(t, err)
|
|
|
|
secretKey := armoredSecretKeys(t, key)
|
|
|
|
_, err = SecretKeyIsProtected(secretKey)
|
|
require.ErrorIs(t, err, errNotV4Key)
|
|
|
|
b := NewBuilder()
|
|
b.SetSigningOptions(&SigningOptions{SecretKey: secretKey})
|
|
require.ErrorIs(t, b.Build(context.Background(), io.Discard), errNotV4Key)
|
|
}
|
|
|
|
// TestMalformedArmorIsNamed reads a signing key file, a signature and an
|
|
// embedded public key block whose armor is malformed: a header line with
|
|
// no colon, or no blank line after the BEGIN line. Each must fail saying
|
|
// the armor is malformed.
|
|
func TestMalformedArmorIsNamed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := testSigningOptions(t)
|
|
manifest := signedTestManifest(t, opts)
|
|
|
|
for name, change := range map[string]func([]byte) []byte{
|
|
"header line with no colon": func(block []byte) []byte {
|
|
return bytes.Replace(block,
|
|
[]byte("-----\n"), []byte("-----\nno colon\n"), 1)
|
|
},
|
|
"no blank line after the BEGIN line": func(block []byte) []byte {
|
|
return bytes.Replace(block, []byte("-----\n\n"), []byte("-----\n"), 1)
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := SecretKeyIsProtected(change(opts.SecretKey))
|
|
require.ErrorIs(t, err, errMalformedArmor)
|
|
|
|
for _, changed := range [][]byte{
|
|
rewriteOuter(t, manifest, func(outer *MFFileOuter) {
|
|
outer.Signature = change(outer.GetSignature())
|
|
}),
|
|
rewriteOuter(t, manifest, func(outer *MFFileOuter) {
|
|
outer.SigningPubKey = change(outer.GetSigningPubKey())
|
|
}),
|
|
} {
|
|
_, err = NewManifestFromReader(bytes.NewReader(changed))
|
|
require.ErrorIs(t, err, errMalformedArmor)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerifySignature(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key := newTestKey(t, nil)
|
|
data := []byte("test data to sign and verify")
|
|
|
|
var armored, binary bytes.Buffer
|
|
|
|
require.NoError(t, openpgp.ArmoredDetachSign(&armored, key,
|
|
bytes.NewReader(data), nil))
|
|
require.NoError(t, openpgp.DetachSign(&binary, key, bytes.NewReader(data), nil))
|
|
|
|
pubKey, err := armoredPublicKey(key)
|
|
require.NoError(t, err)
|
|
|
|
var binaryPubKey bytes.Buffer
|
|
|
|
require.NoError(t, key.Serialize(&binaryPubKey))
|
|
|
|
// Verifying names the key that made the signature, whether the
|
|
// signature and key are armored or not.
|
|
signer, err := verifySignature(data, armored.Bytes(), pubKey)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, fingerprint(key), signer)
|
|
|
|
signer, err = verifySignature(data, binary.Bytes(), binaryPubKey.Bytes())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, fingerprint(key), signer)
|
|
|
|
// A signature over other data is bad.
|
|
_, err = verifySignature([]byte("different data"), armored.Bytes(), pubKey)
|
|
require.Error(t, err)
|
|
|
|
// A public key that is not one cannot verify anything.
|
|
_, err = verifySignature(data, armored.Bytes(), []byte("not a public key"))
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
// TestVerifySignatureKeyExpiredSince verifies a signature made in 2020 by
|
|
// a key that expired a day after it was made. The signature is still good.
|
|
func TestVerifySignatureKeyExpiredSince(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
made := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
config := &packet.Config{
|
|
Time: func() time.Time { return made },
|
|
KeyLifetimeSecs: uint32((24 * time.Hour).Seconds()),
|
|
}
|
|
key := newTestKey(t, config)
|
|
data := []byte("signed in 2020")
|
|
|
|
var sig bytes.Buffer
|
|
|
|
require.NoError(t, openpgp.ArmoredDetachSign(&sig, key, bytes.NewReader(data), config))
|
|
|
|
pubKey, err := armoredPublicKey(key)
|
|
require.NoError(t, err)
|
|
|
|
signer, err := verifySignature(data, sig.Bytes(), pubKey)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, fingerprint(key), signer)
|
|
}
|
|
|
|
func TestManifestSignatureVerification(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Parse the manifest - signature should be verified during load
|
|
manifest, err := NewManifestFromReader(bytes.NewReader(
|
|
signedTestManifest(t, testSigningOptions(t))))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, manifest)
|
|
|
|
// Signature should be present and valid
|
|
assert.NotEmpty(t, manifest.pbOuter.GetSignature())
|
|
}
|
|
|
|
func TestManifestTamperedSignatureFails(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Change one character of the signature's base64 body, which starts
|
|
// after the blank line that ends the armor headers.
|
|
data := rewriteOuter(t, signedTestManifest(t, testSigningOptions(t)),
|
|
func(outer *MFFileOuter) {
|
|
sig := outer.GetSignature()
|
|
i := bytes.Index(sig, []byte("\n\n")) + len("\n\n") + 20
|
|
|
|
sig[i]++
|
|
})
|
|
|
|
// Try to load the tampered manifest - should fail
|
|
_, err := NewManifestFromReader(bytes.NewReader(data))
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
// TestManifestSignedByGPGLoads loads the signed seed of
|
|
// FuzzNewManifestFromReader, a manifest signed with gpg before mfer signed
|
|
// and verified manifests itself.
|
|
func TestManifestSignedByGPGLoads(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
seed, err := os.ReadFile(filepath.Join(
|
|
"testdata", "fuzz", "FuzzNewManifestFromReader", "signed"))
|
|
require.NoError(t, err)
|
|
|
|
// After its header line the seed holds the manifest as []byte("...").
|
|
_, quoted, found := strings.Cut(string(seed), "[]byte(")
|
|
require.True(t, found)
|
|
|
|
manifest, err := strconv.Unquote(
|
|
strings.TrimSuffix(strings.TrimSpace(quoted), ")"))
|
|
require.NoError(t, err)
|
|
|
|
m, err := NewManifestFromReader(strings.NewReader(manifest))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "4F562BFB863FDC6B51B4EE88872A51176CEF23AE",
|
|
string(m.pbOuter.GetSigner()))
|
|
}
|
|
|
|
// TestManifestRefusesSecondEmbeddedKey loads manifests whose embedded
|
|
// public key block holds another key besides the key that signed it: as a
|
|
// public key, as a secret key with no user ID, which openpgp.ReadKeyRing
|
|
// skips, and written as a subkey packet at the start of the block, which
|
|
// openpgp.ReadKeyRing reads as a primary key. Loading must refuse each,
|
|
// although the signature is good and the signer field names the key that
|
|
// made it.
|
|
func TestManifestRefusesSecondEmbeddedKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
other := newTestKey(t, nil)
|
|
signer := newTestKey(t, nil)
|
|
manifest := signedTestManifest(t,
|
|
&SigningOptions{SecretKey: armoredSecretKeys(t, signer)})
|
|
|
|
otherWithoutUserID := newTestKey(t, nil)
|
|
otherWithoutUserID.Identities = map[string]*openpgp.Identity{}
|
|
|
|
otherAsSubkey := newTestKey(t, nil)
|
|
otherAsSubkey.PrimaryKey.IsSubkey = true
|
|
|
|
for name, block := range map[string][]byte{
|
|
"public key": armoredPublicKeys(t, other, signer),
|
|
"secret key without user ID": armoredSecretKeys(t, signer, otherWithoutUserID),
|
|
"subkey packet first": armoredPublicKeys(t, otherAsSubkey, signer),
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
embedded := rewriteOuter(t, manifest, func(outer *MFFileOuter) {
|
|
outer.SigningPubKey = block
|
|
})
|
|
|
|
_, err := NewManifestFromReader(bytes.NewReader(embedded))
|
|
require.ErrorIs(t, err, errKeyCount)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestManifestRefusesSecondEmbeddedKeyWithoutUserID loads a manifest whose
|
|
// embedded public key block holds, before the key that signed it, another
|
|
// key with no user ID, which openpgp.ReadKeyRing skips. Loading must
|
|
// refuse it: the block holds two keys.
|
|
func TestManifestRefusesSecondEmbeddedKeyWithoutUserID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
other := newTestKey(t, nil)
|
|
other.Identities = map[string]*openpgp.Identity{}
|
|
|
|
signer := newTestKey(t, nil)
|
|
|
|
manifest := rewriteOuter(t, signedTestManifest(t,
|
|
&SigningOptions{SecretKey: armoredSecretKeys(t, signer)}),
|
|
func(outer *MFFileOuter) {
|
|
outer.SigningPubKey = armoredPublicKeys(t, other, signer)
|
|
})
|
|
|
|
_, err := NewManifestFromReader(bytes.NewReader(manifest))
|
|
require.ErrorIs(t, err, errKeyCount)
|
|
}
|
|
|
|
// dsaKeyPacket returns a public key packet holding a DSA key, or a public
|
|
// subkey packet when isSubkey. Its numbers are not a working key: loading
|
|
// must refuse the packet before it uses them.
|
|
func dsaKeyPacket(t *testing.T, isSubkey bool) []byte {
|
|
t.Helper()
|
|
|
|
key := packet.NewDSAPublicKey(time.Now(), &dsa.PublicKey{
|
|
P: big.NewInt(23), Q: big.NewInt(11), G: big.NewInt(4), Y: big.NewInt(8),
|
|
})
|
|
key.IsSubkey = isSubkey
|
|
|
|
var buf bytes.Buffer
|
|
|
|
require.NoError(t, key.Serialize(&buf))
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// TestManifestRefusesDSAKey loads manifests whose embedded public key
|
|
// block holds a DSA key: alone, or as a subkey after the key that signed
|
|
// the manifest. openpgp.ReadKeyRing checks a key's self-signatures, which
|
|
// for a DSA key with very large numbers takes minutes each. Loading must
|
|
// refuse each block before that.
|
|
func TestManifestRefusesDSAKey(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 name, block := range map[string][]byte{
|
|
"DSA key": dsaKeyPacket(t, false),
|
|
"DSA subkey": slices.Concat(signerKey.Bytes(), dsaKeyPacket(t, true)),
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
embedded := rewriteOuter(t, manifest, func(outer *MFFileOuter) {
|
|
outer.SigningPubKey = block
|
|
})
|
|
|
|
_, err := NewManifestFromReader(bytes.NewReader(embedded))
|
|
require.ErrorIs(t, err, errDSAKey)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
t.Parallel()
|
|
|
|
manifest := rewriteOuter(t, signedTestManifest(t, testSigningOptions(t)),
|
|
func(outer *MFFileOuter) {
|
|
sig, err := dearmor(outer.GetSignature())
|
|
require.NoError(t, err)
|
|
|
|
outer.Signature = slices.Concat(sig, sig)
|
|
})
|
|
|
|
_, err := NewManifestFromReader(bytes.NewReader(manifest))
|
|
require.ErrorIs(t, err, errNotOneSignature)
|
|
}
|
|
|
|
// TestManifestRefusesFieldNotOneArmoredBlock loads manifests whose
|
|
// signature or embedded public key block holds its good armored block with
|
|
// something else: a second armored block, text after the END line, or many
|
|
// END lines before the block. Decoding the block once for each END line
|
|
// before it would take time and memory that grow with the square of the
|
|
// field's size. Loading must refuse each.
|
|
func TestManifestRefusesFieldNotOneArmoredBlock(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifest := signedTestManifest(t, testSigningOptions(t))
|
|
|
|
for name, change := range map[string]func([]byte) []byte{
|
|
"second armored block": func(block []byte) []byte {
|
|
return joinArmored(block, block)
|
|
},
|
|
"text after the END line": func(block []byte) []byte {
|
|
return slices.Concat(block, []byte("\nmore text\n"))
|
|
},
|
|
"END lines before the block": func(block []byte) []byte {
|
|
return slices.Concat(
|
|
[]byte(strings.Repeat(armorEnd+"\n", 1000)), block)
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, changed := range [][]byte{
|
|
rewriteOuter(t, manifest, func(outer *MFFileOuter) {
|
|
outer.Signature = change(outer.GetSignature())
|
|
}),
|
|
rewriteOuter(t, manifest, func(outer *MFFileOuter) {
|
|
outer.SigningPubKey = change(outer.GetSigningPubKey())
|
|
}),
|
|
} {
|
|
_, err := NewManifestFromReader(bytes.NewReader(changed))
|
|
require.ErrorIs(t, err, errNotOneArmoredBlock)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestManifestSignedWithSubkey signs with a key that has a signing subkey,
|
|
// which signs in place of the primary key. The manifest must load, with
|
|
// the primary key's fingerprint as signer.
|
|
func TestManifestSignedWithSubkey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key := newTestKey(t, nil)
|
|
require.NoError(t, key.AddSigningSubkey(
|
|
&packet.Config{Algorithm: packet.PubKeyAlgoEdDSA}))
|
|
|
|
m, err := NewManifestFromReader(bytes.NewReader(signedTestManifest(t,
|
|
&SigningOptions{SecretKey: armoredSecretKeys(t, key)})))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, fingerprint(key), string(m.pbOuter.GetSigner()))
|
|
|
|
block, err := armor.Decode(bytes.NewReader(m.pbOuter.GetSignature()))
|
|
require.NoError(t, err)
|
|
|
|
p, err := packet.Read(block.Body)
|
|
require.NoError(t, err)
|
|
|
|
sig, ok := p.(*packet.Signature)
|
|
require.True(t, ok)
|
|
|
|
subkey := key.Subkeys[len(key.Subkeys)-1].PublicKey
|
|
assert.Equal(t, subkey.KeyId, *sig.IssuerKeyId,
|
|
"the signing subkey made the signature")
|
|
}
|
|
|
|
// TestManifestRefusesSignerOtherThanSigningKey loads a manifest whose
|
|
// signer field names a key other than the one that made the signature.
|
|
func TestManifestRefusesSignerOtherThanSigningKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manifest := rewriteOuter(t, signedTestManifest(t, testSigningOptions(t)),
|
|
func(outer *MFFileOuter) {
|
|
outer.Signer = []byte(strings.Repeat("A", len(outer.GetSigner())))
|
|
})
|
|
|
|
_, err := NewManifestFromReader(bytes.NewReader(manifest))
|
|
require.ErrorIs(t, err, errSignerNotSigningKey)
|
|
}
|
|
|
|
func TestBuilderWithoutSigning(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a builder without signing options
|
|
b := NewBuilder()
|
|
|
|
// Add a test file
|
|
content := []byte("test file content")
|
|
reader := bytes.NewReader(content)
|
|
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, 0, reader, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Build the manifest
|
|
var buf bytes.Buffer
|
|
|
|
err = b.Build(context.Background(), &buf)
|
|
require.NoError(t, err)
|
|
|
|
// Parse the manifest and verify signature fields are empty
|
|
manifest, err := NewManifestFromReader(&buf)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, manifest.pbOuter)
|
|
|
|
assert.Empty(t, manifest.pbOuter.GetSignature(),
|
|
"signature should be empty when not signing")
|
|
assert.Empty(t, manifest.pbOuter.GetSigner(),
|
|
"signer should be empty when not signing")
|
|
assert.Empty(t, manifest.pbOuter.GetSigningPubKey(),
|
|
"signing public key should be empty when not signing")
|
|
}
|
|
|
|
// TestBuildPassesContextToSigning checks that Build does not sign once the
|
|
// context given to it has ended.
|
|
func TestBuildPassesContextToSigning(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
b := NewBuilder()
|
|
b.SetSigningOptions(&SigningOptions{SecretKey: []byte("any")})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
require.ErrorIs(t, b.Build(ctx, io.Discard), context.Canceled)
|
|
}
|