Skeleton, mnemonic input, derivation, and the ssh commands (closes #1)
All checks were successful
check / check (push) Successful in 34s
All checks were successful
check / check (push) Successful in 34s
The tool derives ed25519 SSH keys from a BIP-39 mnemonic and prints them, either as an authorized_keys line or as an unencrypted OpenSSH private key. Both README test vectors are asserted in the tests. The mnemonic is looked for in the order the README gives, and refused when it fails its checksum or when there is nowhere left to look. The repository standards come with it: the vendored linter configuration and policies, the script/ entrypoints with a thin Makefile, and a Gitea workflow. Linting happens only inside the image built from Dockerfile.lint, which pins the linter by hash, so the root Dockerfile runs the formatting check, the tests and the build, and script/cibuild runs the linter before it. Model: opus-5
This commit is contained in:
52
internal/derive/derive.go
Normal file
52
internal/derive/derive.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Package derive turns a mnemonic into the bytes a key is made from.
|
||||
package derive
|
||||
|
||||
import (
|
||||
"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
|
||||
)
|
||||
|
||||
// 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.
|
||||
func Bytes(words string, application, index uint32) ([]byte, error) {
|
||||
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
|
||||
}
|
||||
51
internal/derive/derive_test.go
Normal file
51
internal/derive/derive_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
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 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