Skeleton, mnemonic input, derivation, and the ssh pub and priv commands (closes #1)
All checks were successful
check / check (push) Successful in 5s
All checks were successful
check / check (push) Successful in 5s
The module, the script entrypoints and Makefile, Docker-only linting, the mnemonic sources in the specified order with their refusals, the BIP-85 derivation, and keyfunc ssh pub and priv with the README test vectors as tests. Two review rounds; the second passed with no findings. Model: opus-5 (implementation and review); fable-5-1 (landing)
This commit was merged in pull request #5.
This commit is contained in:
71
internal/derive/derive.go
Normal file
71
internal/derive/derive.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Package derive turns a mnemonic into the bytes a key is made from.
|
||||
package derive
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.eeqj.de/sneak/secret/pkg/bip85"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
bip39 "github.com/tyler-smith/go-bip39"
|
||||
)
|
||||
|
||||
const (
|
||||
// purpose is the number BIP-85 reserves for itself.
|
||||
purpose = 83696968
|
||||
|
||||
// Size is how many bytes every key type is given.
|
||||
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
|
||||
// index.
|
||||
func Path(application, index uint32) string {
|
||||
return fmt.Sprintf("m/%d'/%d'/%d'", purpose, application, index)
|
||||
}
|
||||
|
||||
// Bytes returns the bytes for an application number and a key index.
|
||||
// The mnemonic becomes a seed with an empty passphrase, the seed
|
||||
// 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.
|
||||
// An index above MaxIndex is refused before any of that happens.
|
||||
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, "")
|
||||
|
||||
master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making the master key: %w", err)
|
||||
}
|
||||
|
||||
entropy, err := bip85.DeriveBIP85Entropy(master, Path(application, index))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("deriving entropy: %w", err)
|
||||
}
|
||||
|
||||
out := make([]byte, Size)
|
||||
|
||||
_, err = bip85.NewBIP85DRNG(entropy).Read(out)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading derived bytes: %w", err)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
61
internal/derive/derive_test.go
Normal file
61
internal/derive/derive_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package derive_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/keyfunc/internal/derive"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// application is the number the SSH key type uses.
|
||||
const application = 838372
|
||||
|
||||
// example returns the mnemonic every BIP-39 document uses to show its
|
||||
// test vectors: eleven abandons and about.
|
||||
func example() string {
|
||||
return strings.Repeat("abandon ", 11) + "about"
|
||||
}
|
||||
|
||||
func TestPathIsTheOneTheSpecificationGives(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.Equal(t, "m/83696968'/838372'/3'", derive.Path(application, 3))
|
||||
}
|
||||
|
||||
func TestEveryIndexGivesItsOwnBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first, err := derive.Bytes(example(), application, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, first, derive.Size)
|
||||
|
||||
second, err := derive.Bytes(example(), application, 1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, second, derive.Size)
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
once, err := derive.Bytes(example(), application, 7)
|
||||
require.NoError(t, err)
|
||||
|
||||
again, err := derive.Bytes(example(), application, 7)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, once, again)
|
||||
}
|
||||
Reference in New Issue
Block a user