Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7279329be9 | ||
|
|
64dcc7f42b | ||
|
|
3d90ac87f1 | ||
|
|
e6ddf49acc |
@@ -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
|
||||||
|
|
||||||
@@ -54,8 +111,14 @@ 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
|
refuses and exits with status 1. A mnemonic that fails the BIP-39 checksum is
|
||||||
refused with a message saying so.
|
refused with a message saying so.
|
||||||
|
|
||||||
|
`KEYFUNC_MNEMONIC` and `KEYFUNC_MNEMONIC_COMMAND` are removed from the
|
||||||
|
environment before the system `ssh` (`keyfunc ssh to`) and `sftp`
|
||||||
|
(`keyfunc ssh install`) are started, so the mnemonic is never handed on to
|
||||||
|
them.
|
||||||
|
|
||||||
Every command takes `--index` / `-n` and `--mnemonic-command`, and has `--help`.
|
Every command takes `--index` / `-n` and `--mnemonic-command`, and has `--help`.
|
||||||
`keyfunc --version` prints the version set at build time.
|
`keyfunc --version` prints the version. `make build` stamps it; a binary
|
||||||
|
installed with `go install` reports the module version instead.
|
||||||
|
|
||||||
## SSH keys: `keyfunc ssh`
|
## SSH keys: `keyfunc ssh`
|
||||||
|
|
||||||
@@ -150,6 +213,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.
|
||||||
@@ -182,33 +254,67 @@ 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: most Makefile targets are thin shims over an executable in
|
||||||
```
|
`script/` (`build` and `clean` are the exceptions).
|
||||||
|
|
||||||
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
|
- [#14 Choose a license and add LICENSE](https://git.eeqj.de/sneak/keyfunc/issues/14)
|
||||||
keyfunc age pub -n 0
|
- [#15 Decide the Go module path before 1.0](https://git.eeqj.de/sneak/keyfunc/issues/15)
|
||||||
keyfunc age encrypt -n 0 --armor -o notes.age notes.txt
|
- [#17 Clean up the agent socket and working files when a signal ends the tool](https://git.eeqj.de/sneak/keyfunc/issues/17)
|
||||||
keyfunc age decrypt -n 0 notes.age
|
- [#22 1.0 release readiness](https://git.eeqj.de/sneak/keyfunc/issues/22)
|
||||||
keyfunc mnemonic -n 1 --words 24
|
|
||||||
```
|
## 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).
|
||||||
|
|||||||
+27
-3
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/cli/age"
|
"git.eeqj.de/sneak/keyfunc/internal/cli/age"
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/cli/mnemonic"
|
"git.eeqj.de/sneak/keyfunc/internal/cli/mnemonic"
|
||||||
@@ -13,20 +14,43 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version is what --version prints. The build sets it.
|
// devVersion is what Version holds until a build stamps a real one.
|
||||||
|
const devVersion = "dev"
|
||||||
|
|
||||||
|
// Version is what --version prints. make build stamps it with -ldflags.
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // set at build time with -ldflags
|
//nolint:gochecknoglobals // set at build time with -ldflags
|
||||||
var Version = "dev"
|
var Version = devVersion
|
||||||
|
|
||||||
|
// resolveVersion chooses what --version reports. A value stamped at
|
||||||
|
// build time wins. Otherwise, for a binary from go install, the module
|
||||||
|
// version recorded in the build info is used, unless that is empty or
|
||||||
|
// the "(devel)" of a local build. When neither names a version, the
|
||||||
|
// "dev" fallback stays.
|
||||||
|
func resolveVersion(stamped string, info *debug.BuildInfo) string {
|
||||||
|
if stamped != devVersion {
|
||||||
|
return stamped
|
||||||
|
}
|
||||||
|
|
||||||
|
if info != nil && info.Main.Version != "" &&
|
||||||
|
info.Main.Version != "(devel)" {
|
||||||
|
return info.Main.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
return devVersion
|
||||||
|
}
|
||||||
|
|
||||||
// Root returns the whole command tree.
|
// Root returns the whole command tree.
|
||||||
func Root() *cobra.Command {
|
func Root() *cobra.Command {
|
||||||
|
info, _ := debug.ReadBuildInfo()
|
||||||
|
|
||||||
root := &cobra.Command{
|
root := &cobra.Command{
|
||||||
Use: "keyfunc",
|
Use: "keyfunc",
|
||||||
Short: "derive key pairs from a BIP-39 mnemonic",
|
Short: "derive key pairs from a BIP-39 mnemonic",
|
||||||
Long: "keyfunc turns a BIP-39 mnemonic into key pairs that can " +
|
Long: "keyfunc turns a BIP-39 mnemonic into key pairs that can " +
|
||||||
"be recreated from that mnemonic at any time. The same " +
|
"be recreated from that mnemonic at any time. The same " +
|
||||||
"mnemonic, key type and index always give the same key.",
|
"mnemonic, key type and index always give the same key.",
|
||||||
Version: Version,
|
Version: resolveVersion(Version, info),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ func session(
|
|||||||
|
|
||||||
//nolint:gosec // the options are the user's own, meant for sftp
|
//nolint:gosec // the options are the user's own, meant for sftp
|
||||||
command := exec.CommandContext(cmd.Context(), "sftp", argv...)
|
command := exec.CommandContext(cmd.Context(), "sftp", argv...)
|
||||||
|
command.Env = childEnv()
|
||||||
command.Stdin = strings.NewReader(strings.Join(batch, "\n") + "\n")
|
command.Stdin = strings.NewReader(strings.Join(batch, "\n") + "\n")
|
||||||
command.Stdout = &said
|
command.Stdout = &said
|
||||||
command.Stderr = &said
|
command.Stderr = &said
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package ssh
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/cli/options"
|
"git.eeqj.de/sneak/keyfunc/internal/cli/options"
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/derive"
|
"git.eeqj.de/sneak/keyfunc/internal/derive"
|
||||||
|
"git.eeqj.de/sneak/keyfunc/internal/mnemonic"
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/sshkey"
|
"git.eeqj.de/sneak/keyfunc/internal/sshkey"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -84,6 +87,26 @@ func write(cmd *cobra.Command, text string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// childEnv is the tool's environment with the mnemonic variables taken
|
||||||
|
// out, for the ssh and sftp children it starts. "ssh to" exists so the
|
||||||
|
// private key never leaves the tool; the mnemonic, from either variable,
|
||||||
|
// must not leave it either.
|
||||||
|
func childEnv() []string {
|
||||||
|
environ := os.Environ()
|
||||||
|
kept := make([]string, 0, len(environ))
|
||||||
|
|
||||||
|
for _, entry := range environ {
|
||||||
|
name, _, _ := strings.Cut(entry, "=")
|
||||||
|
if name == mnemonic.Variable || name == mnemonic.CommandVariable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kept = append(kept, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return kept
|
||||||
|
}
|
||||||
|
|
||||||
// addComment gives a command its comment flag.
|
// addComment gives a command its comment flag.
|
||||||
func addComment(cmd *cobra.Command) {
|
func addComment(cmd *cobra.Command) {
|
||||||
cmd.Flags().String(
|
cmd.Flags().String(
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ func to() *cobra.Command {
|
|||||||
func connect(ctx context.Context, argv []string) error {
|
func connect(ctx context.Context, argv []string) error {
|
||||||
//nolint:gosec // the arguments are the user's own, meant for ssh
|
//nolint:gosec // the arguments are the user's own, meant for ssh
|
||||||
command := exec.CommandContext(ctx, "ssh", argv...)
|
command := exec.CommandContext(ctx, "ssh", argv...)
|
||||||
|
command.Env = childEnv()
|
||||||
command.Stdin = os.Stdin
|
command.Stdin = os.Stdin
|
||||||
command.Stdout = os.Stdout
|
command.Stdout = os.Stdout
|
||||||
command.Stderr = os.Stderr
|
command.Stderr = os.Stderr
|
||||||
|
|||||||
@@ -60,13 +60,19 @@ const (
|
|||||||
// an authorized_keys file.
|
// an authorized_keys file.
|
||||||
const keyLine = vectorZero + " keyfunc/ssh/0\n"
|
const keyLine = vectorZero + " keyfunc/ssh/0\n"
|
||||||
|
|
||||||
|
// marker is a variable set beside the mnemonic ones and expected to
|
||||||
|
// reach the stand-in, so a scrubbed environment is told apart from an
|
||||||
|
// empty one.
|
||||||
|
const marker = "KEYFUNC_TEST_MARKER"
|
||||||
|
|
||||||
// installer is a stand-in for the system sftp for the install
|
// installer is a stand-in for the system sftp for the install
|
||||||
// command. It writes down the arguments and every command of the
|
// command. It writes down the arguments and every command of the
|
||||||
// batch it is given, echoes each command as sftp does, and carries
|
// batch it is given, echoes each command as sftp does, writes down its
|
||||||
// the commands out against a directory standing in for the host's
|
// own environment when a test asks for it, and carries the commands out
|
||||||
// home directory, so that what keyfunc sends can be watched doing its
|
// against a directory standing in for the host's home directory, so that
|
||||||
// work. A command that begins with a dash may fail; any other failure
|
// what keyfunc sends can be watched doing its work. A command that
|
||||||
// ends the session, as it does in sftp's own batch mode.
|
// begins with a dash may fail; any other failure ends the session, as it
|
||||||
|
// does in sftp's own batch mode.
|
||||||
//
|
//
|
||||||
// The listing and the two ways a get can fail are worded as the
|
// The listing and the two ways a get can fail are worded as the
|
||||||
// OpenSSH client words them, each naming the path the server expanded.
|
// OpenSSH client words them, each naming the path the server expanded.
|
||||||
@@ -81,6 +87,7 @@ const keyLine = vectorZero + " keyfunc/ssh/0\n"
|
|||||||
// draws the warning ssh writes for it, which carries the wording of a
|
// draws the warning ssh writes for it, which carries the wording of a
|
||||||
// missing file into a session that goes on to authenticate.
|
// missing file into a session that goes on to authenticate.
|
||||||
const installer = `
|
const installer = `
|
||||||
|
[ -n "$KEYFUNC_TEST_ENVIRONMENT" ] && env > "$KEYFUNC_TEST_ENVIRONMENT"
|
||||||
previous=
|
previous=
|
||||||
for argument in "$@"; do
|
for argument in "$@"; do
|
||||||
printf '%s\n' "$argument" >> "$KEYFUNC_TEST_ARGUMENTS"
|
printf '%s\n' "$argument" >> "$KEYFUNC_TEST_ARGUMENTS"
|
||||||
@@ -144,9 +151,11 @@ done
|
|||||||
|
|
||||||
// caller is a stand-in for the system ssh for the to command. It
|
// caller is a stand-in for the system ssh for the to command. It
|
||||||
// writes down the arguments it was given, notes the agent socket if
|
// writes down the arguments it was given, notes the agent socket if
|
||||||
// there really is one at the path it was handed, and ends with the
|
// there really is one at the path it was handed, writes down its own
|
||||||
// status the test asked for.
|
// environment when a test asks for it, and ends with the status the
|
||||||
|
// test asked for.
|
||||||
const caller = `
|
const caller = `
|
||||||
|
[ -n "$KEYFUNC_TEST_ENVIRONMENT" ] && env > "$KEYFUNC_TEST_ENVIRONMENT"
|
||||||
for argument in "$@"; do
|
for argument in "$@"; do
|
||||||
printf '%s\n' "$argument" >> "$KEYFUNC_TEST_ARGUMENTS"
|
printf '%s\n' "$argument" >> "$KEYFUNC_TEST_ARGUMENTS"
|
||||||
done
|
done
|
||||||
@@ -444,6 +453,36 @@ func TestTheToolEndsWithTheStatusSSHEndedWith(t *testing.T) {
|
|||||||
require.Equal(t, failingStatus, cli.Main())
|
require.Equal(t, failingStatus, cli.Main())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTheMnemonicIsNotHandedToSFTP(t *testing.T) {
|
||||||
|
t.Setenv(mnemonic.CommandVariable, "echo "+example())
|
||||||
|
t.Setenv(mnemonic.Variable, example())
|
||||||
|
t.Setenv(marker, "reaches the stand-in")
|
||||||
|
|
||||||
|
pretendHost(t)
|
||||||
|
environment := recordEnvironment(t)
|
||||||
|
|
||||||
|
install(t, host)
|
||||||
|
|
||||||
|
mnemonicWithheld(t, read(t, environment))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTheMnemonicIsNotHandedToSSH(t *testing.T) {
|
||||||
|
t.Setenv(mnemonic.CommandVariable, "echo "+example())
|
||||||
|
t.Setenv(mnemonic.Variable, example())
|
||||||
|
t.Setenv(marker, "reaches the stand-in")
|
||||||
|
|
||||||
|
pretendCall(t)
|
||||||
|
environment := recordEnvironment(t)
|
||||||
|
|
||||||
|
_, err := execute(t, subcommand, "to", host, "uptime")
|
||||||
|
|
||||||
|
var passed ssh.StatusError
|
||||||
|
|
||||||
|
require.ErrorAs(t, err, &passed)
|
||||||
|
|
||||||
|
mnemonicWithheld(t, read(t, environment))
|
||||||
|
}
|
||||||
|
|
||||||
// pretendHost puts the install stand-in on the path and gives back the
|
// pretendHost puts the install stand-in on the path and gives back the
|
||||||
// places it writes to.
|
// places it writes to.
|
||||||
func pretendHost(t *testing.T) pretended {
|
func pretendHost(t *testing.T) pretended {
|
||||||
@@ -592,6 +631,28 @@ func standIn(t *testing.T, name, body string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordEnvironment asks the stand-in to write its environment down and
|
||||||
|
// gives back the file it writes it to.
|
||||||
|
func recordEnvironment(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "environment")
|
||||||
|
t.Setenv("KEYFUNC_TEST_ENVIRONMENT", path)
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// mnemonicWithheld requires that neither mnemonic variable reached the
|
||||||
|
// stand-in and that the marker set beside them did, so an empty
|
||||||
|
// environment does not pass for a scrubbed one.
|
||||||
|
func mnemonicWithheld(t *testing.T, environment string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
require.NotContains(t, environment, mnemonic.Variable+"=")
|
||||||
|
require.NotContains(t, environment, mnemonic.CommandVariable+"=")
|
||||||
|
require.Contains(t, environment, marker+"=")
|
||||||
|
}
|
||||||
|
|
||||||
// read returns what is in a file.
|
// read returns what is in a file.
|
||||||
func read(t *testing.T, path string) string {
|
func read(t *testing.T, path string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime/debug"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
release := &debug.BuildInfo{Main: debug.Module{Version: "v1.2.3"}}
|
||||||
|
local := &debug.BuildInfo{Main: debug.Module{Version: "(devel)"}}
|
||||||
|
empty := &debug.BuildInfo{}
|
||||||
|
|
||||||
|
t.Run("stamped value wins over build info", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
require.Equal(t, "v0.1.0", resolveVersion("v0.1.0", release))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("go install reports the module version", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
require.Equal(t, "v1.2.3", resolveVersion(devVersion, release))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("a local build stays dev", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
require.Equal(t, devVersion, resolveVersion(devVersion, local))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no version anywhere stays dev", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
require.Equal(t, devVersion, resolveVersion(devVersion, empty))
|
||||||
|
require.Equal(t, devVersion, resolveVersion(devVersion, nil))
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user