| // 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" |
| "net" |
| "strings" |
| ) |
| |
| // The Permissions type holds fine-grained permissions that are |
| // specific to a user or a specific authentication method for a |
| // user. Permissions, except for "source-address", must be enforced in |
| // the server application layer, after successful authentication. The |
| // Permissions are passed on in ServerConn so a server implementation |
| // can honor them. |
| type Permissions struct { |
| // Critical options restrict default permissions. Common |
| // restrictions are "source-address" and "force-command". If |
| // the server cannot enforce the restriction, or does not |
| // recognize it, the user should not authenticate. |
| CriticalOptions map[string]string |
| |
| // Extensions are extra functionality that the server may |
| // offer on authenticated connections. Common extensions are |
| // "permit-agent-forwarding", "permit-X11-forwarding". Lack of |
| // support for an extension does not preclude authenticating a |
| // user. |
| Extensions map[string]string |
| } |
| |
| // ServerConfig holds server specific configuration data. |
| type ServerConfig struct { |
| // Config contains configuration shared between client and server. |
| Config |
| |
| hostKeys []Signer |
| |
| // NoClientAuth is true if clients are allowed to connect without |
| // authenticating. |
| NoClientAuth bool |
| |
| // PasswordCallback, if non-nil, is called when a user |
| // attempts to authenticate using a password. |
| PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error) |
| |
| // PublicKeyCallback, if non-nil, is called when a client attempts public |
| // key authentication. It must return true if the given public key is |
| // valid for the given user. For example, see CertChecker.Authenticate. |
| PublicKeyCallback func(conn ConnMetadata, key PublicKey) (*Permissions, error) |
| |
| // KeyboardInteractiveCallback, if non-nil, is called when |
| // keyboard-interactive authentication is selected (RFC |
| // 4256). The client object's Challenge function should be |
| // used to query the user. The callback may offer multiple |
| // Challenge rounds. To avoid information leaks, the client |
| // should be presented a challenge even if the user is |
| // unknown. |
| KeyboardInteractiveCallback func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error) |
| |
| // AuthLogCallback, if non-nil, is called to log all authentication |
| // attempts. |
| AuthLogCallback func(conn ConnMetadata, method string, err error) |
| |
| // ServerVersion is the version identification string to announce in |
| // the public handshake. |
| // If empty, a reasonable default is used. |
| // Note that RFC 4253 section 4.2 requires that this string start with |
| // "SSH-2.0-". |
| ServerVersion string |
| } |
| |
| // AddHostKey adds a private key as a host key. If an existing host |
| // key exists with the same algorithm, it is overwritten. Each server |
| // config must have at least one host key. |
| func (s *ServerConfig) AddHostKey(key Signer) { |
| for i, k := range s.hostKeys { |
| if k.PublicKey().Type() == key.PublicKey().Type() { |
| s.hostKeys[i] = key |
| return |
| } |
| } |
| |
| s.hostKeys = append(s.hostKeys, key) |
| } |
| |
| // cachedPubKey contains the results of querying whether a public key is |
| // acceptable for a user. |
| type cachedPubKey struct { |
| user string |
| pubKeyData []byte |
| result error |
| perms *Permissions |
| } |
| |
| const maxCachedPubKeys = 16 |
| |
| // pubKeyCache caches tests for public keys. Since SSH clients |
| // will query whether a public key is acceptable before attempting to |
| // authenticate with it, we end up with duplicate queries for public |
| // key validity. The cache only applies to a single ServerConn. |
| type pubKeyCache struct { |
| keys []cachedPubKey |
| } |
| |
| // get returns the result for a given user/algo/key tuple. |
| func (c *pubKeyCache) get(user string, pubKeyData []byte) (cachedPubKey, bool) { |
| for _, k := range c.keys { |
| if k.user == user && bytes.Equal(k.pubKeyData, pubKeyData) { |
| return k, true |
| } |
| } |
| return cachedPubKey{}, false |
| } |
| |
| // add adds the given tuple to the cache. |
| func (c *pubKeyCache) add(candidate cachedPubKey) { |
| if len(c.keys) < maxCachedPubKeys { |
| c.keys = append(c.keys, candidate) |
| } |
| } |
| |
| // ServerConn is an authenticated SSH connection, as seen from the |
| // server |
| type ServerConn struct { |
| Conn |
| |
| // If the succeeding authentication callback returned a |
| // non-nil Permissions pointer, it is stored here. |
| Permissions *Permissions |
| } |
| |
| // NewServerConn starts a new SSH server with c as the underlying |
| // transport. It starts with a handshake and, if the handshake is |
| // unsuccessful, it closes the connection and returns an error. The |
| // Request and NewChannel channels must be serviced, or the connection |
| // will hang. |
| func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewChannel, <-chan *Request, error) { |
| fullConf := *config |
| fullConf.SetDefaults() |
| s := &connection{ |
| sshConn: sshConn{conn: c}, |
| } |
| perms, err := s.serverHandshake(&fullConf) |
| if err != nil { |
| c.Close() |
| return nil, nil, nil, err |
| } |
| return &ServerConn{s, perms}, s.mux.incomingChannels, s.mux.incomingRequests, nil |
| } |
| |
| // signAndMarshal signs the data with the appropriate algorithm, |
| // and serializes the result in SSH wire format. |
| func signAndMarshal(k Signer, rand io.Reader, data []byte) ([]byte, error) { |
| sig, err := k.Sign(rand, data) |
| if err != nil { |
| return nil, err |
| } |
| |
| return Marshal(sig), nil |
| } |
| |
| // handshake performs key exchange and user authentication. |
| func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error) { |
| if len(config.hostKeys) == 0 { |
| return nil, errors.New("ssh: server has no host keys") |
| } |
| |
| if !config.NoClientAuth && config.PasswordCallback == nil && config.PublicKeyCallback == nil && config.KeyboardInteractiveCallback == nil { |
| return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false") |
| } |
| |
| if config.ServerVersion != "" { |
| s.serverVersion = []byte(config.ServerVersion) |
| } else { |
| s.serverVersion = []byte(packageVersion) |
| } |
| var err error |
| s.clientVersion, err = exchangeVersions(s.sshConn.conn, s.serverVersion) |
| if err != nil { |
| return nil, err |
| } |
| |
| tr := newTransport(s.sshConn.conn, config.Rand, false /* not client */) |
| s.transport = newServerTransport(tr, s.clientVersion, s.serverVersion, config) |
| |
| if err := s.transport.waitSession(); err != nil { |
| return nil, err |
| } |
| |
| // We just did the key change, so the session ID is established. |
| s.sessionID = s.transport.getSessionID() |
| |
| var packet []byte |
| if packet, err = s.transport.readPacket(); err != nil { |
| return nil, err |
| } |
| |
| var serviceRequest serviceRequestMsg |
| if err = Unmarshal(packet, &serviceRequest); err != nil { |
| return nil, err |
| } |
| if serviceRequest.Service != serviceUserAuth { |
| return nil, errors.New("ssh: requested service '" + serviceRequest.Service + "' before authenticating") |
| } |
| serviceAccept := serviceAcceptMsg{ |
| Service: serviceUserAuth, |
| } |
| if err := s.transport.writePacket(Marshal(&serviceAccept)); err != nil { |
| return nil, err |
| } |
| |
| perms, err := s.serverAuthenticate(config) |
| if err != nil { |
| return nil, err |
| } |
| s.mux = newMux(s.transport) |
| return perms, err |
| } |
| |
| func isAcceptableAlgo(algo string) bool { |
| switch algo { |
| case KeyAlgoRSA, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, KeyAlgoED25519, |
| CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01: |
| return true |
| } |
| return false |
| } |
| |
| func checkSourceAddress(addr net.Addr, sourceAddrs string) error { |
| if addr == nil { |
| return errors.New("ssh: no address known for client, but source-address match required") |
| } |
| |
| tcpAddr, ok := addr.(*net.TCPAddr) |
| if !ok { |
| return fmt.Errorf("ssh: remote address %v is not an TCP address when checking source-address match", addr) |
| } |
| |
| for _, sourceAddr := range strings.Split(sourceAddrs, ",") { |
| if allowedIP := net.ParseIP(sourceAddr); allowedIP != nil { |
| if allowedIP.Equal(tcpAddr.IP) { |
| return nil |
| } |
| } else { |
| _, ipNet, err := net.ParseCIDR(sourceAddr) |
| if err != nil { |
| return fmt.Errorf("ssh: error parsing source-address restriction %q: %v", sourceAddr, err) |
| } |
| |
| if ipNet.Contains(tcpAddr.IP) { |
| return nil |
| } |
| } |
| } |
| |
| return fmt.Errorf("ssh: remote address %v is not allowed because of source-address restriction", addr) |
| } |
| |
| func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) { |
| sessionID := s.transport.getSessionID() |
| var cache pubKeyCache |
| var perms *Permissions |
| |
| userAuthLoop: |
| for { |
| var userAuthReq userAuthRequestMsg |
| if packet, err := s.transport.readPacket(); err != nil { |
| return nil, err |
| } else if err = Unmarshal(packet, &userAuthReq); err != nil { |
| return nil, err |
| } |
| |
| if userAuthReq.Service != serviceSSH { |
| return nil, errors.New("ssh: client attempted to negotiate for unknown service: " + userAuthReq.Service) |
| } |
| |
| s.user = userAuthReq.User |
| perms = nil |
| authErr := errors.New("no auth passed yet") |
| |
| switch userAuthReq.Method { |
| case "none": |
| if config.NoClientAuth { |
| authErr = nil |
| } |
| case "password": |
| if config.PasswordCallback == nil { |
| authErr = errors.New("ssh: password auth not configured") |
| break |
| } |
| payload := userAuthReq.Payload |
| if len(payload) < 1 || payload[0] != 0 { |
| return nil, parseError(msgUserAuthRequest) |
| } |
| payload = payload[1:] |
| password, payload, ok := parseString(payload) |
| if !ok || len(payload) > 0 { |
| return nil, parseError(msgUserAuthRequest) |
| } |
| |
| perms, authErr = config.PasswordCallback(s, password) |
| case "keyboard-interactive": |
| if config.KeyboardInteractiveCallback == nil { |
| authErr = errors.New("ssh: keyboard-interactive auth not configubred") |
| break |
| } |
| |
| prompter := &sshClientKeyboardInteractive{s} |
| perms, authErr = config.KeyboardInteractiveCallback(s, prompter.Challenge) |
| case "publickey": |
| if config.PublicKeyCallback == nil { |
| authErr = errors.New("ssh: publickey auth not configured") |
| break |
| } |
| payload := userAuthReq.Payload |
| if len(payload) < 1 { |
| return nil, parseError(msgUserAuthRequest) |
| } |
| isQuery := payload[0] == 0 |
| payload = payload[1:] |
| algoBytes, payload, ok := parseString(payload) |
| if !ok { |
| return nil, parseError(msgUserAuthRequest) |
| } |
| algo := string(algoBytes) |
| if !isAcceptableAlgo(algo) { |
| authErr = fmt.Errorf("ssh: algorithm %q not accepted", algo) |
| break |
| } |
| |
| pubKeyData, payload, ok := parseString(payload) |
| if !ok { |
| return nil, parseError(msgUserAuthRequest) |
| } |
| |
| pubKey, err := ParsePublicKey(pubKeyData) |
| if err != nil { |
| return nil, err |
| } |
| |
| candidate, ok := cache.get(s.user, pubKeyData) |
| if !ok { |
| candidate.user = s.user |
| candidate.pubKeyData = pubKeyData |
| candidate.perms, candidate.result = config.PublicKeyCallback(s, pubKey) |
| if candidate.result == nil && candidate.perms != nil && candidate.perms.CriticalOptions != nil && candidate.perms.CriticalOptions[sourceAddressCriticalOption] != "" { |
| candidate.result = checkSourceAddress( |
| s.RemoteAddr(), |
| candidate.perms.CriticalOptions[sourceAddressCriticalOption]) |
| } |
| cache.add(candidate) |
| } |
| |
| if isQuery { |
| // The client can query if the given public key |
| // would be okay. |
| if len(payload) > 0 { |
| return nil, parseError(msgUserAuthRequest) |
| } |
| |
| if candidate.result == nil { |
| okMsg := userAuthPubKeyOkMsg{ |
| Algo: algo, |
| PubKey: pubKeyData, |
| } |
| if err = s.transport.writePacket(Marshal(&okMsg)); err != nil { |
| return nil, err |
| } |
| continue userAuthLoop |
| } |
| authErr = candidate.result |
| } else { |
| sig, payload, ok := parseSignature(payload) |
| if !ok || len(payload) > 0 { |
| return nil, parseError(msgUserAuthRequest) |
| } |
| // Ensure the public key algo and signature algo |
| // are supported. Compare the private key |
| // algorithm name that corresponds to algo with |
| // sig.Format. This is usually the same, but |
| // for certs, the names differ. |
| if !isAcceptableAlgo(sig.Format) { |
| break |
| } |
| signedData := buildDataSignedForAuth(sessionID, userAuthReq, algoBytes, pubKeyData) |
| |
| if err := pubKey.Verify(signedData, sig); err != nil { |
| return nil, err |
| } |
| |
| authErr = candidate.result |
| perms = candidate.perms |
| } |
| default: |
| authErr = fmt.Errorf("ssh: unknown method %q", userAuthReq.Method) |
| } |
| |
| if config.AuthLogCallback != nil { |
| config.AuthLogCallback(s, userAuthReq.Method, authErr) |
| } |
| |
| if authErr == nil { |
| break userAuthLoop |
| } |
| |
| var failureMsg userAuthFailureMsg |
| if config.PasswordCallback != nil { |
| failureMsg.Methods = append(failureMsg.Methods, "password") |
| } |
| if config.PublicKeyCallback != nil { |
| failureMsg.Methods = append(failureMsg.Methods, "publickey") |
| } |
| if config.KeyboardInteractiveCallback != nil { |
| failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive") |
| } |
| |
| if len(failureMsg.Methods) == 0 { |
| return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false") |
| } |
| |
| if err := s.transport.writePacket(Marshal(&failureMsg)); err != nil { |
| return nil, err |
| } |
| } |
| |
| if err := s.transport.writePacket([]byte{msgUserAuthSuccess}); err != nil { |
| return nil, err |
| } |
| return perms, nil |
| } |
| |
| // sshClientKeyboardInteractive implements a ClientKeyboardInteractive by |
| // asking the client on the other side of a ServerConn. |
| type sshClientKeyboardInteractive struct { |
| *connection |
| } |
| |
| func (c *sshClientKeyboardInteractive) Challenge(user, instruction string, questions []string, echos []bool) (answers []string, err error) { |
| if len(questions) != len(echos) { |
| return nil, errors.New("ssh: echos and questions must have equal length") |
| } |
| |
| var prompts []byte |
| for i := range questions { |
| prompts = appendString(prompts, questions[i]) |
| prompts = appendBool(prompts, echos[i]) |
| } |
| |
| if err := c.transport.writePacket(Marshal(&userAuthInfoRequestMsg{ |
| Instruction: instruction, |
| NumPrompts: uint32(len(questions)), |
| Prompts: prompts, |
| })); err != nil { |
| return nil, err |
| } |
| |
| packet, err := c.transport.readPacket() |
| if err != nil { |
| return nil, err |
| } |
| if packet[0] != msgUserAuthInfoResponse { |
| return nil, unexpectedMessageError(msgUserAuthInfoResponse, packet[0]) |
| } |
| packet = packet[1:] |
| |
| n, packet, ok := parseUint32(packet) |
| if !ok || int(n) != len(questions) { |
| return nil, parseError(msgUserAuthInfoResponse) |
| } |
| |
| for i := uint32(0); i < n; i++ { |
| ans, rest, ok := parseString(packet) |
| if !ok { |
| return nil, parseError(msgUserAuthInfoResponse) |
| } |
| |
| answers = append(answers, string(ans)) |
| packet = rest |
| } |
| if len(packet) != 0 { |
| return nil, errors.New("ssh: junk at end of message") |
| } |
| |
| return answers, nil |
| } |