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, and is checked before any file is read. 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. An armored key or signature must be one block and nothing else. Model: opus-5-5
189 lines
5.6 KiB
Go
189 lines
5.6 KiB
Go
//nolint:testpackage // white-box tests exercise unexported internals
|
|
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/creack/pty"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
testFlagSignKey = "--sign-key"
|
|
testKeyFile = "/key.asc"
|
|
)
|
|
|
|
// TestGenAndFreshenSignWithKeyFile runs gen, then freshen after a file is
|
|
// added, with --sign-key naming a key file: one key with no passphrase and
|
|
// one protected by the passphrase in MFER_SIGN_KEY_PASSPHRASE. check
|
|
// --require-signature must accept each manifest as signed by that key.
|
|
// freshen leaves its manifest out of the listing only on the real
|
|
// filesystem, so the test uses that.
|
|
func TestGenAndFreshenSignWithKeyFile(t *testing.T) {
|
|
for name, passphrase := range map[string][]byte{
|
|
"unprotected": nil,
|
|
"protected": []byte("passphrase"),
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Setenv(envSignKeyPassphrase, string(passphrase))
|
|
|
|
secretKey, fingerprint := testSecretKey(t, passphrase)
|
|
|
|
fs := afero.NewOsFs()
|
|
keyFile := filepath.Join(t.TempDir(), "key.asc")
|
|
root := t.TempDir()
|
|
manifestPath := filepath.Join(root, defaultManifestName)
|
|
|
|
require.NoError(t, afero.WriteFile(fs, keyFile, secretKey, 0o600))
|
|
writeTestFile(t, fs, filepath.Join(root, testFileTxt), "hello")
|
|
|
|
opts := testOpts([]string{
|
|
testApp, cmdGenerate, "-q", testFlagSignKey, keyFile,
|
|
"-o", manifestPath, root,
|
|
}, fs)
|
|
require.Equal(t, 0, runCLI(opts), testStderr(t, opts))
|
|
|
|
check := []string{
|
|
testApp, cmdCheck, "-q",
|
|
"--" + flagRequireSignature, fingerprint, manifestPath,
|
|
}
|
|
|
|
opts = testOpts(check, fs)
|
|
require.Equal(t, 0, runCLI(opts), testStderr(t, opts))
|
|
|
|
writeTestFile(t, fs, filepath.Join(root, "added.txt"), "added")
|
|
|
|
opts = testOpts([]string{
|
|
testApp, cmdFreshen, "-q", testFlagSignKey, keyFile, manifestPath,
|
|
}, fs)
|
|
require.Equal(t, 0, runCLI(opts), testStderr(t, opts))
|
|
|
|
opts = testOpts(check, fs)
|
|
require.Equal(t, 0, runCLI(opts), testStderr(t, opts))
|
|
assert.Len(t, manifestFiles(t, fs, manifestPath), 2)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSignWithProtectedKeyNeedsPassphrase runs gen with a protected key,
|
|
// with MFER_SIGN_KEY_PASSPHRASE empty and no terminal to ask on. gen must
|
|
// fail, naming the variable, and write no manifest.
|
|
func TestSignWithProtectedKeyNeedsPassphrase(t *testing.T) {
|
|
t.Setenv(envSignKeyPassphrase, "")
|
|
|
|
secretKey, _ := testSecretKey(t, []byte("secret"))
|
|
|
|
fs := afero.NewMemMapFs()
|
|
require.NoError(t, afero.WriteFile(fs, testKeyFile, secretKey, 0o600))
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "hello")
|
|
|
|
opts := testOpts([]string{
|
|
testApp, cmdGenerate, "-q", testFlagSignKey, testKeyFile,
|
|
"-o", testMF, testDir,
|
|
}, fs)
|
|
assert.Equal(t, 1, runCLI(opts))
|
|
assert.Contains(t, testStderr(t, opts),
|
|
"signing key is protected: set MFER_SIGN_KEY_PASSPHRASE to its passphrase")
|
|
|
|
exists, err := afero.Exists(fs, testMF)
|
|
require.NoError(t, err)
|
|
assert.False(t, exists)
|
|
}
|
|
|
|
// TestSignWithWrongPassphraseFailsFirst runs gen on a directory and
|
|
// freshen on a manifest, neither of which exists, with a protected key and
|
|
// a wrong MFER_SIGN_KEY_PASSPHRASE. Each must fail to unlock the key: it
|
|
// does that before it reads any file, so a missing file goes unnoticed.
|
|
func TestSignWithWrongPassphraseFailsFirst(t *testing.T) {
|
|
t.Setenv(envSignKeyPassphrase, "wrong")
|
|
|
|
secretKey, _ := testSecretKey(t, []byte("right"))
|
|
|
|
fs := afero.NewMemMapFs()
|
|
require.NoError(t, afero.WriteFile(fs, testKeyFile, secretKey, 0o600))
|
|
|
|
for _, args := range [][]string{
|
|
{
|
|
testApp, cmdGenerate, "-q", testFlagSignKey, testKeyFile,
|
|
"-o", testMF, "/missing",
|
|
},
|
|
{testApp, cmdFreshen, "-q", testFlagSignKey, testKeyFile, "/missing.mf"},
|
|
} {
|
|
opts := testOpts(args, fs)
|
|
assert.Equal(t, 1, runCLI(opts), args[1])
|
|
assert.Contains(t, testStderr(t, opts),
|
|
testKeyFile+": unlock signing key", args[1])
|
|
}
|
|
}
|
|
|
|
// TestGenAsksForPassphraseOnTerminal runs gen with a protected key, no
|
|
// MFER_SIGN_KEY_PASSPHRASE, and a terminal as stdin and stderr. gen must
|
|
// ask for the passphrase on stderr, and sign with what is typed after the
|
|
// prompt.
|
|
func TestGenAsksForPassphraseOnTerminal(t *testing.T) {
|
|
t.Setenv(envSignKeyPassphrase, "")
|
|
|
|
secretKey, fingerprint := testSecretKey(t, []byte("passphrase"))
|
|
|
|
fs := afero.NewOsFs()
|
|
keyFile := filepath.Join(t.TempDir(), "key.asc")
|
|
root := t.TempDir()
|
|
manifestPath := filepath.Join(root, defaultManifestName)
|
|
|
|
require.NoError(t, afero.WriteFile(fs, keyFile, secretKey, 0o600))
|
|
writeTestFile(t, fs, filepath.Join(root, testFileTxt), "hello")
|
|
|
|
terminal, tty, err := pty.Open()
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() { _ = terminal.Close() })
|
|
|
|
opts := testOpts([]string{
|
|
testApp, cmdGenerate, "-q", testFlagSignKey, keyFile,
|
|
"-o", manifestPath, root,
|
|
}, fs)
|
|
opts.Stdin = tty
|
|
opts.Stderr = tty
|
|
|
|
exitCode := make(chan int, 1)
|
|
|
|
go func() {
|
|
exitCode <- runCLI(opts)
|
|
|
|
// Once gen has ended, reading the terminal fails instead of
|
|
// waiting for a prompt that will not come.
|
|
_ = tty.Close()
|
|
}()
|
|
|
|
prompt := "Passphrase for " + keyFile + ": "
|
|
output := bufio.NewReader(terminal)
|
|
written := ""
|
|
|
|
for !strings.HasSuffix(written, prompt) {
|
|
b, err := output.ReadByte()
|
|
require.NoError(t, err, "gen wrote %q and no prompt", written)
|
|
|
|
written += string(b)
|
|
}
|
|
|
|
_, err = terminal.WriteString("passphrase\n")
|
|
require.NoError(t, err)
|
|
|
|
code := <-exitCode
|
|
rest, _ := io.ReadAll(output)
|
|
require.Equal(t, 0, code, "gen wrote %q", rest)
|
|
|
|
check := testOpts([]string{
|
|
testApp, cmdCheck, "-q",
|
|
"--" + flagRequireSignature, fingerprint, manifestPath,
|
|
}, fs)
|
|
require.Equal(t, 0, runCLI(check), testStderr(t, check))
|
|
}
|