The private key is now the BIP85 HEX application output for 32 bytes at
m/83696968'/128169'/32'/{index}' from the BIP32 master key of the BIP39
seed, via sneak.berlin/go/secret/pkg/bip85, so any BIP85 tool reproduces
it and one mnemonic backs many keypairs through Params.Index. Tests
check the mnemonic-to-master step and the HEX application against the
BIP85 specification vectors and pin the resulting keypair.
225 lines
6.2 KiB
Go
225 lines
6.2 KiB
Go
package cbk_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tyler-smith/go-bip39"
|
|
|
|
"sneak.berlin/go/secret/pkg/bip85"
|
|
"sneak.berlin/go/x/pkg/bip39wordlist"
|
|
"sneak.berlin/go/x/pkg/cbk"
|
|
)
|
|
|
|
// The BIP85 specification publishes its test vectors against one master
|
|
// key. specMnemonic is the BIP39 phrase that produces that master key with
|
|
// an empty passphrase, so the whole mnemonic-to-entropy chain this package
|
|
// relies on can be checked against the specification.
|
|
const (
|
|
specMnemonic = "install scatter logic circle pencil average fall shoe " +
|
|
"quantum disease suspect usage"
|
|
|
|
specMasterKey = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8" +
|
|
"kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
|
|
|
|
// specHex64 is the specification's HEX application vector for
|
|
// m/83696968'/128169'/64'/0'.
|
|
specHex64 = "492db4698cf3b73a5a24998aa3e9d7fa96275d85724a91e7" +
|
|
"1aa2d645442f878555d078fd1f1f67e368976f04137b1f7a" +
|
|
"0d19232136ca50c44614af72b5582a5c"
|
|
)
|
|
|
|
// The keys below are the keypair for specMnemonic at index 0, pinned when
|
|
// the BIP85 derivation was implemented. If this test starts failing, the
|
|
// derivation has changed and existing keys are no longer reproducible.
|
|
const (
|
|
vectorPublicKeyHex = "dbef711b3f340d336eb99cd8d0dbd491e1edce8eaad71cd66be228032a9a743e"
|
|
|
|
vectorPrivateKeyHex = "ea3ceb0b02ee8e587779c63f4b7b3a21e950a213f1ec53cab608d13e8796e6dc"
|
|
)
|
|
|
|
func TestSpecMnemonicProducesSpecMasterKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
masterKey := specMasterKeyFromMnemonic(t)
|
|
assert.Equal(t, specMasterKey, masterKey.String())
|
|
|
|
hex64, err := bip85.DeriveHex(masterKey, 64, 0)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, specHex64, hex64)
|
|
}
|
|
|
|
func TestNewMatchesPinnedVector(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keypair, err := cbk.FromMnemonic(specMnemonic)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, vectorPublicKeyHex, keypair.PublicKeyHex())
|
|
assert.Equal(t, vectorPrivateKeyHex, keypair.PrivateKeyHex())
|
|
}
|
|
|
|
func TestPrivateKeyIsBIP85HexApplicationOutput(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
masterKey := specMasterKeyFromMnemonic(t)
|
|
|
|
for _, index := range []uint32{0, 1, 7} {
|
|
expected, err := bip85.DeriveHex(masterKey, cbk.KeySize, index)
|
|
require.NoError(t, err)
|
|
|
|
keypair, err := cbk.New(cbk.Params{
|
|
Mnemonic: specMnemonic,
|
|
Passphrase: "",
|
|
Index: index,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, expected, keypair.PrivateKeyHex(), "index %d", index)
|
|
}
|
|
}
|
|
|
|
func TestNewIndexSelectsIndependentKeypairs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
first, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Passphrase: "", Index: 0})
|
|
require.NoError(t, err)
|
|
|
|
second, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Passphrase: "", Index: 1})
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEqual(t, first.PublicKey, second.PublicKey)
|
|
assert.NotEqual(t, first.PrivateKey, second.PrivateKey)
|
|
|
|
fromMnemonic, err := cbk.FromMnemonic(specMnemonic)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, first, fromMnemonic, "FromMnemonic uses index 0")
|
|
}
|
|
|
|
func TestNewRejectsIndexBeyondHardenedRange(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := cbk.New(cbk.Params{
|
|
Mnemonic: specMnemonic,
|
|
Passphrase: "",
|
|
Index: cbk.MaxIndex + 1,
|
|
})
|
|
require.ErrorIs(t, err, cbk.ErrIndexTooLarge)
|
|
|
|
_, err = cbk.New(cbk.Params{
|
|
Mnemonic: specMnemonic,
|
|
Passphrase: "",
|
|
Index: cbk.MaxIndex,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestNewNormalizesMnemonic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
canonical, err := cbk.FromMnemonic(specMnemonic)
|
|
require.NoError(t, err)
|
|
|
|
words := strings.Fields(specMnemonic)
|
|
messy := " " + strings.ToUpper(words[0]) + "\t" +
|
|
strings.Join(words[1:], " ") + "\n"
|
|
|
|
normalized, err := cbk.New(cbk.Params{
|
|
Mnemonic: messy,
|
|
Passphrase: "",
|
|
Index: 0,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, canonical, normalized)
|
|
}
|
|
|
|
func TestNewPassphraseChangesKeys(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
plain, err := cbk.FromMnemonic(specMnemonic)
|
|
require.NoError(t, err)
|
|
|
|
extraWord := "TREZOR"
|
|
|
|
withExtraWord, err := cbk.New(cbk.Params{
|
|
Mnemonic: specMnemonic,
|
|
Passphrase: extraWord,
|
|
Index: 0,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEqual(t, plain.PublicKey, withExtraWord.PublicKey)
|
|
assert.NotEqual(t, plain.PrivateKey, withExtraWord.PrivateKey)
|
|
|
|
again, err := cbk.New(cbk.Params{
|
|
Mnemonic: specMnemonic,
|
|
Passphrase: extraWord,
|
|
Index: 0,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, withExtraWord, again, "derivation is deterministic")
|
|
}
|
|
|
|
func TestNewDifferentMnemonicsGiveDifferentKeys(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
other, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
|
|
require.NoError(t, err)
|
|
|
|
specKeypair, err := cbk.FromMnemonic(specMnemonic)
|
|
require.NoError(t, err)
|
|
|
|
otherKeypair, err := cbk.FromMnemonic(other)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEqual(t, specKeypair.PublicKey, otherKeypair.PublicKey)
|
|
assert.NotEqual(t, specKeypair.PrivateKey, otherKeypair.PrivateKey)
|
|
}
|
|
|
|
func TestNewRejectsInvalidMnemonic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := cbk.FromMnemonic("abandon bogus about")
|
|
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
|
|
|
|
words := strings.Fields(specMnemonic)
|
|
words[len(words)-1] = "bogus"
|
|
|
|
_, err = cbk.FromMnemonic(strings.Join(words, " "))
|
|
require.ErrorIs(t, err, bip39wordlist.ErrUnknownWord)
|
|
}
|
|
|
|
func TestKeyAccessors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keypair, err := cbk.FromMnemonic(specMnemonic)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, keypair.PublicKey[:], keypair.PublicKeyBytes())
|
|
assert.Equal(t, keypair.PrivateKey[:], keypair.PrivateKeyBytes())
|
|
assert.Len(t, keypair.PublicKeyBytes(), cbk.KeySize)
|
|
assert.Len(t, keypair.PrivateKeyBytes(), cbk.KeySize)
|
|
assert.Equal(t, keypair.PublicKey.Hex(), keypair.PublicKeyHex())
|
|
assert.Equal(t, keypair.PrivateKey.Hex(), keypair.PrivateKeyHex())
|
|
assert.Len(t, keypair.PublicKeyHex(), 2*cbk.KeySize)
|
|
}
|
|
|
|
// specMasterKeyFromMnemonic builds the BIP32 master key from specMnemonic
|
|
// the same way the package does, independently of it.
|
|
func specMasterKeyFromMnemonic(t *testing.T) *hdkeychain.ExtendedKey {
|
|
t.Helper()
|
|
|
|
seed := bip39.NewSeed(specMnemonic, "")
|
|
|
|
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
|
require.NoError(t, err)
|
|
|
|
return masterKey
|
|
}
|