ssh: reject incomplete gssapi-with-mic configurations

Make the runtime gssapi-with-mic guard match the existing
configuration and method advertisement checks.

An incomplete GSSAPIWithMICConfig can be treated as unavailable when
building the advertised auth method list, while still remaining
reachable from the runtime auth dispatcher. Treat incomplete
configurations as not configured.

This change introduces a single internal completeness check for
GSSAPIWithMICConfig and uses it for the startup authentication
validation, the runtime gssapi-with-mic dispatch guard, and the
advertised authentication method list.

The change also adds a regression test. The test configures a server
with a normal PasswordCallback, a GSSAPIWithMICConfig with Server set,
and AllowLogin intentionally unset. It then uses a custom client auth
method that explicitly sends a USERAUTH_REQUEST with Method set to
gssapi-with-mic even though the server does not advertise that method,
and verifies that authentication fails cleanly with
"ssh: gssapi-with-mic auth not configured".

No golang/go issue reference is available yet.

Change-Id: I9a0c965d3a56192bd68309aa41e2c1f91952036c
GitHub-Last-Rev: 0267bda8e15e7c258ba3b92cd54f0941534c5fc9
GitHub-Pull-Request: golang/crypto#345
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/773460
Reviewed-by: Mark Freeman <markfreeman@google.com>
Reviewed-by: Nicola Murino <nicola.murino@gmail.com>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
povcfe
2026-06-02 00:25:39 -07:00
committed by Nicola Murino
parent 60e158ad30
commit e2ffffe738
2 changed files with 78 additions and 5 deletions
+68
View File
@@ -1654,6 +1654,74 @@ func TestAuthMethodGSSAPIWithMIC(t *testing.T) {
}
}
type maliciousGSSAPIWithMICAuthMethod struct{}
func (maliciousGSSAPIWithMICAuthMethod) method() string {
// Reuse an advertised auth method name so the client attempts this
// AuthMethod even when gssapi-with-mic is not advertised.
return "password"
}
func (maliciousGSSAPIWithMICAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) {
req := &userAuthRequestMsg{
User: user,
Service: serviceSSH,
Method: "gssapi-with-mic",
}
req.Payload = appendU32(req.Payload, 1)
req.Payload = appendString(req.Payload, string(krb5OID))
if err := c.writePacket(Marshal(req)); err != nil {
return authFailure, nil, err
}
packet, err := c.readPacket()
if err != nil {
return authFailure, nil, err
}
switch packet[0] {
case msgUserAuthFailure:
var msg userAuthFailureMsg
if err := Unmarshal(packet, &msg); err != nil {
return authFailure, nil, err
}
if msg.PartialSuccess {
return authPartialSuccess, msg.Methods, nil
}
return authFailure, msg.Methods, nil
default:
return authFailure, nil, unexpectedMessageError(msgUserAuthFailure, packet[0])
}
}
func TestGSSAPIWithMICPartialConfigRejectedAtRuntime(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
maliciousGSSAPIWithMICAuthMethod{},
},
HostKeyCallback: InsecureIgnoreHostKey(),
}
clientErr, serverErrs := tryAuthBothSides(t, config, &GSSAPIWithMICConfig{
Server: &FakeServer{},
})
if clientErr == nil || !strings.Contains(clientErr.Error(), "ssh: unable to authenticate") {
t.Fatalf("client got %v, want authentication failure", clientErr)
}
found := false
for _, err := range serverErrs {
if err != nil && strings.Contains(err.Error(), "ssh: gssapi-with-mic auth not configured") {
found = true
break
}
}
if !found {
t.Fatalf("server auth errors = %v, want gssapi-with-mic auth not configured", serverErrs)
}
}
func TestCompatibleAlgoAndSignatures(t *testing.T) {
type testcase struct {
algo string
+10 -5
View File
@@ -54,6 +54,9 @@ type Permissions struct {
ExtraData map[any]any
}
// GSSAPIWithMICConfig includes the server callbacks for gssapi-with-mic
// authentication. If either field is nil, gssapi-with-mic is considered not
// configured.
type GSSAPIWithMICConfig struct {
// AllowLogin, must be set, is called when gssapi-with-mic
// authentication is selected (RFC 4462 section 3). The srcName is from the
@@ -68,6 +71,10 @@ type GSSAPIWithMICConfig struct {
Server GSSAPIServer
}
func gssapiWithMICConfigured(config *GSSAPIWithMICConfig) bool {
return config != nil && config.AllowLogin != nil && config.Server != nil
}
// SendAuthBanner implements [ServerPreAuthConn].
func (s *connection) SendAuthBanner(msg string) error {
return s.transport.writePacket(Marshal(&userAuthBannerMsg{
@@ -382,8 +389,7 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error)
}
if !config.NoClientAuth && config.PasswordCallback == nil && config.PublicKeyCallback == nil &&
config.KeyboardInteractiveCallback == nil && (config.GSSAPIWithMICConfig == nil ||
config.GSSAPIWithMICConfig.AllowLogin == nil || config.GSSAPIWithMICConfig.Server == nil) {
config.KeyboardInteractiveCallback == nil && !gssapiWithMICConfigured(config.GSSAPIWithMICConfig) {
return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
}
@@ -869,7 +875,7 @@ userAuthLoop:
}
}
case "gssapi-with-mic":
if authConfig.GSSAPIWithMICConfig == nil {
if !gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) {
authErr = errors.New("ssh: gssapi-with-mic auth not configured")
break
}
@@ -1002,8 +1008,7 @@ userAuthLoop:
if authConfig.KeyboardInteractiveCallback != nil {
failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
}
if authConfig.GSSAPIWithMICConfig != nil && authConfig.GSSAPIWithMICConfig.Server != nil &&
authConfig.GSSAPIWithMICConfig.AllowLogin != nil {
if gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) {
failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic")
}