153 lines
4.0 KiB
Go
153 lines
4.0 KiB
Go
|
|
//go:build windows
|
||
|
|
|
||
|
|
package starssh
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"errors"
|
||
|
|
"io"
|
||
|
|
"net"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strconv"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestParseGPGAssuanSocketInfo(t *testing.T) {
|
||
|
|
info, ok := parseGPGAssuanSocketInfo([]byte("7247\n0123456789abcdef"))
|
||
|
|
if !ok {
|
||
|
|
t.Fatal("expected Assuan socket info to parse")
|
||
|
|
}
|
||
|
|
if info.port != 7247 || string(info.nonce) != "0123456789abcdef" || info.cygwin {
|
||
|
|
t.Fatalf("info=%+v nonce=%x", info, info.nonce)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestParseGPGCygwinSocketInfo(t *testing.T) {
|
||
|
|
info, ok := parseGPGCygwinSocketInfo([]byte("!<socket >7247 s 00000001-02030405-06070809-0a0b0c0d\x00"))
|
||
|
|
if !ok {
|
||
|
|
t.Fatal("expected Cygwin socket info to parse")
|
||
|
|
}
|
||
|
|
want := []byte{1, 0, 0, 0, 5, 4, 3, 2, 9, 8, 7, 6, 13, 12, 11, 10}
|
||
|
|
if info.port != 7247 || string(info.nonce) != string(want) || !info.cygwin {
|
||
|
|
t.Fatalf("info=%+v nonce=%x", info, info.nonce)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestParseGPGAssuanSocketRedirect(t *testing.T) {
|
||
|
|
t.Setenv("STARSSH_TEST_PIPE", `\\.\pipe\openssh-ssh-agent`)
|
||
|
|
target, ok := parseGPGAssuanSocketRedirect([]byte("%Assuan%\r\nsocket=${STARSSH_TEST_PIPE}\r\n"))
|
||
|
|
if !ok {
|
||
|
|
t.Fatal("expected Assuan redirect to parse")
|
||
|
|
}
|
||
|
|
if target != `\\.\pipe\openssh-ssh-agent` {
|
||
|
|
t.Fatalf("target=%q", target)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestReadInvalidAgentSSHSocketReturnsGPGSocketError(t *testing.T) {
|
||
|
|
path := t.TempDir() + "/S.gpg-agent.ssh"
|
||
|
|
if err := os.WriteFile(path, []byte("not a socket info file"), 0o600); err != nil {
|
||
|
|
t.Fatalf("write socket file: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
_, err := dialResolvedSSHAgent(resolvedSSHAgentEndpoint{
|
||
|
|
Endpoint: path,
|
||
|
|
Source: "SSH_AUTH_SOCK",
|
||
|
|
Network: defaultSSHAgentNetwork(path),
|
||
|
|
}, 0)
|
||
|
|
if !errors.Is(err, errInvalidGPGSocketInfo) {
|
||
|
|
t.Fatalf("err=%v want errInvalidGPGSocketInfo", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMissingAgentSSHSocketReturnsReadError(t *testing.T) {
|
||
|
|
path := filepath.Join(t.TempDir(), "S.gpg-agent.ssh")
|
||
|
|
|
||
|
|
_, err := dialResolvedSSHAgent(resolvedSSHAgentEndpoint{
|
||
|
|
Endpoint: path,
|
||
|
|
Source: "identity-agent",
|
||
|
|
Network: defaultSSHAgentNetwork(path),
|
||
|
|
}, 0)
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("expected missing GPG socket file error")
|
||
|
|
}
|
||
|
|
if !errors.Is(err, os.ErrNotExist) {
|
||
|
|
t.Fatalf("err=%v want os.ErrNotExist", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUnreadableAgentSSHSocketReturnsReadError(t *testing.T) {
|
||
|
|
path := filepath.Join(t.TempDir(), "S.gpg-agent.ssh")
|
||
|
|
if err := os.Mkdir(path, 0o700); err != nil {
|
||
|
|
t.Fatalf("mkdir socket path: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
_, err := dialResolvedSSHAgent(resolvedSSHAgentEndpoint{
|
||
|
|
Endpoint: path,
|
||
|
|
Source: "identity-agent",
|
||
|
|
Network: defaultSSHAgentNetwork(path),
|
||
|
|
}, 0)
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("expected unreadable GPG socket file error")
|
||
|
|
}
|
||
|
|
if errors.Is(err, errInvalidGPGSocketInfo) {
|
||
|
|
t.Fatalf("err=%v should expose read failure before parse", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDialWindowsGPGSocketFilePerformsNonceHandshake(t *testing.T) {
|
||
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("listen tcp: %v", err)
|
||
|
|
}
|
||
|
|
defer listener.Close()
|
||
|
|
|
||
|
|
type handshakeResult struct {
|
||
|
|
nonce []byte
|
||
|
|
err error
|
||
|
|
}
|
||
|
|
resultCh := make(chan handshakeResult, 1)
|
||
|
|
go func() {
|
||
|
|
conn, err := listener.Accept()
|
||
|
|
if err != nil {
|
||
|
|
resultCh <- handshakeResult{err: err}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer conn.Close()
|
||
|
|
|
||
|
|
nonce := make([]byte, 16)
|
||
|
|
if _, err := io.ReadFull(conn, nonce); err != nil {
|
||
|
|
resultCh <- handshakeResult{err: err}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
resultCh <- handshakeResult{nonce: append([]byte(nil), nonce...)}
|
||
|
|
}()
|
||
|
|
|
||
|
|
socketPath := filepath.Join(t.TempDir(), "S.gpg-agent.ssh")
|
||
|
|
if err := os.WriteFile(socketPath, []byte(strconv.Itoa(listener.Addr().(*net.TCPAddr).Port)+"\n0123456789abcdef"), 0o600); err != nil {
|
||
|
|
t.Fatalf("write socket file: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
conn, err := dialWindowsGPGSocketFile(socketPath, time.Second)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("dialWindowsGPGSocketFile: %v", err)
|
||
|
|
}
|
||
|
|
_ = conn.Close()
|
||
|
|
|
||
|
|
var result handshakeResult
|
||
|
|
select {
|
||
|
|
case result = <-resultCh:
|
||
|
|
case <-time.After(time.Second):
|
||
|
|
t.Fatal("listener did not accept GPG socket connection")
|
||
|
|
}
|
||
|
|
if result.err != nil {
|
||
|
|
t.Fatalf("listener handshake error: %v", result.err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if !bytes.Equal(result.nonce, []byte("0123456789abcdef")) {
|
||
|
|
t.Fatalf("nonce=%q", result.nonce)
|
||
|
|
}
|
||
|
|
}
|