Skeleton, mnemonic input, derivation, and the ssh commands (closes #1)
All checks were successful
check / check (push) Successful in 34s

The tool derives ed25519 SSH keys from a BIP-39 mnemonic and prints
them, either as an authorized_keys line or as an unencrypted OpenSSH
private key. Both README test vectors are asserted in the tests.

The mnemonic is looked for in the order the README gives, and refused
when it fails its checksum or when there is nowhere left to look.

The repository standards come with it: the vendored linter
configuration and policies, the script/ entrypoints with a thin
Makefile, and a Gitea workflow. Linting happens only inside the image
built from Dockerfile.lint, which pins the linter by hash, so the root
Dockerfile runs the formatting check, the tests and the build, and
script/cibuild runs the linter before it.

Model: opus-5
This commit is contained in:
2026-09-07 14:44:33 +00:00
parent c3fd26a1de
commit 90a7c96cd1
34 changed files with 1737 additions and 0 deletions

63
internal/sshkey/sshkey.go Normal file
View File

@@ -0,0 +1,63 @@
// Package sshkey turns derived bytes into an ed25519 SSH key.
package sshkey
import (
"crypto/ed25519"
"encoding/pem"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/ssh"
)
// Application is the number this key type occupies in the derivation
// path. It spells SSH the way BIP-85 spells RSA, as the ASCII codes of
// the letters written out.
const Application = 838372
// ErrSize is returned when the derived bytes are not the length an
// ed25519 seed has to be.
var ErrSize = errors.New("an ed25519 key needs 32 derived bytes")
// Key is one ed25519 SSH key.
type Key struct {
private ed25519.PrivateKey
}
// New makes a key whose ed25519 seed is the derived bytes.
func New(derived []byte) (*Key, error) {
if len(derived) != ed25519.SeedSize {
return nil, fmt.Errorf("%w, got %d", ErrSize, len(derived))
}
return &Key{private: ed25519.NewKeyFromSeed(derived)}, nil
}
// Line returns the public key as one authorized_keys line, without a
// trailing newline.
func (k *Key) Line(comment string) (string, error) {
public, err := ssh.NewPublicKey(k.private.Public())
if err != nil {
return "", fmt.Errorf("encoding the public key: %w", err)
}
line := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(public)))
if comment != "" {
line += " " + comment
}
return line, nil
}
// Block returns the unencrypted private key in the OpenSSH format that
// ssh reads, ending in a newline.
func (k *Key) Block(comment string) (string, error) {
block, err := ssh.MarshalPrivateKey(k.private, comment)
if err != nil {
return "", fmt.Errorf("encoding the private key: %w", err)
}
return string(pem.EncodeToMemory(block)), nil
}