1
0
forked from sneak/hacks
Files
hacks/keyfunc/README.md
clawbot b2214ca5c5 keyfunc: the tool's name, chosen by sneak
The name says what the tool is: a key is a function of the mnemonic and
an index, computed when asked for and stored nowhere. Directory, module,
binary and environment variables take it.

Model: fable-5-1
2026-09-07 13:29:02 +00:00

5.3 KiB

keyfunc

keyfunc turns a BIP-39 mnemonic into key pairs that can be recreated from that mnemonic at any time. The same mnemonic, key type and index always give the same key.

It uses the BIP-85 entropy deriver from git.eeqj.de/sneak/secret/pkg/bip85 and takes the same steps as that repository's agehd package.

Commands are grouped by key type: keyfunc ssh ... for ed25519 SSH keys (in this PR) and keyfunc age ... for age identities (planned, see below).

Derivation

The mnemonic is turned into a key like this:

  1. mnemonic -> BIP-39 seed (empty passphrase);
  2. seed -> BIP-32 master key;
  3. master key -> 64 bytes of BIP-85 entropy at the path below;
  4. entropy -> BIP-85 DRNG (SHAKE256); read 32 bytes;
  5. those 32 bytes become the key in the way the key type needs.

The path is:

m/83696968'/<app>'/<n>'
  • 83696968 is the fixed BIP-85 purpose.
  • app is the application number of the key type. There is no vendor id: the path is meant as a standard any implementation can follow, not something tied to one tool. Each key type's number is spelled the way BIP-85 spells its own RSA application (828365 is the ASCII codes of R, S, A written out): SSH is 838372 (S S H), age is 657169 (A G E).
  • n is the key index: flag --index / -n, default 0.

Giving it the mnemonic

The mnemonic itself is never a command-line argument. It is looked for in this order; the first one found wins:

  1. --mnemonic-command <command>: a shell command, run with sh -c, whose standard output is the mnemonic. Example: --mnemonic-command 'secret get foo'. Whitespace around the output is dropped. If the command exits with a non-zero status, the tool prints its standard error and exits with status 1.
  2. Environment variable KEYFUNC_MNEMONIC_COMMAND: the same, as a shell command held in the environment.
  3. Environment variable KEYFUNC_MNEMONIC: the mnemonic itself.
  4. A prompt on the terminal with echo turned off.

If none of these is available and standard input is not a terminal, the tool refuses and exits with status 1. A mnemonic that fails the BIP-39 checksum is refused with a message saying so.

Every command takes --index / -n and --mnemonic-command, and has --help. keyfunc --version prints the version set at build time.

SSH keys: keyfunc ssh

Only ed25519 keys are produced. The application number is 838372, so the path is m/83696968'/838372'/<n>'. The 32 bytes from step 4 are the ed25519 seed.

Test vector, mnemonic abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about:

index 0: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJZOtOczrc/7CQytcuFwt7s4r8KjkZWkwjLZWBaFKD+7
index 1: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOEWY8+/gmHYVC4u0Y0I4FKs+eVUulTPHfk9VtXw1tMF

keyfunc ssh pub

Prints one authorized_keys line to standard output:

ssh-ed25519 <key> <comment>

The comment defaults to keyfunc/ssh/<n>; change it with --comment.

keyfunc ssh priv

Prints the unencrypted private key in OpenSSH format (the -----BEGIN OPENSSH PRIVATE KEY----- block that ssh reads) to standard output and nothing else, so it can be redirected into a file. The key's comment is the same as for pub.

keyfunc ssh install <[user@]host> [-- ssh options...]

Runs the system ssh to the host and, on the host:

  • creates ~/.ssh with mode 0700 if it is missing;
  • creates ~/.ssh/authorized_keys with mode 0600 if it is missing;
  • appends the pub line only if an identical line is not already there.

It then prints added or already present. How this ssh connection authenticates is up to the user's normal ssh setup (existing keys, agent, password). Anything after -- is passed to ssh unchanged.

keyfunc ssh to <host> [ssh arguments...]

Derives the key, serves it from an SSH agent that runs inside the tool on a unix socket in a new private 0700 temporary directory, then runs the system ssh with -o IdentityAgent=<that socket> followed by the host and all remaining arguments unchanged. The tool exits with ssh's exit status and removes the socket and directory on the way out. The private key is never written to disk.

age identities: keyfunc age (planned)

Not in this PR, which delivers the SSH type first; it is the next type to add. keyfunc age pub and keyfunc age priv will use application number 657169, path m/83696968'/657169'/<n>'; the 32 bytes from step 4 are clamped as X25519 requires and encoded as an AGE-SECRET-KEY-1... identity, the same steps sneak/secret takes in its agehd package. secret derives at a vendor-specific path today; for its keys to equal this tool's it moves to this path, which is a change in secret, not here.

Adding a key type

Adding a key type is one package under internal/ that turns the 32 derived bytes into that type's key, plus one cobra subcommand under internal/cli/ that groups its commands.

Errors

Errors go to standard error and the exit status is 1, except for ssh to, which passes through ssh's own exit status.

Building and running

make build    # produces ./keyfunc
make check    # fmt-check, lint (golangci-lint) and tests

Examples:

keyfunc ssh pub -n 3 --mnemonic-command 'secret get foo'
keyfunc ssh priv -n 3 > ~/.ssh/id_bip85_3
keyfunc ssh install -n 3 user@example.com
keyfunc ssh to -n 3 user@example.com uptime