All checks were successful
check / check (push) Successful in 19s
keyfunc mnemonic prints a child mnemonic derived from the main one with BIP-85's own mnemonic application, in English, at 12, 18 or 24 words. Its entropy is the BIP-85 entropy cut to the length the word count needs, which is what that application asks for, so it does not go through the generator the other key types read their bytes from. The BIP-85 specification's own test vectors for the application are the tests. Two pieces the derivation package already had inside one function are now named: the master key a mnemonic stands for, and the refusal of a key index that has no hardened child. Both key types call them. Model: opus-5
141 lines
3.5 KiB
Go
141 lines
3.5 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.eeqj.de/sneak/keyfunc/internal/childmnemonic"
|
|
"git.eeqj.de/sneak/keyfunc/internal/cli"
|
|
"git.eeqj.de/sneak/keyfunc/internal/derive"
|
|
"git.eeqj.de/sneak/keyfunc/internal/mnemonic"
|
|
"github.com/stretchr/testify/require"
|
|
bip39 "github.com/tyler-smith/go-bip39"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// 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 two child mnemonic lengths the tests ask for.
|
|
const (
|
|
twelve = 12
|
|
twentyFour = 24
|
|
)
|
|
|
|
// 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")),
|
|
)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func TestAnIndexWithNoHardenedChildIsRefused(t *testing.T) {
|
|
t.Setenv(mnemonic.Variable, example())
|
|
|
|
out, err := execute(t, "ssh", "pub", "-n", "2147483648")
|
|
require.ErrorIs(t, err, derive.ErrIndexTooLarge)
|
|
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)
|
|
}
|
|
|
|
func TestALengthTheToolDoesNotOfferIsRefused(t *testing.T) {
|
|
t.Setenv(mnemonic.Variable, example())
|
|
|
|
out, err := execute(t, "mnemonic", "--words", "15")
|
|
require.ErrorIs(t, err, childmnemonic.ErrWordCount)
|
|
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
|
|
}
|