Refuse a key index with no hardened child
All checks were successful
check / check (push) Successful in 23s
All checks were successful
check / check (push) Successful in 23s
Every element of the derivation path is hardened, so an index above 2147483647 has no child to derive: the hardened offset wrapped around and the tool silently produced a non-hardened key that no other implementation reading the path as written would reproduce. Such an index is now refused with a message before anything is derived, and tests pin the refusal at both the derivation and the command level. Also use the hook path variable in script/install-precommit instead of repeating the literal beside it. Model: opus-5
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/cli"
|
"git.eeqj.de/sneak/keyfunc/internal/cli"
|
||||||
|
"git.eeqj.de/sneak/keyfunc/internal/derive"
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/mnemonic"
|
"git.eeqj.de/sneak/keyfunc/internal/mnemonic"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
@@ -62,6 +63,14 @@ func TestThePrivateKeyMatchesThePublicOne(t *testing.T) {
|
|||||||
require.Equal(t, vectorOne, back)
|
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) {
|
func TestTheMnemonicCommandIsUsed(t *testing.T) {
|
||||||
t.Setenv(mnemonic.Variable, "")
|
t.Setenv(mnemonic.Variable, "")
|
||||||
|
|
||||||
@@ -77,6 +86,17 @@ func TestTheMnemonicCommandIsUsed(t *testing.T) {
|
|||||||
func run(t *testing.T, args ...string) string {
|
func run(t *testing.T, args ...string) string {
|
||||||
t.Helper()
|
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
|
var out bytes.Buffer
|
||||||
|
|
||||||
root := cli.Root()
|
root := cli.Root()
|
||||||
@@ -84,7 +104,7 @@ func run(t *testing.T, args ...string) string {
|
|||||||
root.SetErr(&out)
|
root.SetErr(&out)
|
||||||
root.SetArgs(args)
|
root.SetArgs(args)
|
||||||
|
|
||||||
require.NoError(t, root.ExecuteContext(t.Context()))
|
err := root.ExecuteContext(t.Context())
|
||||||
|
|
||||||
return out.String()
|
return out.String(), err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package derive
|
package derive
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/secret/pkg/bip85"
|
"git.eeqj.de/sneak/secret/pkg/bip85"
|
||||||
@@ -16,8 +17,18 @@ const (
|
|||||||
|
|
||||||
// Size is how many bytes every key type is given.
|
// Size is how many bytes every key type is given.
|
||||||
Size = 32
|
Size = 32
|
||||||
|
|
||||||
|
// MaxIndex is the largest key index there is. Every element of
|
||||||
|
// the path is hardened, and a hardened BIP-32 child index stops
|
||||||
|
// here.
|
||||||
|
MaxIndex = 1<<31 - 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrIndexTooLarge is returned for a key index above MaxIndex. Such an
|
||||||
|
// index has no hardened child to derive, so there is no key to give
|
||||||
|
// back rather than a key nobody else would reproduce.
|
||||||
|
var ErrIndexTooLarge = errors.New("the key index is too large")
|
||||||
|
|
||||||
// Path returns the derivation path for an application number and a key
|
// Path returns the derivation path for an application number and a key
|
||||||
// index.
|
// index.
|
||||||
func Path(application, index uint32) string {
|
func Path(application, index uint32) string {
|
||||||
@@ -28,7 +39,15 @@ func Path(application, index uint32) string {
|
|||||||
// The mnemonic becomes a seed with an empty passphrase, the seed
|
// The mnemonic becomes a seed with an empty passphrase, the seed
|
||||||
// becomes a master key, the master key gives BIP-85 entropy at the
|
// becomes a master key, the master key gives BIP-85 entropy at the
|
||||||
// path, and the entropy seeds the generator the bytes are read from.
|
// path, and the entropy seeds the generator the bytes are read from.
|
||||||
|
// An index above MaxIndex is refused before any of that happens.
|
||||||
func Bytes(words string, application, index uint32) ([]byte, error) {
|
func Bytes(words string, application, index uint32) ([]byte, error) {
|
||||||
|
if index > MaxIndex {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: %d is above %d",
|
||||||
|
ErrIndexTooLarge, index, MaxIndex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
seed := bip39.NewSeed(words, "")
|
seed := bip39.NewSeed(words, "")
|
||||||
|
|
||||||
master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ func TestEveryIndexGivesItsOwnBytes(t *testing.T) {
|
|||||||
require.False(t, bytes.Equal(first, second))
|
require.False(t, bytes.Equal(first, second))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAnIndexWithNoHardenedChildIsRefused(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := derive.Bytes(example(), application, derive.MaxIndex+1)
|
||||||
|
require.ErrorIs(t, err, derive.ErrIndexTooLarge)
|
||||||
|
|
||||||
|
_, err = derive.Bytes(example(), application, derive.MaxIndex)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestTheSameInputAlwaysGivesTheSameBytes(t *testing.T) {
|
func TestTheSameInputAlwaysGivesTheSameBytes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
hook=".git/hooks/pre-commit"
|
hook=".git/hooks/pre-commit"
|
||||||
printf '#!/bin/sh\nset -e\nscript/precommit\n' > .git/hooks/pre-commit
|
printf '#!/bin/sh\nset -e\nscript/precommit\n' > "$hook"
|
||||||
chmod +x .git/hooks/pre-commit
|
chmod +x "$hook"
|
||||||
echo "pre-commit hook installed: runs script/precommit"
|
echo "pre-commit hook installed: runs script/precommit"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user