README: policy sections and age and child mnemonic vectors (closes #21)
check / check (push) Failing after 1s

Add the sections REPO_POLICIES.md requires that the README lacked: a
first line naming category, license and author; Getting Started;
Entrypoints (one line per script/ file); Rationale; Design (the
internal/ packages, with the existing "Adding a key type" note moved
under it); TODO (the open issues between the tree and 1.0, as links);
License; and Author. Add the age recipients and identity vectors for
the eleven-abandon example mnemonic, copied from the agekey test, and
the BIP-85 specification's own 12-word child mnemonic vector, copied
from the childmnemonic test.

Documentation only; no code changes.

Model: opus-4-8
This commit is contained in:
2026-09-21 07:40:22 +00:00
parent e6ddf49acc
commit 1f0dcb8a03
+129 -27
View File
@@ -1,16 +1,73 @@
# keyfunc # keyfunc
`keyfunc` turns a BIP-39 mnemonic into key pairs that can be recreated from `keyfunc` is an MIT-licensed Go command-line tool by
that mnemonic at any time. The same mnemonic, key type and index always give the [@sneak](https://sneak.berlin) that turns a BIP-39 mnemonic into SSH keys, age
same key. identities and child mnemonics, each of which 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 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. takes the same steps as that repository's `agehd` package.
Commands are grouped by what is derived: `keyfunc ssh ...` for ed25519 SSH Commands are grouped by what is derived: `keyfunc ssh ...` for ed25519 SSH keys,
keys, `keyfunc age ...` for age identities and for encrypting and decrypting `keyfunc age ...` for age identities and for encrypting and decrypting with
with them, and `keyfunc mnemonic ...` for child mnemonics derived from the them, and `keyfunc mnemonic ...` for child mnemonics derived from the main one.
main one.
## Getting Started
Build from a clone and run the binary:
```
git clone git@git.eeqj.de:sneak/keyfunc.git
cd keyfunc
make build
./keyfunc --version
```
`make build` produces `./keyfunc`. Every deriving command needs a mnemonic; see
[Giving it the mnemonic](#giving-it-the-mnemonic) for where it is read from, then
for example:
```
./keyfunc ssh pub -n 0 --mnemonic-command 'secret get foo'
```
## Rationale
A key you can derive again never has to be backed up. One mnemonic, kept safe
once, stands behind every key this tool produces: lose a laptop and the SSH key,
the age identity and any child mnemonic on it come back from the mnemonic alone,
at the same index, byte for byte. Nothing else has to be written down, copied
between machines, or stored in a secret manager, because it can always be
derived again.
## Design
The entry point is a thin `cmd/keyfunc/main.go` (what `make build` builds) that
calls into `internal/`. The packages there are:
- `internal/derive` turns a mnemonic into the 32 bytes a key is made from: it
walks BIP-39 seed, BIP-32 master key and BIP-85 entropy, and holds the shared
constants (the byte count and the largest key index).
- `internal/mnemonic` finds the mnemonic to work from — a command, an
environment variable, or a terminal prompt — and refuses one that fails the
BIP-39 checksum.
- `internal/sshkey` turns the derived bytes into an ed25519 SSH key
(`sshkey.go`) and serves that key from an in-process SSH agent on a private
unix socket, keeping it out of any file (`agent.go`).
- `internal/agekey` turns the derived bytes into an age identity and encrypts
and decrypts with it.
- `internal/childmnemonic` derives a child mnemonic from the main one using
BIP-85's own mnemonic application.
- `internal/cli` builds the cobra command tree and runs it. Under it,
`cli/options` holds the flags every command shares, and `cli/ssh`, `cli/age`
and `cli/mnemonic` are the command groups.
### 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.
## Derivation ## Derivation
@@ -149,6 +206,15 @@ 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 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. this path, which is a change in `secret`, not here.
Test vectors, mnemonic
`abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about`:
```
recipient index 0: age1xwdy9y6ckyfsgjc8k02e9uhsf3fmjy0ufysewlj68kmx5n67e3nsg2mftq
recipient index 1: age1pmm92sxaf5mazjwvjph7dx2zq9r5p8l3rarfgqm7hmakqhvgyy4q5p3w7j
identity index 0: AGE-SECRET-KEY-19QKK2P38598XLXMQFFU3P7J9PLDD7527T70JDHGDJ7AMNF3XT44S00JFU5
```
### `keyfunc age pub` ### `keyfunc age pub`
Prints the recipient, the `age1...` public key, on one line. Prints the recipient, the `age1...` public key, on one line.
@@ -181,33 +247,69 @@ not through step 4). Default 12 words. A child mnemonic is a full mnemonic in
its own right: it can seed another `keyfunc`, another wallet, or `secret`, and its own right: it can seed another `keyfunc`, another wallet, or `secret`, and
it never has to be written down, since it can be derived again. it never has to be written down, since it can be derived again.
## Adding a key type Test vector: the child-mnemonic step is checked against BIP-85's own published
vectors, which derive from the specification's master key
`xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb`.
At key index 0 the 12-word English child mnemonic is:
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 girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose
groups its commands. ```
## Errors ## Errors
Errors go to standard error and the exit status is 1, except for `ssh to`, Errors go to standard error and the exit status is 1, except for `ssh to`,
which passes through `ssh`'s own exit status. which passes through `ssh`'s own exit status.
## Building and running ## Entrypoints
``` The repo adheres to the
make build # produces ./keyfunc [Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
make check # fmt-check, lint (golangci-lint) and tests standard: each Makefile target is a thin shim over an executable in `script/`.
```
Examples: - `script/bootstrap` installs everything needed to build and develop (git, make,
Go), idempotently, from nix, apt, brew or apk; it does not install the linter,
which only runs inside Docker.
- `script/setup` prepares a fresh clone: it runs `bootstrap`, then installs the
git pre-commit hook.
- `script/projectname` prints the project name; other scripts call it so they
stay identical across repos.
- `script/test` runs `go vet` and then the test suite, rerunning verbosely if a
test fails.
- `script/lint` runs the linter inside the image built from `Dockerfile.lint`
(which pins the linter by hash), so a complaint fails the build and leaves no
container behind.
- `script/fmt` formats the Go source in place.
- `script/fmt-check` checks that formatting without writing, failing if anything
is unformatted.
- `script/check` runs `test`, `lint` and `fmt-check` and changes no files.
- `script/docker` builds the Docker image tagged with the project name.
- `script/cibuild` is the CI build the Gitea workflow calls: it runs the linter,
then `docker build`.
- `script/precommit` is what the git pre-commit hook runs: `go mod tidy` and
`go fmt`, failing if `go.mod` or `go.sum` changed, then `check`.
- `script/install-precommit` installs the git pre-commit hook that runs
`script/precommit`.
``` ## TODO
keyfunc ssh pub -n 3 --mnemonic-command 'secret get foo'
keyfunc ssh priv -n 3 > ~/.ssh/id_bip85_3 The open issues that stand between the tree and a 1.0 release:
keyfunc ssh install -n 3 user@example.com
keyfunc ssh to -n 3 user@example.com uptime - [#10 ssh install needs rework](https://git.eeqj.de/sneak/keyfunc/issues/10)
keyfunc age pub -n 0 - [#13 Review toward 1.0: identify and file all next steps](https://git.eeqj.de/sneak/keyfunc/issues/13)
keyfunc age encrypt -n 0 --armor -o notes.age notes.txt - [#14 Choose a license and add LICENSE](https://git.eeqj.de/sneak/keyfunc/issues/14)
keyfunc age decrypt -n 0 notes.age - [#15 Decide the Go module path before 1.0](https://git.eeqj.de/sneak/keyfunc/issues/15)
keyfunc mnemonic -n 1 --words 24 - [#16 Do not pass the mnemonic environment variables on to ssh and sftp](https://git.eeqj.de/sneak/keyfunc/issues/16)
``` - [#17 Clean up the agent socket and working files when a signal ends the tool](https://git.eeqj.de/sneak/keyfunc/issues/17)
- [#22 1.0 release readiness](https://git.eeqj.de/sneak/keyfunc/issues/22)
## License
MIT. The license is not yet settled on the tracker
([#14](https://git.eeqj.de/sneak/keyfunc/issues/14)); MIT is the recommended
option there, so this README names it and the `LICENSE` file is added when that
issue is answered.
## Author
[@sneak](https://sneak.berlin).