Golang 的 ssh 包 与 Open-SSH 的兼容性
GoSSHD
golang.org/x/crypto/ssh
中的 ServerConfig
有 PublicKeyCallback
字段,用于接受客户端发过来的公钥,做一个验证;
但是参数只有一个 PublicKey
以及网络元信息,好像也只能检测一下 authorized_keys
中有没有该公钥了。
最近发现使用 Open-SSH 客户端来连接自己用 golang 写的 SSH 服务器总是无法调用 PublicKeyCallback
;大致看了一下源码,觉得可能原因如下:
公钥身份认证
Open-SSH
客户端通过服务端返回的失败消息中包含的方法列表,来进行下一步处理,如果列表中包含 publickey
,那么 Open-SSH
会中断连接;如果列表中包含 password
, 就会再次尝试重新输入密码来认证;而方法列表是 ServerConfig
设置的回调函数决定的。
而 Open-SSH 的认证顺序就是 none
-> password
,至于 publickey
方式在哪里,反正我在debug过程中没看见。。。
一句话就是:Open-SSH 第一次发送的认证方式为 none
,如果服务器没通过认证,而且 Open-SSH 收到失败消息中的可用方法列表中没有 password
,那么之后就 Open-SSH 就不再尝试连接了。。。
自始至终 Open-SSH 都不会动用密钥身份验证方式来连接用 golang ssh 包编写的服务器。
难道是 Open-SSH 使用 none
作为密钥身份认证的标识?🤔
过程如下:
golang ssh 身份验证逻辑
在 golang golang.org/x/crypto/ssh
包 server.go
中,身份验证放在一个 for
循环中:
如果失败次数达到最大值 MaxAuthTries
,会发送一个 “失败太多次的消息” 来告知客户端停止连接并返回:
紧接着是一段 switch
结构通过客户端的身份认证请求方式来执行认证过程,此处我们定义的 callback 函数将被调用:
然后如果失败(对于服务器只设置了 PublickeyCallback
而客户端发送了 none
请求来说必定失败),会将认证方法添加到失败消息中:
关于 userAuthFailureMsg
这个被定义于 RFC 4252 5.1.
中:https://datatracker.ietf.org/doc/html/rfc4252#section-5.1
If the server rejects the authentication request, it MUST respond with the following:
byte SSH_MSG_USERAUTH_FAILURE name-list authentications that can continue boolean partial success
It is RECOMMENDED that servers only include those 'method name' values in the name-list that are actually useful. However, it is not illegal to include 'method name' values that cannot be used to authenticate the user.
Already successfully completed authentications SHOULD NOT be included in the name-list, unless they should be performed again for some reason.
The value of 'partial success' MUST be TRUE if the authentication request to which this is a response was successful. It MUST be FALSE if the request was not successfully processed.
大概就是说如果身份验证失败,那么服务端要发送一个包含可用的身份验证方式列表的验证消息至客户端。
在 golang 中定义如下:
type userAuthFailureMsg struct {
Methods []string `sshtype:"51"`
PartialSuccess bool
}
Open-SSH 的连接逻辑
使用 Open-SSH 客户端连接我们写的服务器,可以发现第一次请求总是为 none
请求,
这是 AuthLogCallback
打印的记录:
由于 ServerConfig.NoClientAuth
为 false
,所以身份验证失败;
紧接着 server.go
会生成一个 failureMsg
,包含可用的认证方法,它是通过回调函数是否为 nil
来判断的:
由于我们的 config.PublickeyCallback
不为 nil
,故它将被添加到 failureMsg
的失败消息的方法列表中,并发送至客户端:
但 Open-SSH 客户端收到后就不再尝试了,会中断连接:
然后服务端也会因为网络断开产生 io.EOF
并返回:
这也就导致了无法使用 Open-SSH 客户端通过 publickey
的方式连接至 golang.org/x/crypto/ssh
库编写的 SSH 服务器。
对于 password 认证
Open-SSH 会
现在为服务器配置设置 PasswordCallback
,那么第一次的 none
请求会导致包含 password
与 publickey
方法的认证失败消息被返回:
但是回想我们平时使用 Open-SSH 连接服务器时,如果输错密码,会再次尝试,最多三次;此次并不向 publickey
认证那样直接断开连接,而是重新发送一个 password
方式认证消息:
其实这里可以推测
Open-SSH
客户端就是通过失败消息中包含的password
来尝试让用户重新输入密码的:
尝试输入密码 123456
,发现进入 password
方式认证的代码块,并且 payload
数据长度为 6
,之后的就是字符 1
、2
、...、6
的 ascii
码:
再插一嘴,我自己用 golang 写的客户端就可以成功调用服务器的 PublickKeyCallback
:
不会有人用 fmt.Println
来调试吧?。。。那就是我了😅😅😅
第一次验证方式使用的也是 none
:
第二次就是 publickey
验证了,并且附带了正确的 payload
: