All checks were successful
check / check (push) Successful in 2m30s
keyfunc ssh install appends the public line on a host through the system ssh, only when absent, feeding the line on standard input; keyfunc ssh to serves the derived key from an in-process agent on a private socket and runs the system ssh with it, the private key never on disk. Two review rounds; the second passed with no findings. Model: opus-5 (implementation and review); fable-5-1 (landing)
129 lines
2.9 KiB
Go
129 lines
2.9 KiB
Go
package sshkey_test
|
|
|
|
import (
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.eeqj.de/sneak/keyfunc/internal/derive"
|
|
"git.eeqj.de/sneak/keyfunc/internal/sshkey"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/agent"
|
|
)
|
|
|
|
// agentDirectoryMode is what the directory holding the agent socket
|
|
// has to be: nobody but its owner may enter it.
|
|
const agentDirectoryMode = 0o700
|
|
|
|
// exampleIndex is the key index every test here derives at.
|
|
const exampleIndex = 0
|
|
|
|
// example returns the mnemonic every BIP-39 document uses to show its
|
|
// test vectors: eleven abandons and about.
|
|
func example() string {
|
|
return strings.Repeat("abandon ", 11) + "about"
|
|
}
|
|
|
|
func TestTooFewBytesAreRefused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := sshkey.New([]byte("short"))
|
|
require.ErrorIs(t, err, sshkey.ErrSize)
|
|
}
|
|
|
|
func TestTheCommentIsPutAtTheEndOfTheLine(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key := exampleKey(t)
|
|
|
|
line, err := key.Line("hello")
|
|
require.NoError(t, err)
|
|
require.True(t, strings.HasPrefix(line, "ssh-ed25519 "))
|
|
require.True(t, strings.HasSuffix(line, " hello"))
|
|
}
|
|
|
|
func TestThePrivateKeyCarriesTheSamePublicKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key := exampleKey(t)
|
|
|
|
line, err := key.Line("")
|
|
require.NoError(t, err)
|
|
|
|
block, err := key.Block("a comment")
|
|
require.NoError(t, err)
|
|
|
|
parsed, err := ssh.ParsePrivateKey([]byte(block))
|
|
require.NoError(t, err)
|
|
|
|
back := strings.TrimSpace(
|
|
string(ssh.MarshalAuthorizedKey(parsed.PublicKey())),
|
|
)
|
|
require.Equal(t, line, back)
|
|
}
|
|
|
|
func TestTheAgentServesTheOneKeyAndNothingElse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key := exampleKey(t)
|
|
|
|
served, err := key.Serve(t.Context(), "a comment")
|
|
require.NoError(t, err)
|
|
t.Cleanup(served.Stop)
|
|
|
|
directory, err := os.Stat(filepath.Dir(served.Socket()))
|
|
require.NoError(t, err)
|
|
require.Equal(t,
|
|
os.FileMode(agentDirectoryMode), directory.Mode().Perm(),
|
|
)
|
|
|
|
var dialer net.Dialer
|
|
|
|
connection, err := dialer.DialContext(t.Context(), "unix", served.Socket())
|
|
require.NoError(t, err)
|
|
|
|
defer func() { _ = connection.Close() }()
|
|
|
|
held, err := agent.NewClient(connection).List()
|
|
require.NoError(t, err)
|
|
require.Len(t, held, 1)
|
|
|
|
line, err := key.Line("a comment")
|
|
require.NoError(t, err)
|
|
require.Equal(t, line, held[0].String())
|
|
}
|
|
|
|
func TestStoppingTheAgentLeavesNothingBehind(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
served, err := exampleKey(t).Serve(t.Context(), "a comment")
|
|
require.NoError(t, err)
|
|
|
|
directory := filepath.Dir(served.Socket())
|
|
require.DirExists(t, directory)
|
|
|
|
served.Stop()
|
|
require.NoDirExists(t, directory)
|
|
|
|
var dialer net.Dialer
|
|
|
|
_, err = dialer.DialContext(t.Context(), "unix", served.Socket())
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// exampleKey derives the key the example mnemonic gives.
|
|
func exampleKey(t *testing.T) *sshkey.Key {
|
|
t.Helper()
|
|
|
|
material, err := derive.Bytes(example(), sshkey.Application, exampleIndex)
|
|
require.NoError(t, err)
|
|
|
|
key, err := sshkey.New(material)
|
|
require.NoError(t, err)
|
|
|
|
return key
|
|
}
|