| // Copyright 2011 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package ssh | |
| import ( | |
| "bytes" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "slices" | |
| "strings" | |
| ) | |
| type authResult int | |
| const ( | |
| authFailure authResult = iota | |
| authPartialSuccess | |
| authSuccess | |
| ) | |
| // maxAuthClientTried bounds the total number of authentication attempts | |
| // (failures and partial successes combined) the client makes before | |
| // aborting the loop, to prevent unbounded growth when an AuthCallback | |
| // keeps supplying methods. | |
| const maxAuthClientTried = 64 | |
| // clientAuthenticate authenticates with the remote server. See RFC 4252. | |
| func (c *connection) clientAuthenticate(config *ClientConfig) error { | |
| // initiate user auth session | |
| if err := c.transport.writePacket(Marshal(&serviceRequestMsg{serviceUserAuth})); err != nil { | |
| return err | |
| } | |
| packet, err := c.transport.readPacket() | |
| if err != nil { | |
| return err | |
| } | |
| // The server may choose to send a SSH_MSG_EXT_INFO at this point (if we | |
| // advertised willingness to receive one, which we always do) or not. See | |
| // RFC 8308, Section 2.4. | |
| extensions := make(map[string][]byte) | |
| if len(packet) > 0 && packet[0] == msgExtInfo { | |
| var extInfo extInfoMsg | |
| if err := Unmarshal(packet, &extInfo); err != nil { | |
| return err | |
| } | |
| payload := extInfo.Payload | |
| for i := uint32(0); i < extInfo.NumExtensions; i++ { | |
| name, rest, ok := parseString(payload) | |
| if !ok { | |
| return parseError(msgExtInfo) | |
| } | |
| value, rest, ok := parseString(rest) | |
| if !ok { | |
| return parseError(msgExtInfo) | |
| } | |
| extensions[string(name)] = value | |
| payload = rest | |
| } | |
| packet, err = c.transport.readPacket() | |
| if err != nil { | |
| return err | |
| } | |
| } | |
| var serviceAccept serviceAcceptMsg | |
| if err := Unmarshal(packet, &serviceAccept); err != nil { | |
| return err | |
| } | |
| // during the authentication phase the client first attempts the "none" method | |
| // then any untried methods suggested by the server. | |
| var tried []string | |
| var lastMethods []string | |
| var partialSuccess []string | |
| sessionID := c.transport.getSessionID() | |
| for auth := AuthMethod(new(noneAuth)); auth != nil; { | |
| ok, methods, err := auth.auth(sessionID, config.User, c.transport, config.Rand, extensions) | |
| if err != nil { | |
| // On disconnect, return error immediately | |
| if _, isDisconnect := err.(*disconnectMsg); isDisconnect { | |
| return err | |
| } | |
| // We return the error later if there is no other method | |
| // left to try. | |
| ok = authFailure | |
| } | |
| switch ok { | |
| case authSuccess: | |
| return nil | |
| case authPartialSuccess: | |
| partialSuccess = append(partialSuccess, auth.method()) | |
| case authFailure: | |
| tried = append(tried, auth.method()) | |
| } | |
| if len(partialSuccess)+len(tried) > maxAuthClientTried { | |
| return fmt.Errorf("ssh: too many authentication attempts (%d), aborting", | |
| len(partialSuccess)+len(tried)) | |
| } | |
| if methods == nil { | |
| methods = lastMethods | |
| } | |
| lastMethods = methods | |
| // If AuthCallback is set it takes precedence: it picks the next | |
| // AuthMethod dynamically. The returned method need not be in | |
| // config.Auth. If the callback returns (nil, nil) we fall back to | |
| // selecting the next untried method from config.Auth below; on | |
| // (nil, error) the handshake aborts. | |
| if config.AuthCallback != nil { | |
| ctx := &ClientAuthContext{ | |
| Metadata: c, | |
| Algorithms: c.Algorithms(), | |
| AllowedMethods: slices.Clone(methods), | |
| PartialSuccessMethods: slices.Clone(partialSuccess), | |
| TriedMethods: slices.Clone(tried), | |
| } | |
| altAuth, cbErr := config.AuthCallback(ctx) | |
| if cbErr != nil { | |
| return cbErr | |
| } | |
| if altAuth != nil { | |
| auth = altAuth | |
| continue | |
| } | |
| } | |
| auth = nil | |
| findNext: | |
| for _, a := range config.Auth { | |
| candidateMethod := a.method() | |
| if slices.Contains(tried, candidateMethod) { | |
| continue | |
| } | |
| for _, meth := range methods { | |
| if meth == candidateMethod { | |
| auth = a | |
| break findNext | |
| } | |
| } | |
| } | |
| if auth == nil && err != nil { | |
| // We have an error and there are no other authentication methods to | |
| // try, so we return it. | |
| return err | |
| } | |
| } | |
| return fmt.Errorf("ssh: unable to authenticate, attempted methods %v, no supported methods remain", tried) | |
| } | |
| // An AuthMethod represents an instance of an RFC 4252 authentication method. | |
| type AuthMethod interface { | |
| // auth authenticates user over transport t. | |
| // Returns true if authentication is successful. | |
| // If authentication is not successful, a []string of alternative | |
| // method names is returned. If the slice is nil, it will be ignored | |
| // and the previous set of possible methods will be reused. | |
| auth(session []byte, user string, p packetConn, rand io.Reader, extensions map[string][]byte) (authResult, []string, error) | |
| // method returns the RFC 4252 method name. | |
| method() string | |
| } | |
| // "none" authentication, RFC 4252 section 5.2. | |
| type noneAuth int | |
| func (n *noneAuth) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) { | |
| if err := c.writePacket(Marshal(&userAuthRequestMsg{ | |
| User: user, | |
| Service: serviceSSH, | |
| Method: "none", | |
| })); err != nil { | |
| return authFailure, nil, err | |
| } | |
| return handleAuthResponse(c) | |
| } | |
| func (n *noneAuth) method() string { | |
| return "none" | |
| } | |
| // passwordCallback is an AuthMethod that fetches the password through | |
| // a function call, e.g. by prompting the user. | |
| type passwordCallback func() (password string, err error) | |
| func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) { | |
| type passwordAuthMsg struct { | |
| User string `sshtype:"50"` | |
| Service string | |
| Method string | |
| Reply bool | |
| Password string | |
| } | |
| pw, err := cb() | |
| // REVIEW NOTE: is there a need to support skipping a password attempt? | |
| // The program may only find out that the user doesn't have a password | |
| // when prompting. | |
| if err != nil { | |
| return authFailure, nil, err | |
| } | |
| if err := c.writePacket(Marshal(&passwordAuthMsg{ | |
| User: user, | |
| Service: serviceSSH, | |
| Method: cb.method(), | |
| Reply: false, | |
| Password: pw, | |
| })); err != nil { | |
| return authFailure, nil, err | |
| } | |
| return handleAuthResponse(c) | |
| } | |
| func (cb passwordCallback) method() string { | |
| return "password" | |
| } | |
| // Password returns an AuthMethod using the given password. | |
| func Password(secret string) AuthMethod { | |
| return passwordCallback(func() (string, error) { return secret, nil }) | |
| } | |
| // PasswordCallback returns an AuthMethod that uses a callback for | |
| // fetching a password. | |
| func PasswordCallback(prompt func() (secret string, err error)) AuthMethod { | |
| return passwordCallback(prompt) | |
| } | |
| type publickeyAuthMsg struct { | |
| User string `sshtype:"50"` | |
| Service string | |
| Method string | |
| // HasSig indicates to the receiver packet that the auth request is signed and | |
| // should be used for authentication of the request. | |
| HasSig bool | |
| Algoname string | |
| PubKey []byte | |
| // Sig is tagged with "rest" so Marshal will exclude it during | |
| // validateKey | |
| Sig []byte `ssh:"rest"` | |
| } | |
| // publicKeyCallback is an AuthMethod that uses a set of key | |
| // pairs for authentication. | |
| type publicKeyCallback func() ([]Signer, error) | |
| func (cb publicKeyCallback) method() string { | |
| return "publickey" | |
| } | |
| func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiAlgorithmSigner, string, error) { | |
| var as MultiAlgorithmSigner | |
| keyFormat := signer.PublicKey().Type() | |
| // If the signer implements MultiAlgorithmSigner we use the algorithms it | |
| // support, if it implements AlgorithmSigner we assume it supports all | |
| // algorithms, otherwise only the key format one. | |
| switch s := signer.(type) { | |
| case MultiAlgorithmSigner: | |
| as = s | |
| case AlgorithmSigner: | |
| as = &multiAlgorithmSigner{ | |
| AlgorithmSigner: s, | |
| supportedAlgorithms: algorithmsForKeyFormat(underlyingAlgo(keyFormat)), | |
| } | |
| default: | |
| as = &multiAlgorithmSigner{ | |
| AlgorithmSigner: algorithmSignerWrapper{signer}, | |
| supportedAlgorithms: []string{underlyingAlgo(keyFormat)}, | |
| } | |
| } | |
| getFallbackAlgo := func() (string, error) { | |
| // Fallback to use if there is no "server-sig-algs" extension or a | |
| // common algorithm cannot be found. We use the public key format if the | |
| // MultiAlgorithmSigner supports it, otherwise we return an error. | |
| if !slices.Contains(as.Algorithms(), underlyingAlgo(keyFormat)) { | |
| return "", fmt.Errorf("ssh: no common public key signature algorithm, server only supports %q for key type %q, signer only supports %v", | |
| underlyingAlgo(keyFormat), keyFormat, as.Algorithms()) | |
| } | |
| return keyFormat, nil | |
| } | |
| extPayload, ok := extensions["server-sig-algs"] | |
| if !ok { | |
| // If there is no "server-sig-algs" extension use the fallback | |
| // algorithm. | |
| algo, err := getFallbackAlgo() | |
| return as, algo, err | |
| } | |
| // The server-sig-algs extension only carries underlying signature | |
| // algorithm, but we are trying to select a protocol-level public key | |
| // algorithm, which might be a certificate type. Extend the list of server | |
| // supported algorithms to include the corresponding certificate algorithms. | |
| serverAlgos := strings.Split(string(extPayload), ",") | |
| for _, algo := range serverAlgos { | |
| if certAlgo, ok := certificateAlgo(algo); ok { | |
| serverAlgos = append(serverAlgos, certAlgo) | |
| } | |
| } | |
| // Filter algorithms based on those supported by MultiAlgorithmSigner. | |
| // Iterate over the signer's algorithms first to preserve its preference order. | |
| supportedKeyAlgos := algorithmsForKeyFormat(keyFormat) | |
| var keyAlgos []string | |
| for _, signerAlgo := range as.Algorithms() { | |
| if idx := slices.IndexFunc(supportedKeyAlgos, func(algo string) bool { | |
| return underlyingAlgo(algo) == signerAlgo | |
| }); idx >= 0 { | |
| keyAlgos = append(keyAlgos, supportedKeyAlgos[idx]) | |
| } | |
| } | |
| algo, err := findCommon("public key signature algorithm", keyAlgos, serverAlgos, true) | |
| if err != nil { | |
| // If there is no overlap, return the fallback algorithm to support | |
| // servers that fail to list all supported algorithms. | |
| algo, err := getFallbackAlgo() | |
| return as, algo, err | |
| } | |
| return as, algo, nil | |
| } | |
| func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader, extensions map[string][]byte) (authResult, []string, error) { | |
| // Authentication is performed by sending an enquiry to test if a key is | |
| // acceptable to the remote. If the key is acceptable, the client will | |
| // attempt to authenticate with the valid key. If not the client will repeat | |
| // the process with the remaining keys. | |
| signers, err := cb() | |
| if err != nil { | |
| return authFailure, nil, err | |
| } | |
| var methods []string | |
| var errSigAlgo error | |
| origSignersLen := len(signers) | |
| for idx := 0; idx < len(signers); idx++ { | |
| signer := signers[idx] | |
| pub := signer.PublicKey() | |
| as, algo, err := pickSignatureAlgorithm(signer, extensions) | |
| if err != nil && errSigAlgo == nil { | |
| // If we cannot negotiate a signature algorithm store the first | |
| // error so we can return it to provide a more meaningful message if | |
| // no other signers work. | |
| errSigAlgo = err | |
| continue | |
| } | |
| ok, err := validateKey(pub, algo, user, c) | |
| if err != nil { | |
| return authFailure, nil, err | |
| } | |
| // OpenSSH 7.2-7.7 advertises support for rsa-sha2-256 and rsa-sha2-512 | |
| // in the "server-sig-algs" extension but doesn't support these | |
| // algorithms for certificate authentication, so if the server rejects | |
| // the key try to use the obtained algorithm as if "server-sig-algs" had | |
| // not been implemented if supported from the algorithm signer. | |
| if !ok && idx < origSignersLen && isRSACert(algo) && algo != CertAlgoRSAv01 { | |
| if slices.Contains(as.Algorithms(), KeyAlgoRSA) { | |
| // We retry using the compat algorithm after all signers have | |
| // been tried normally. | |
| signers = append(signers, &multiAlgorithmSigner{ | |
| AlgorithmSigner: as, | |
| supportedAlgorithms: []string{KeyAlgoRSA}, | |
| }) | |
| } | |
| } | |
| if !ok { | |
| continue | |
| } | |
| pubKey := pub.Marshal() | |
| data := buildDataSignedForAuth(session, userAuthRequestMsg{ | |
| User: user, | |
| Service: serviceSSH, | |
| Method: cb.method(), | |
| }, algo, pubKey) | |
| sign, err := as.SignWithAlgorithm(rand, data, underlyingAlgo(algo)) | |
| if err != nil { | |
| return authFailure, nil, err | |
| } | |
| // manually wrap the serialized signature in a string | |
| s := Marshal(sign) | |
| sig := make([]byte, stringLength(len(s))) | |
| marshalString(sig, s) | |
| msg := publickeyAuthMsg{ | |
| User: user, | |
| Service: serviceSSH, | |
| Method: cb.method(), | |
| HasSig: true, | |
| Algoname: algo, | |
| PubKey: pubKey, | |
| Sig: sig, | |
| } | |
| p := Marshal(&msg) | |
| if err := c.writePacket(p); err != nil { | |
| return authFailure, nil, err | |
| } | |
| var success authResult | |
| success, methods, err = handleAuthResponse(c) | |
| if err != nil { | |
| return authFailure, nil, err | |
| } | |
| // If authentication succeeds or partially succeeds, return immediately | |
| // so the caller can select the next auth method. According to RFC 4252 | |
| // Section 7, if the server no longer lists "publickey" among its | |
| // allowed methods, do not attempt to authenticate with any other keys. | |
| if success == authSuccess || success == authPartialSuccess || !slices.Contains(methods, cb.method()) { | |
| return success, methods, err | |
| } | |
| } | |
| return authFailure, methods, errSigAlgo | |
| } | |
| // validateKey validates the key provided is acceptable to the server. | |
| func validateKey(key PublicKey, algo string, user string, c packetConn) (bool, error) { | |
| pubKey := key.Marshal() | |
| msg := publickeyAuthMsg{ | |
| User: user, | |
| Service: serviceSSH, | |
| Method: "publickey", | |
| HasSig: false, | |
| Algoname: algo, | |
| PubKey: pubKey, | |
| } | |
| if err := c.writePacket(Marshal(&msg)); err != nil { | |
| return false, err | |
| } | |
| return confirmKeyAck(key, c) | |
| } | |
| func confirmKeyAck(key PublicKey, c packetConn) (bool, error) { | |
| pubKey := key.Marshal() | |
| for { | |
| packet, err := c.readPacket() | |
| if err != nil { | |
| return false, err | |
| } | |
| switch packet[0] { | |
| case msgUserAuthBanner: | |
| if err := handleBannerResponse(c, packet); err != nil { | |
| return false, err | |
| } | |
| case msgUserAuthPubKeyOk: | |
| var msg userAuthPubKeyOkMsg | |
| if err := Unmarshal(packet, &msg); err != nil { | |
| return false, err | |
| } | |
| // According to RFC 4252 Section 7 the algorithm in | |
| // SSH_MSG_USERAUTH_PK_OK should match that of the request but some | |
| // servers send the key type instead. OpenSSH allows any algorithm | |
| // that matches the public key, so we do the same. | |
| // https://github.com/openssh/openssh-portable/blob/86bdd385/sshconnect2.c#L709 | |
| if !slices.Contains(algorithmsForKeyFormat(key.Type()), msg.Algo) { | |
| return false, nil | |
| } | |
| if !bytes.Equal(msg.PubKey, pubKey) { | |
| return false, nil | |
| } | |
| return true, nil | |
| case msgUserAuthFailure: | |
| return false, nil | |
| default: | |
| return false, unexpectedMessageError(msgUserAuthPubKeyOk, packet[0]) | |
| } | |
| } | |
| } | |
| // PublicKeys returns an AuthMethod that uses the given key | |
| // pairs. | |
| func PublicKeys(signers ...Signer) AuthMethod { | |
| return publicKeyCallback(func() ([]Signer, error) { return signers, nil }) | |
| } | |
| // PublicKeysCallback returns an AuthMethod that runs the given | |
| // function to obtain a list of key pairs. | |
| func PublicKeysCallback(getSigners func() (signers []Signer, err error)) AuthMethod { | |
| return publicKeyCallback(getSigners) | |
| } | |
| // handleAuthResponse returns whether the preceding authentication request succeeded | |
| // along with a list of remaining authentication methods to try next and | |
| // an error if an unexpected response was received. | |
| func handleAuthResponse(c packetConn) (authResult, []string, error) { | |
| gotMsgExtInfo := false | |
| for { | |
| packet, err := c.readPacket() | |
| if err != nil { | |
| return authFailure, nil, err | |
| } | |
| switch packet[0] { | |
| case msgUserAuthBanner: | |
| if err := handleBannerResponse(c, packet); err != nil { | |
| return authFailure, nil, err | |
| } | |
| case msgExtInfo: | |
| // Ignore post-authentication RFC 8308 extensions, once. | |
| if gotMsgExtInfo { | |
| return authFailure, nil, unexpectedMessageError(msgUserAuthSuccess, packet[0]) | |
| } | |
| gotMsgExtInfo = true | |
| 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 | |
| case msgUserAuthSuccess: | |
| return authSuccess, nil, nil | |
| default: | |
| return authFailure, nil, unexpectedMessageError(msgUserAuthSuccess, packet[0]) | |
| } | |
| } | |
| } | |
| func handleBannerResponse(c packetConn, packet []byte) error { | |
| var msg userAuthBannerMsg | |
| if err := Unmarshal(packet, &msg); err != nil { | |
| return err | |
| } | |
| transport, ok := c.(*handshakeTransport) | |
| if !ok { | |
| return nil | |
| } | |
| if transport.bannerCallback != nil { | |
| return transport.bannerCallback(msg.Message) | |
| } | |
| return nil | |
| } | |
| // KeyboardInteractiveChallenge should print questions, optionally | |
| // disabling echoing (e.g. for passwords), and return all the answers. | |
| // Challenge may be called multiple times in a single session. After | |
| // successful authentication, the server may send a challenge with no | |
| // questions, for which the name and instruction messages should be | |
| // printed. RFC 4256 section 3.3 details how the UI should behave for | |
| // both CLI and GUI environments. | |
| type KeyboardInteractiveChallenge func(name, instruction string, questions []string, echos []bool) (answers []string, err error) | |
| // KeyboardInteractive returns an AuthMethod using a prompt/response | |
| // sequence controlled by the server. | |
| func KeyboardInteractive(challenge KeyboardInteractiveChallenge) AuthMethod { | |
| return challenge | |
| } | |
| func (cb KeyboardInteractiveChallenge) method() string { | |
| return "keyboard-interactive" | |
| } | |
| func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) { | |
| type initiateMsg struct { | |
| User string `sshtype:"50"` | |
| Service string | |
| Method string | |
| Language string | |
| Submethods string | |
| } | |
| if err := c.writePacket(Marshal(&initiateMsg{ | |
| User: user, | |
| Service: serviceSSH, | |
| Method: "keyboard-interactive", | |
| })); err != nil { | |
| return authFailure, nil, err | |
| } | |
| gotMsgExtInfo := false | |
| gotUserAuthInfoRequest := false | |
| for { | |
| packet, err := c.readPacket() | |
| if err != nil { | |
| return authFailure, nil, err | |
| } | |
| // like handleAuthResponse, but with less options. | |
| switch packet[0] { | |
| case msgUserAuthBanner: | |
| if err := handleBannerResponse(c, packet); err != nil { | |
| return authFailure, nil, err | |
| } | |
| continue | |
| case msgExtInfo: | |
| // Ignore post-authentication RFC 8308 extensions, once. | |
| if gotMsgExtInfo { | |
| return authFailure, nil, unexpectedMessageError(msgUserAuthInfoRequest, packet[0]) | |
| } | |
| gotMsgExtInfo = true | |
| continue | |
| case msgUserAuthInfoRequest: | |
| // OK | |
| case msgUserAuthFailure: | |
| var msg userAuthFailureMsg | |
| if err := Unmarshal(packet, &msg); err != nil { | |
| return authFailure, nil, err | |
| } | |
| if msg.PartialSuccess { | |
| return authPartialSuccess, msg.Methods, nil | |
| } | |
| if !gotUserAuthInfoRequest { | |
| return authFailure, msg.Methods, unexpectedMessageError(msgUserAuthInfoRequest, packet[0]) | |
| } | |
| return authFailure, msg.Methods, nil | |
| case msgUserAuthSuccess: | |
| return authSuccess, nil, nil | |
| default: | |
| return authFailure, nil, unexpectedMessageError(msgUserAuthInfoRequest, packet[0]) | |
| } | |
| var msg userAuthInfoRequestMsg | |
| if err := Unmarshal(packet, &msg); err != nil { | |
| return authFailure, nil, err | |
| } | |
| gotUserAuthInfoRequest = true | |
| // Manually unpack the prompt/echo pairs. | |
| rest := msg.Prompts | |
| var prompts []string | |
| var echos []bool | |
| for i := 0; i < int(msg.NumPrompts); i++ { | |
| prompt, r, ok := parseString(rest) | |
| if !ok || len(r) == 0 { | |
| return authFailure, nil, errors.New("ssh: prompt format error") | |
| } | |
| prompts = append(prompts, string(prompt)) | |
| echos = append(echos, r[0] != 0) | |
| rest = r[1:] | |
| } | |
| if len(rest) != 0 { | |
| return authFailure, nil, errors.New("ssh: extra data following keyboard-interactive pairs") | |
| } | |
| answers, err := cb(msg.Name, msg.Instruction, prompts, echos) | |
| if err != nil { | |
| return authFailure, nil, err | |
| } | |
| if len(answers) != len(prompts) { | |
| return authFailure, nil, fmt.Errorf("ssh: incorrect number of answers from keyboard-interactive callback %d (expected %d)", len(answers), len(prompts)) | |
| } | |
| responseLength := 1 + 4 | |
| for _, a := range answers { | |
| responseLength += stringLength(len(a)) | |
| } | |
| serialized := make([]byte, responseLength) | |
| p := serialized | |
| p[0] = msgUserAuthInfoResponse | |
| p = p[1:] | |
| p = marshalUint32(p, uint32(len(answers))) | |
| for _, a := range answers { | |
| p = marshalString(p, []byte(a)) | |
| } | |
| if err := c.writePacket(serialized); err != nil { | |
| return authFailure, nil, err | |
| } | |
| } | |
| } | |
| type retryableAuthMethod struct { | |
| authMethod AuthMethod | |
| maxTries int | |
| } | |
| func (r *retryableAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader, extensions map[string][]byte) (ok authResult, methods []string, err error) { | |
| for i := 0; r.maxTries <= 0 || i < r.maxTries; i++ { | |
| ok, methods, err = r.authMethod.auth(session, user, c, rand, extensions) | |
| if ok != authFailure || err != nil { // either success, partial success or error terminate | |
| return ok, methods, err | |
| } | |
| } | |
| return ok, methods, err | |
| } | |
| func (r *retryableAuthMethod) method() string { | |
| return r.authMethod.method() | |
| } | |
| // RetryableAuthMethod is a decorator for other auth methods enabling them to | |
| // be retried up to maxTries before considering that AuthMethod itself failed. | |
| // If maxTries is <= 0, will retry indefinitely | |
| // | |
| // This is useful for interactive clients using challenge/response type | |
| // authentication (e.g. Keyboard-Interactive, Password, etc) where the user | |
| // could mistype their response resulting in the server issuing a | |
| // SSH_MSG_USERAUTH_FAILURE (rfc4252 #8 [password] and rfc4256 #3.4 | |
| // [keyboard-interactive]); Without this decorator, the non-retryable | |
| // AuthMethod would be removed from future consideration, and never tried again | |
| // (and so the user would never be able to retry their entry). | |
| func RetryableAuthMethod(auth AuthMethod, maxTries int) AuthMethod { | |
| return &retryableAuthMethod{authMethod: auth, maxTries: maxTries} | |
| } | |
| // GSSAPIWithMICAuthMethod is an AuthMethod with "gssapi-with-mic" authentication. | |
| // See RFC 4462 section 3 | |
| // gssAPIClient is implementation of the GSSAPIClient interface, see the definition of the interface for details. | |
| // target is the server host you want to log in to. | |
| func GSSAPIWithMICAuthMethod(gssAPIClient GSSAPIClient, target string) AuthMethod { | |
| if gssAPIClient == nil { | |
| panic("gss-api client must be not nil with enable gssapi-with-mic") | |
| } | |
| return &gssAPIWithMICCallback{gssAPIClient: gssAPIClient, target: target} | |
| } | |
| type gssAPIWithMICCallback struct { | |
| gssAPIClient GSSAPIClient | |
| target string | |
| } | |
| func (g *gssAPIWithMICCallback) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) { | |
| m := &userAuthRequestMsg{ | |
| User: user, | |
| Service: serviceSSH, | |
| Method: g.method(), | |
| } | |
| // The GSS-API authentication method is initiated when the client sends an SSH_MSG_USERAUTH_REQUEST. | |
| // See RFC 4462 section 3.2. | |
| m.Payload = appendU32(m.Payload, 1) | |
| m.Payload = appendString(m.Payload, string(krb5OID)) | |
| if err := c.writePacket(Marshal(m)); err != nil { | |
| return authFailure, nil, err | |
| } | |
| // The server responds to the SSH_MSG_USERAUTH_REQUEST with either an | |
| // SSH_MSG_USERAUTH_FAILURE if none of the mechanisms are supported or | |
| // with an SSH_MSG_USERAUTH_GSSAPI_RESPONSE. | |
| // See RFC 4462 section 3.3. | |
| // OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication,so I don't want to check | |
| // selected mech if it is valid. | |
| packet, err := c.readPacket() | |
| if err != nil { | |
| return authFailure, nil, err | |
| } | |
| userAuthGSSAPIResp := &userAuthGSSAPIResponse{} | |
| if err := Unmarshal(packet, userAuthGSSAPIResp); err != nil { | |
| return authFailure, nil, err | |
| } | |
| // Start the loop into the exchange token. | |
| // See RFC 4462 section 3.4. | |
| var token []byte | |
| defer g.gssAPIClient.DeleteSecContext() | |
| for { | |
| // Initiates the establishment of a security context between the application and a remote peer. | |
| nextToken, needContinue, err := g.gssAPIClient.InitSecContext("host@"+g.target, token, false) | |
| if err != nil { | |
| return authFailure, nil, err | |
| } | |
| if len(nextToken) > 0 { | |
| if err := c.writePacket(Marshal(&userAuthGSSAPIToken{ | |
| Token: nextToken, | |
| })); err != nil { | |
| return authFailure, nil, err | |
| } | |
| } | |
| if !needContinue { | |
| break | |
| } | |
| 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 | |
| case msgUserAuthGSSAPIError: | |
| userAuthGSSAPIErrorResp := &userAuthGSSAPIError{} | |
| if err := Unmarshal(packet, userAuthGSSAPIErrorResp); err != nil { | |
| return authFailure, nil, err | |
| } | |
| return authFailure, nil, fmt.Errorf("GSS-API Error:\n"+ | |
| "Major Status: %d\n"+ | |
| "Minor Status: %d\n"+ | |
| "Error Message: %q\n", userAuthGSSAPIErrorResp.MajorStatus, userAuthGSSAPIErrorResp.MinorStatus, | |
| userAuthGSSAPIErrorResp.Message) | |
| case msgUserAuthGSSAPIToken: | |
| userAuthGSSAPITokenReq := &userAuthGSSAPIToken{} | |
| if err := Unmarshal(packet, userAuthGSSAPITokenReq); err != nil { | |
| return authFailure, nil, err | |
| } | |
| token = userAuthGSSAPITokenReq.Token | |
| } | |
| } | |
| // Binding Encryption Keys. | |
| // See RFC 4462 section 3.5. | |
| micField := buildMIC(string(session), user, "ssh-connection", "gssapi-with-mic") | |
| micToken, err := g.gssAPIClient.GetMIC(micField) | |
| if err != nil { | |
| return authFailure, nil, err | |
| } | |
| if err := c.writePacket(Marshal(&userAuthGSSAPIMIC{ | |
| MIC: micToken, | |
| })); err != nil { | |
| return authFailure, nil, err | |
| } | |
| return handleAuthResponse(c) | |
| } | |
| func (g *gssAPIWithMICCallback) method() string { | |
| return "gssapi-with-mic" | |
| } |