Skeleton, mnemonic input, derivation, and the ssh commands #5
Reference in New Issue
Block a user
Delete Branch "issue-1-skeleton"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #1
keyfunc ssh pubandkeyfunc ssh privderive an ed25519 key from a BIP-39 mnemonic and print it, as anauthorized_keysline or as an unencrypted OpenSSH private key. The tests assert both README test vectors end to end, the private key parsing back to the public one, the order the mnemonic sources are tried in, and the refusal of a mnemonic that fails its checksum.main.gosits at the root;internal/mnemonicfinds the mnemonic,internal/derivedoes the path and the bytes,internal/sshkeymakes the key, andinternal/cliholds the command tree with one package per key type below it, so adding a type is one package and one subcommand.What the diff does not show:
Dockerfile.lint, which pins the linter by hash. A build step cannot start Docker, so the rootDockerfileruns the formatting check, the tests and the build, andscript/cibuildruns the linter before it. This departs from the single-Dockerfile lint stage described inREPO_POLICIES.md.go.modasks for Go 1.26 because the pinned linter is built with 1.26.2.nolintcomments: the mnemonic command is meant to be an arbitrary shell command, and the version variable is set at build time.README.mdneeded no change;make buildandmake checkdo what it says they do.LICENSEfile yet; that choice is yours.make checkis green.Model: opus-5
FAIL — needs-rework. Two findings, the first blocking.
1.
internal/derive/derive.go:23, with the flag atinternal/cli/options/options.go:19.--indexis auint32, so the tool accepts 0 through 4294967295, but a hardened BIP-32 child index only reaches 2147483647. For any index at or above 2147483648 the hardened offset added while parsing the path wraps around, and the derivation silently falls through to a non-hardened child instead.keyfunc ssh pub -n 2147483648prints exactly the key that the pathm/83696968'/838372'/0(no apostrophe on the last element) produces, labelledkeyfunc/ssh/2147483648, and says nothing. This matters because the README presents the path as a standard any implementation can follow: half the range the tool advertises in its own help text yields keys that no conforming implementation reproduces, with no error and no warning. Acceptable: refuse an index at or above 2147483648 with a clear message before any derivation happens, plus a test pinning the refusal.2.
script/install-precommit:10.hook=".git/hooks/pre-commit"is assigned and never read; the following two lines write and chmod the same path as a literal. A reader hunts for the use and finds none. Acceptable: use the variable below, or drop the line.What I checked. I read every file in the diff; the code reads plainly in one pass and coins no vocabulary, and
README.mdis untouched. I walked the definition of done item by item against the tree. I ranmake checkin the fresh clone, and re-ran the linter with its Docker cache defeated so the linting step genuinely executed rather than replaying a cached layer. I built the binary and compared both README test vectors byte for byte against its output, fed the printed private key tossh-keygento confirm it yields the same public key and comment, and exercised each mnemonic source, the precedence between them, the bad-checksum refusal, the no-terminal refusal and the exit statuses. I planted defects — a wrong application number, a swapped source order, a dropped whitespace trim — and confirmed the tests catch each. The vendored.golangci.ymlandREPO_POLICIES.mdare byte-identical to the canonical copies.Open points, for the owner rather than the author. There is no
LICENSE; the issue does not ask for one and the policy says to ask you. The rootDockerfilehas no lint stage, which departs fromREPO_POLICIES.md; I read the issue's explicit call forDockerfile.lintas overriding it.Model: opus-5
Both findings addressed on
issue-1-skeleton.1. Every element of the path is hardened, so an index above 2147483647 has no hardened child; the tool used to wrap around and print a key from a non-hardened path with no warning. Such an index is now refused with a message before anything is derived, and two tests pin it: one on the derivation, one on
keyfunc ssh pub -n 2147483648, which now exits 1 and prints nothing on standard output.2. The hook path variable is now used on both lines that had repeated the literal beside it.
Nothing else changed;
README.mduntouched.make checkis green, with the linter run in Docker.Model: opus-5
PASS — no findings. Both round-one findings are fixed and nothing else moved.
Index refusal. Built the binary from the branch head and ran it:
-n 2147483648and-n 4294967295write nothing to standard output, printkeyfunc: the key index is too large: 2147483648 is above 2147483647to standard error and exit 1, for bothssh pubandssh priv;-n 2147483647still prints a key. The check sits ininternal/derive/derive.goahead of the seed, so nothing is derived. I removed the check in a scratch copy and both new tests, ininternal/derive/derive_test.goandinternal/cli/cli_test.go, fail. I moved the comparison by one and the derivation test fails again, so the boundary is pinned in both directions. Out-of-range and non-numeric-nvalues are refused by the flag parser with exit 1.Hook script.
script/install-precommitnow useshookon both lines; run against a scratch repository it writes the hook and marks it executable.README vectors. I extracted both
authorized_keyslines fromREADME.mdand compared them to the binary's output for indices 0 and 1: identical.README.mdis untouched.Gate.
make checkin this fresh clone exits 0, with the linter step genuinely executing inside the image rather than replaying a cached layer. The head is a fast-forward on the currentnext. Both commit messages and the pull request body carry a model line and no attribution.Scope. The rework touches four files, all inside the two findings.
Judgement call: the index is checked after the mnemonic has been fetched, so a
--mnemonic-commandruns before the refusal is printed. The finding asked for refusal before derivation, which this is, so I did not treat it as a defect.Model: opus-5