check / check (push) Canceled after 0s
The key index, --words, the --to recipients and the input file of age encrypt and age decrypt were checked only after the mnemonic had been obtained, so a run that could not succeed still prompted for it or ran the mnemonic command. options.Index now refuses an index above derive.MaxIndex, which covers every command group; the mnemonic command calls the new childmnemonic.CheckWords first; age encrypt reads its recipients with the new agekey.Recipients, and both age commands open their input, before the key is derived. The README says which checks come first. Judgement call: the -o path is still looked at after the mnemonic. Model: opus-5-5
258 lines
7.1 KiB
Go
258 lines
7.1 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"bytes"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
"sneak.berlin/go/keyfunc/internal/bip39"
|
|
"sneak.berlin/go/keyfunc/internal/childmnemonic"
|
|
"sneak.berlin/go/keyfunc/internal/cli"
|
|
"sneak.berlin/go/keyfunc/internal/derive"
|
|
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
|
)
|
|
|
|
// The two lines the README says the example mnemonic produces.
|
|
const (
|
|
vectorZero = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJZOtOczrc/7CQytc" +
|
|
"uFwt7s4r8KjkZWkwjLZWBaFKD+7"
|
|
vectorOne = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOEWY8+/gmHYVC4u0Y" +
|
|
"0I4FKs+eVUulTPHfk9VtXw1tMF"
|
|
)
|
|
|
|
// The child mnemonic the README says the example mnemonic gives at
|
|
// index 0.
|
|
const childZero = "prosper short ramp prepare exchange stove life " +
|
|
"snack client enough purpose fold"
|
|
|
|
// The two child mnemonic lengths the tests ask for.
|
|
const (
|
|
twelve = 12
|
|
twentyFour = 24
|
|
)
|
|
|
|
// tooLarge is the smallest key index with no hardened child.
|
|
const tooLarge = "2147483648"
|
|
|
|
// failingMnemonicCommand fails loudly, so a run that looks for the
|
|
// mnemonic ends with that failure. A run that ends with the refusal of
|
|
// an argument instead shows that the argument was checked first.
|
|
const failingMnemonicCommand = "echo the mnemonic was looked for >&2; exit 1"
|
|
|
|
// example returns the mnemonic the README gives its test vectors for:
|
|
// eleven abandons and about.
|
|
func example() string {
|
|
return strings.Repeat("abandon ", 11) + "about"
|
|
}
|
|
|
|
func TestTheReadmeTestVectors(t *testing.T) {
|
|
t.Setenv(mnemonic.Variable, example())
|
|
|
|
require.Equal(t,
|
|
vectorZero+" keyfunc/ssh/0",
|
|
strings.TrimSpace(run(t, "ssh", "pub", "-n", "0")),
|
|
)
|
|
require.Equal(t,
|
|
vectorOne+" keyfunc/ssh/1",
|
|
strings.TrimSpace(run(t, "ssh", "pub", "-n", "1")),
|
|
)
|
|
require.Equal(t,
|
|
childZero, strings.TrimSpace(run(t, "mnemonic", "-n", "0")),
|
|
)
|
|
}
|
|
|
|
func TestTheSpacingBetweenTheWordsDoesNotChangeTheKeys(t *testing.T) {
|
|
words := strings.Fields(example())
|
|
|
|
for name, spaced := range map[string]string{
|
|
"one word per line": strings.Join(words, "\n"),
|
|
"double spaces": strings.Join(words, " "),
|
|
"tabs": strings.Join(words, "\t"),
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Setenv(mnemonic.Variable, spaced)
|
|
|
|
require.Equal(t,
|
|
vectorZero+" keyfunc/ssh/0",
|
|
strings.TrimSpace(run(t, "ssh", "pub", "-n", "0")),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTheCommentCanBeChosen(t *testing.T) {
|
|
t.Setenv(mnemonic.Variable, example())
|
|
|
|
line := strings.TrimSpace(run(t, "ssh", "pub", "--comment", "mine"))
|
|
require.Equal(t, vectorZero+" mine", line)
|
|
}
|
|
|
|
func TestThePrivateKeyMatchesThePublicOne(t *testing.T) {
|
|
t.Setenv(mnemonic.Variable, example())
|
|
|
|
block := run(t, "ssh", "priv", "-n", "1")
|
|
require.True(t,
|
|
strings.HasPrefix(block, "-----BEGIN OPENSSH PRIVATE KEY-----"),
|
|
)
|
|
|
|
parsed, err := ssh.ParsePrivateKey([]byte(block))
|
|
require.NoError(t, err)
|
|
|
|
back := strings.TrimSpace(
|
|
string(ssh.MarshalAuthorizedKey(parsed.PublicKey())),
|
|
)
|
|
require.Equal(t, vectorOne, back)
|
|
}
|
|
|
|
// The mnemonic command fails in these runs, so the refusal of the index
|
|
// shows that it was checked before the mnemonic was looked for. The age
|
|
// commands have a test of their own.
|
|
func TestAnIndexWithNoHardenedChildIsRefused(t *testing.T) {
|
|
t.Setenv(mnemonic.CommandVariable, failingMnemonicCommand)
|
|
|
|
for _, args := range [][]string{
|
|
{subcommand, "pub", "-n", tooLarge},
|
|
{subcommand, "priv", "-n", tooLarge},
|
|
{subcommand, installing, "-n", tooLarge, host},
|
|
{subcommand, "to", "-n", tooLarge, host},
|
|
{"mnemonic", "-n", tooLarge},
|
|
} {
|
|
name := strings.Join(args, " ")
|
|
|
|
out, err := execute(t, args...)
|
|
require.ErrorIs(t, err, derive.ErrIndexTooLarge, name)
|
|
require.Empty(t, out, name)
|
|
}
|
|
}
|
|
|
|
func TestAnIndexWithALeadingZeroIsDecimal(t *testing.T) {
|
|
t.Setenv(mnemonic.Variable, example())
|
|
|
|
line := run(t, "ssh", "pub", "-n", "010")
|
|
require.Equal(t, run(t, "ssh", "pub", "-n", "10"), line)
|
|
require.True(t,
|
|
strings.HasSuffix(strings.TrimSpace(line), " keyfunc/ssh/10"),
|
|
)
|
|
}
|
|
|
|
// The mnemonic command in these runs fails, so a refusal naming the
|
|
// flag shows that the number was refused before the mnemonic was
|
|
// looked for.
|
|
func TestAnIndexThatIsNotDecimalDigitsIsRefused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, index := range []string{"0x10", "1_0", "+1", "-1", ""} {
|
|
t.Run(index, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
out, err := execute(t,
|
|
"ssh", "pub", "-n", index, "--mnemonic-command", "exit 1",
|
|
)
|
|
require.ErrorIs(t, err, strconv.ErrSyntax)
|
|
require.ErrorContains(t, err, "--index")
|
|
require.Empty(t, out)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Neither a command group nor --version reads the index, so these
|
|
// show that it is refused while the flags are parsed.
|
|
func TestAnIndexNotInDecimalIsRefusedBeforeAnyCommandRuns(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, args := range [][]string{
|
|
{"ssh", "-n", "0x10"},
|
|
{"-n", "0x10", "--version"},
|
|
} {
|
|
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
out, err := execute(t, args...)
|
|
require.ErrorIs(t, err, strconv.ErrSyntax)
|
|
require.ErrorContains(t, err, "--index")
|
|
require.Empty(t, out)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTheMnemonicCommandIsUsed(t *testing.T) {
|
|
t.Setenv(mnemonic.Variable, "")
|
|
|
|
line := strings.TrimSpace(run(t,
|
|
"ssh", "pub",
|
|
"--mnemonic-command", "printf '%s\\n' '"+example()+"'",
|
|
))
|
|
require.Equal(t, vectorZero+" keyfunc/ssh/0", line)
|
|
}
|
|
|
|
func TestAChildMnemonicIsPrinted(t *testing.T) {
|
|
t.Setenv(mnemonic.Variable, example())
|
|
|
|
short := strings.Fields(run(t, "mnemonic"))
|
|
require.Len(t, short, twelve)
|
|
require.True(t, bip39.IsMnemonicValid(strings.Join(short, " ")))
|
|
|
|
long := strings.Fields(run(t, "mnemonic", "--words", "24"))
|
|
require.Len(t, long, twentyFour)
|
|
|
|
next := strings.Fields(run(t, "mnemonic", "-n", "1"))
|
|
require.NotEqual(t, short, next)
|
|
}
|
|
|
|
// The mnemonic command fails in this run, so the refusal of the length
|
|
// shows that it was checked before the mnemonic was looked for.
|
|
func TestALengthTheToolDoesNotOfferIsRefused(t *testing.T) {
|
|
t.Setenv(mnemonic.CommandVariable, failingMnemonicCommand)
|
|
|
|
out, err := execute(t, "mnemonic", "--words", "15")
|
|
require.ErrorIs(t, err, childmnemonic.ErrWordCount)
|
|
require.Empty(t, out)
|
|
}
|
|
|
|
func TestTheWordCountIsDecimal(t *testing.T) {
|
|
t.Setenv(mnemonic.Variable, example())
|
|
|
|
words := strings.Fields(run(t, "mnemonic", "--words", "012"))
|
|
require.Len(t, words, twelve)
|
|
|
|
// As for the index, the failing mnemonic command shows that the
|
|
// number was refused first.
|
|
out, err := execute(t,
|
|
"mnemonic", "--words", "0x18", "--mnemonic-command", "exit 1",
|
|
)
|
|
require.ErrorIs(t, err, strconv.ErrSyntax)
|
|
require.ErrorContains(t, err, "--words")
|
|
require.Empty(t, out)
|
|
}
|
|
|
|
// run executes the tool with the given arguments and returns what it
|
|
// wrote to standard output.
|
|
func run(t *testing.T, args ...string) string {
|
|
t.Helper()
|
|
|
|
out, err := execute(t, args...)
|
|
require.NoError(t, err)
|
|
|
|
return out
|
|
}
|
|
|
|
// execute runs the tool and returns both what it wrote and how it
|
|
// ended.
|
|
func execute(t *testing.T, args ...string) (string, error) {
|
|
t.Helper()
|
|
|
|
var out bytes.Buffer
|
|
|
|
root := cli.Root()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs(args)
|
|
|
|
err := root.ExecuteContext(t.Context())
|
|
|
|
return out.String(), err
|
|
}
|