diff --git a/keyfunc/README.md b/keyfunc/README.md new file mode 100644 index 0000000..a5b03d6 --- /dev/null +++ b/keyfunc/README.md @@ -0,0 +1,180 @@ +# 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 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. + +## 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'/'/' +``` + +- `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 `: 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'/'`. 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 +``` + +The comment defaults to `keyfunc/ssh/`; 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 [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=` 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` + +The application number is `657169`, path `m/83696968'/657169'/'`. The 32 +bytes from step 4 are clamped as X25519 requires and become an age 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. + +### `keyfunc age pub` + +Prints the recipient, the `age1...` public key, on one line. + +### `keyfunc age priv` + +Prints the identity, the `AGE-SECRET-KEY-1...` line, and nothing else. + +### `keyfunc age encrypt [-n N] [--to ...] [-o ] []` + +Encrypts the file (or standard input) with age. The recipients are the derived +identity's own recipient, plus any given with `--to`, so the same mnemonic can +always decrypt what it encrypted. Output goes to `-o` or standard output; +`--armor` writes the text form. Nothing is written except the output. + +### `keyfunc age decrypt [-n N] [-o ] []` + +Decrypts the file (or standard input) with the derived identity. Output goes to +`-o` or standard output. If the identity is not one of the recipients, the tool +says so and exits with status 1. + +## Derived mnemonics: `keyfunc mnemonic` + +### `keyfunc mnemonic [-n N] [--words 12|18|24]` + +Prints a child mnemonic derived from the main one, using BIP-85's own mnemonic +application (number `39`, English, path +`m/83696968'/39'/0'/'/'`, entropy taken as the specification says, +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 + +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 +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 +```