starssh/sshagent_windows_test.go
starainrt 0c23e7d4bf
feat: 增强 ssh-agent 认证与转发可靠性
- 拆分 ssh-agent 认证、连接与 endpoint 解析逻辑
- 新增 IdentityAgent、SSHAgentTimeout、SSHAgentForwardTimeout 和调试事件
- 为 agent list/sign 操作增加独立 deadline,避免硬件 agent 卡死登录
- 支持 agent signer 失败后跳过坏 key 并重试后续 key
- 优先处理 RSA-SHA2 签名,兼容现代 OpenSSH 认证要求
- 增强 agent forwarding 的探测、通道空闲超时和关闭清理
- 补充 Windows OpenSSH pipe 与 GPG S.gpg-agent.ssh socket 文件支持
- 增加相关回归测试和 Windows 编译验证覆盖
2026-05-27 13:10:35 +08:00

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)
}
}