diff --git a/README.md b/README.md index 7dcb19f..9eeb02c 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,73 @@ # 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. +`keyfunc` is an MIT-licensed Go command-line tool by +[@sneak](https://sneak.berlin) that turns a BIP-39 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 takes the same steps as that repository's `agehd` package. -Commands are grouped by what is derived: `keyfunc ssh ...` for ed25519 SSH -keys, `keyfunc age ...` for age identities and for encrypting and decrypting -with them, and `keyfunc mnemonic ...` for child mnemonics derived from the -main one. +Commands are grouped by what is derived: `keyfunc ssh ...` for ed25519 SSH keys, +`keyfunc age ...` for age identities and for encrypting and decrypting 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 program is a thin `main.go` 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 @@ -148,6 +205,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 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` Prints the recipient, the `age1...` public key, on one line. @@ -180,33 +246,72 @@ 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 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 -groups its commands. +``` +girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose +``` ## 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 +## Entrypoints -``` -make build # produces ./keyfunc -make check # fmt-check, lint (golangci-lint) and tests -``` +The repo adheres to the +[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) +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`. -``` -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 -keyfunc age pub -n 0 -keyfunc age encrypt -n 0 --armor -o notes.age notes.txt -keyfunc age decrypt -n 0 notes.age -keyfunc mnemonic -n 1 --words 24 -``` +## TODO + +The open issues that stand between the tree and a 1.0 release: + +- [#10 ssh install needs rework](https://git.eeqj.de/sneak/keyfunc/issues/10) +- [#13 Review toward 1.0: identify and file all next steps](https://git.eeqj.de/sneak/keyfunc/issues/13) +- [#14 Choose a license and add LICENSE](https://git.eeqj.de/sneak/keyfunc/issues/14) +- [#15 Decide the Go module path before 1.0](https://git.eeqj.de/sneak/keyfunc/issues/15) +- [#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) +- [#18 Print a real version from a go install build](https://git.eeqj.de/sneak/keyfunc/issues/18) +- [#19 Update dependencies, golang.org/x/crypto first](https://git.eeqj.de/sneak/keyfunc/issues/19) +- [#20 Move main.go to cmd/keyfunc](https://git.eeqj.de/sneak/keyfunc/issues/20) +- [#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).