1 Commits
Author SHA1 Message Date
sneak 332bc909ce Clean up the agent socket and working files when a signal ends the tool (closes #17)
check / check (push) Successful in 39s
Main ran the command tree on a background context, so SIGINT, SIGTERM or
SIGHUP killed the process before the deferred cleanup ran: the agent's
temporary directory and socket, and the install working directory, were
left behind.

Main now runs the tree on a signal.NotifyContext for those three signals.
A signal cancels the context, which ends the child ssh or sftp started
with exec.CommandContext, and the deferred cleanup then runs. The exit
status after a signal stays 1 unless ssh reported one of its own.

For "ssh to" the child is cancelled with SIGTERM rather than the default
kill, so ssh restores the terminal before it goes.

Model: opus-4-8
2026-09-21 14:07:40 +00:00
+27 -126
View File
@@ -1,74 +1,16 @@
# keyfunc # keyfunc
`keyfunc` is a Go command-line tool by [@sneak](https://sneak.berlin) — its `keyfunc` turns a BIP-39 mnemonic into key pairs that can be recreated from
license is not yet chosen that mnemonic at any time. The same mnemonic, key type and index always give the
([#14](https://git.eeqj.de/sneak/keyfunc/issues/14)) — that turns a BIP-39 same key.
mnemonic into SSH keys, age 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 keys, Commands are grouped by what is derived: `keyfunc ssh ...` for ed25519 SSH
`keyfunc age ...` for age identities and for encrypting and decrypting with keys, `keyfunc age ...` for age identities and for encrypting and decrypting
them, and `keyfunc mnemonic ...` for child mnemonics derived from the main one. with them, and `keyfunc mnemonic ...` for child mnemonics derived from the
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
@@ -216,15 +158,6 @@ 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.
@@ -257,65 +190,33 @@ 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.
Test vector: the child-mnemonic step is checked against BIP-85's own published ## Adding a key type
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
girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose bytes into that type's key, plus one cobra subcommand under `internal/cli/` that
``` 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.
## Entrypoints ## Building and running
The repo adheres to the ```
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) make build # produces ./keyfunc
standard: most Makefile targets are thin shims over an executable in make check # fmt-check, lint (golangci-lint) and tests
`script/` (`build` and `clean` are the exceptions). ```
- `script/bootstrap` installs everything needed to build and develop (git, make, Examples:
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'
The open issues that stand between the tree and a 1.0 release: keyfunc ssh priv -n 3 > ~/.ssh/id_bip85_3
keyfunc ssh install -n 3 user@example.com
- [#14 Choose a license and add LICENSE](https://git.eeqj.de/sneak/keyfunc/issues/14) keyfunc ssh to -n 3 user@example.com uptime
- [#15 Decide the Go module path before 1.0](https://git.eeqj.de/sneak/keyfunc/issues/15) keyfunc age pub -n 0
- [#22 1.0 release readiness](https://git.eeqj.de/sneak/keyfunc/issues/22) keyfunc age encrypt -n 0 --armor -o notes.age notes.txt
keyfunc age decrypt -n 0 notes.age
## License keyfunc mnemonic -n 1 --words 24
```
Not yet chosen. The license is the owner's decision, still open on the tracker
([#14](https://git.eeqj.de/sneak/keyfunc/issues/14)); the `LICENSE` file is added
when that issue is answered.
## Author
[@sneak](https://sneak.berlin).