//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)) }