package deployment import ( "bytes" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "encoding/pem" "errors" "fmt" "net" "strconv" "strings" "sync/atomic" "testing" "time" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" ) func TestSSHFactoryConnectsUsingRegisteredHostKeyAcrossAlgorithms(t *testing.T) { clientSigner, privateKey := newTestSSHSigner(t) ecdsaHostSigner := newTestECDSAHostSigner(t) ed25519HostSigner, _ := newTestSSHSigner(t) server := startTestSSHServer(t, func(key ssh.PublicKey) error { if !bytes.Equal(key.Marshal(), clientSigner.PublicKey().Marshal()) { return errors.New("测试客户端公钥不匹配") } return nil }, ecdsaHostSigner, ed25519HostSigner) factory := SSHFactory{ Secrets: staticSecretReader{ "secret://ssh/hostkey-fallback": {"private_key": privateKey}, }, Timeout: 2 * time.Second, HostKeyAlgorithms: []string{ ssh.KeyAlgoECDSA256, ssh.KeyAlgoED25519, }, } client, err := factory.Connect(t.Context(), testSSHServerModel( server, "secret://ssh/hostkey-fallback", ssh.FingerprintSHA256(ed25519HostSigner.PublicKey()), )) if err != nil { t.Fatalf("应在 ECDSA 指纹不匹配后使用 ED25519 主机密钥连接:%v", err) } defer client.Close() if server.accepted.Load() != 2 { t.Fatalf("应分别尝试两个主机密钥算法,实际连接次数:%d", server.accepted.Load()) } if exists, err := client.Exists(t.Context(), "."); err != nil || !exists { t.Fatalf("SFTP 连接不可用:exists=%t err=%v", exists, err) } } func TestSSHFactorySkipsUnavailableSafeHostKeyAlgorithm(t *testing.T) { clientSigner, privateKey := newTestSSHSigner(t) ed25519HostSigner, _ := newTestSSHSigner(t) server := startTestSSHServer(t, func(key ssh.PublicKey) error { if !bytes.Equal(key.Marshal(), clientSigner.PublicKey().Marshal()) { return errors.New("测试客户端公钥不匹配") } return nil }, ed25519HostSigner) factory := SSHFactory{ Secrets: staticSecretReader{ "secret://ssh/hostkey-unavailable": {"private_key": privateKey}, }, Timeout: 2 * time.Second, HostKeyAlgorithms: []string{ ssh.KeyAlgoECDSA256, ssh.KeyAlgoED25519, }, } client, err := factory.Connect(t.Context(), testSSHServerModel( server, "secret://ssh/hostkey-unavailable", ssh.FingerprintSHA256(ed25519HostSigner.PublicKey()), )) if err != nil { t.Fatalf("不可用的安全主机密钥算法应被跳过:%v", err) } client.Close() if server.accepted.Load() != 2 { t.Fatalf("应在算法协商失败后尝试下一种安全算法,实际连接次数:%d", server.accepted.Load()) } } func TestSSHFactoryRejectsUnregisteredHostKeysAcrossAlgorithms(t *testing.T) { clientSigner, privateKey := newTestSSHSigner(t) ecdsaHostSigner := newTestECDSAHostSigner(t) ed25519HostSigner, _ := newTestSSHSigner(t) server := startTestSSHServer(t, func(key ssh.PublicKey) error { if !bytes.Equal(key.Marshal(), clientSigner.PublicKey().Marshal()) { return errors.New("测试客户端公钥不匹配") } return nil }, ecdsaHostSigner, ed25519HostSigner) factory := SSHFactory{ Secrets: staticSecretReader{ "secret://ssh/hostkey-reject": {"private_key": privateKey}, }, Timeout: 2 * time.Second, HostKeyAlgorithms: []string{ ssh.KeyAlgoECDSA256, ssh.KeyAlgoED25519, }, } _, err := factory.Connect(t.Context(), testSSHServerModel( server, "secret://ssh/hostkey-reject", "SHA256:unregistered-test-host-key", )) if err == nil || !strings.Contains(err.Error(), "SSH 主机指纹不匹配") { t.Fatalf("未登记的主机指纹必须被拒绝:%v", err) } if !strings.Contains(err.Error(), ssh.KeyAlgoECDSA256) || !strings.Contains(err.Error(), ssh.KeyAlgoED25519) { t.Fatalf("错误应包含已经安全校验的主机密钥算法:%v", err) } if server.accepted.Load() != 2 { t.Fatalf("错误指纹应校验两个可用算法,实际连接次数:%d", server.accepted.Load()) } } func TestSSHFactoryStopsAfterMatchedHostKeyAuthenticationFailure(t *testing.T) { _, privateKey := newTestSSHSigner(t) ecdsaHostSigner := newTestECDSAHostSigner(t) ed25519HostSigner, _ := newTestSSHSigner(t) server := startTestSSHServer(t, func(ssh.PublicKey) error { return errors.New("测试认证拒绝") }, ecdsaHostSigner, ed25519HostSigner) factory := SSHFactory{ Secrets: staticSecretReader{ "secret://ssh/auth-reject": {"private_key": privateKey}, }, Timeout: 2 * time.Second, HostKeyAlgorithms: []string{ ssh.KeyAlgoECDSA256, ssh.KeyAlgoED25519, }, } _, err := factory.Connect(t.Context(), testSSHServerModel( server, "secret://ssh/auth-reject", ssh.FingerprintSHA256(ecdsaHostSigner.PublicKey()), )) if err == nil || !strings.Contains(err.Error(), "SSH 握手失败") { t.Fatalf("主机指纹匹配后的认证失败必须直接返回:%v", err) } if strings.Contains(err.Error(), "SSH 主机指纹不匹配") { t.Fatalf("认证失败不得继续尝试其他主机密钥算法:%v", err) } if server.accepted.Load() != 1 { t.Fatalf("认证失败后不得继续重连,实际连接次数:%d", server.accepted.Load()) } } func TestSSHFactoryDefaultHostKeyAlgorithmsExcludeCertificates(t *testing.T) { algorithms, err := (SSHFactory{}).secureHostKeyAlgorithms() if err != nil { t.Fatalf("默认 SSH 主机密钥算法过滤失败:%v", err) } if len(algorithms) == 0 { t.Fatal("默认 SSH 主机密钥算法不能为空") } for _, algorithm := range algorithms { if strings.HasSuffix(algorithm, "-cert-v01@openssh.com") { t.Fatalf("默认算法不应包含 OpenSSH 证书算法:%s", algorithm) } } } func TestSSHFactoryFiltersUnsupportedAndDuplicateHostKeyAlgorithms(t *testing.T) { factory := SSHFactory{HostKeyAlgorithms: []string{ "ssh-dss", ssh.KeyAlgoED25519, ssh.KeyAlgoED25519, }} algorithms, err := factory.secureHostKeyAlgorithms() if err != nil { t.Fatalf("安全算法过滤失败:%v", err) } if len(algorithms) != 1 || algorithms[0] != ssh.KeyAlgoED25519 { t.Fatalf("算法过滤结果不正确:%v", algorithms) } factory.HostKeyAlgorithms = []string{"ssh-dss"} if _, err := factory.secureHostKeyAlgorithms(); err == nil { t.Fatal("仅包含不安全或不支持算法时必须拒绝") } } type testSSHServer struct { listener net.Listener accepted atomic.Int32 host string port int config *ssh.ServerConfig channelHandler func(ssh.Channel, <-chan *ssh.Request) } func startTestSSHServer( t *testing.T, authorize func(ssh.PublicKey) error, hostSigners ...ssh.Signer, ) *testSSHServer { return startTestSSHServerWithHandler(t, authorize, serveTestSFTP, hostSigners...) } func startTestSSHServerWithHandler( t *testing.T, authorize func(ssh.PublicKey) error, handler func(ssh.Channel, <-chan *ssh.Request), hostSigners ...ssh.Signer, ) *testSSHServer { t.Helper() config := &ssh.ServerConfig{ PublicKeyCallback: func(_ ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { return nil, authorize(key) }, } for _, signer := range hostSigners { config.AddHostKey(signer) } listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("启动测试 SSH 服务失败:%v", err) } host, portText, err := net.SplitHostPort(listener.Addr().String()) if err != nil { listener.Close() t.Fatalf("解析测试 SSH 地址失败:%v", err) } port, err := strconv.Atoi(portText) if err != nil { listener.Close() t.Fatalf("解析测试 SSH 端口失败:%v", err) } server := &testSSHServer{ listener: listener, host: host, port: port, config: config, channelHandler: handler, } t.Cleanup(func() { listener.Close() }) go server.serve() return server } func (s *testSSHServer) serve() { for { connection, err := s.listener.Accept() if err != nil { return } s.accepted.Add(1) go s.serveConnection(connection) } } func (s *testSSHServer) serveConnection(connection net.Conn) { defer connection.Close() _, channels, requests, err := ssh.NewServerConn(connection, s.config) if err != nil { return } go ssh.DiscardRequests(requests) for channelRequest := range channels { if channelRequest.ChannelType() != "session" { channelRequest.Reject(ssh.UnknownChannelType, "session channel required") continue } channel, requests, err := channelRequest.Accept() if err != nil { continue } go s.channelHandler(channel, requests) } } func serveTestSFTP(channel ssh.Channel, requests <-chan *ssh.Request) { defer channel.Close() for request := range requests { if request.Type != "subsystem" { request.Reply(false, nil) continue } var subsystem struct { Name string } if err := ssh.Unmarshal(request.Payload, &subsystem); err != nil || subsystem.Name != "sftp" { request.Reply(false, nil) continue } if err := request.Reply(true, nil); err != nil { return } server, err := sftp.NewServer(channel) if err != nil { return } _ = server.Serve() _ = server.Close() return } } func newTestSSHSigner(t *testing.T) (ssh.Signer, string) { t.Helper() _, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatalf("生成测试 ED25519 密钥失败:%v", err) } signer, err := ssh.NewSignerFromKey(privateKey) if err != nil { t.Fatalf("创建测试 SSH 签名器失败:%v", err) } block, err := ssh.MarshalPrivateKey(privateKey, "scert-ssh-test") if err != nil { t.Fatalf("编码测试 SSH 私钥失败:%v", err) } return signer, string(pem.EncodeToMemory(block)) } func newTestECDSAHostSigner(t *testing.T) ssh.Signer { t.Helper() privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("生成测试 ECDSA 主机密钥失败:%v", err) } signer, err := ssh.NewSignerFromKey(privateKey) if err != nil { t.Fatalf("创建测试 ECDSA 主机签名器失败:%v", err) } return signer } func testSSHServerModel(server *testSSHServer, privateKeyRef, fingerprint string) Server { return Server{ ID: fmt.Sprintf("server-%d", server.port), Name: "多主机密钥测试服务器", Address: server.host, Port: server.port, Username: "deploy", PrivateKeyRef: privateKeyRef, HostKeyFingerprint: fingerprint, CertificatePath: "/tmp/scert-test-cert.pem", PrivateKeyPath: "/tmp/scert-test-key.pem", CertificateChainPath: "/tmp/scert-test-fullchain.pem", WebServer: WebServerNginx, } }