mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-11-27 15:36:25 +00:00
903 lines
30 KiB
Go
903 lines
30 KiB
Go
// 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.
|
|
// The Permissions value for a successful authentication attempt is
|
|
// available in ServerConn, so it can be used to pass information from
|
|
// the user-authentication phase to the application layer.
|
|
type Permissions struct {
|
|
// CriticalOptions indicate restrictions to the default
|
|
// permissions, and are typically used in conjunction with
|
|
// user certificates. The standard for SSH certificates
|
|
// defines "force-command" (only allow the given command to
|
|
// execute) and "source-address" (only allow connections from
|
|
// the given address). The SSH package currently only enforces
|
|
// the "source-address" critical option. It is up to server
|
|
// implementations to enforce other critical options, such as
|
|
// "force-command", by checking them after the SSH handshake
|
|
// is successful. In general, SSH servers should reject
|
|
// connections that specify critical options that are unknown
|
|
// or not supported.
|
|
CriticalOptions map[string]string
|
|
|
|
// Extensions are extra functionality that the server may
|
|
// offer on authenticated connections. Lack of support for an
|
|
// extension does not preclude authenticating a user. Common
|
|
// extensions are "permit-agent-forwarding",
|
|
// "permit-X11-forwarding". The Go SSH library currently does
|
|
// not act on any extension, and it is up to server
|
|
// implementations to honor them. Extensions can be used to
|
|
// pass data from the authentication callbacks to the server
|
|
// application layer.
|
|
Extensions map[string]string
|
|
}
|
|
|
|
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
|
|
// results of the GSS-API authentication. The format is username@DOMAIN.
|
|
// GSSAPI just guarantees to the server who the user is, but not if they can log in, and with what permissions.
|
|
// This callback is called after the user identity is established with GSSAPI to decide if the user can login with
|
|
// which permissions. If the user is allowed to login, it should return a nil error.
|
|
AllowLogin func(conn ConnMetadata, srcName string) (*Permissions, error)
|
|
|
|
// Server must be set. It's the implementation
|
|
// of the GSSAPIServer interface. See GSSAPIServer interface for details.
|
|
Server GSSAPIServer
|
|
}
|
|
|
|
// ServerConfig holds server specific configuration data.
|
|
type ServerConfig struct {
|
|
// Config contains configuration shared between client and server.
|
|
Config
|
|
|
|
// PublicKeyAuthAlgorithms specifies the supported client public key
|
|
// authentication algorithms. Note that this should not include certificate
|
|
// types since those use the underlying algorithm. This list is sent to the
|
|
// client if it supports the server-sig-algs extension. Order is irrelevant.
|
|
// If unspecified then a default set of algorithms is used.
|
|
PublicKeyAuthAlgorithms []string
|
|
|
|
hostKeys []Signer
|
|
|
|
// NoClientAuth is true if clients are allowed to connect without
|
|
// authenticating.
|
|
// To determine NoClientAuth at runtime, set NoClientAuth to true
|
|
// and the optional NoClientAuthCallback to a non-nil value.
|
|
NoClientAuth bool
|
|
|
|
// NoClientAuthCallback, if non-nil, is called when a user
|
|
// attempts to authenticate with auth method "none".
|
|
// NoClientAuth must also be set to true for this be used, or
|
|
// this func is unused.
|
|
NoClientAuthCallback func(ConnMetadata) (*Permissions, error)
|
|
|
|
// MaxAuthTries specifies the maximum number of authentication attempts
|
|
// permitted per connection. If set to a negative number, the number of
|
|
// attempts are unlimited. If set to zero, the number of attempts are limited
|
|
// to 6.
|
|
MaxAuthTries int
|
|
|
|
// 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
|
|
// offers a public key for authentication. It must return a nil error
|
|
// if the given public key can be used to authenticate the
|
|
// given user. For example, see CertChecker.Authenticate. A
|
|
// call to this function does not guarantee that the key
|
|
// offered is in fact used to authenticate. To record any data
|
|
// depending on the public key, store it inside a
|
|
// Permissions.Extensions entry.
|
|
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
|
|
|
|
// BannerCallback, if present, is called and the return string is sent to
|
|
// the client after key exchange completed but before authentication.
|
|
BannerCallback func(conn ConnMetadata) string
|
|
|
|
// GSSAPIWithMICConfig includes gssapi server and callback, which if both non-nil, is used
|
|
// when gssapi-with-mic authentication is selected (RFC 4462 section 3).
|
|
GSSAPIWithMICConfig *GSSAPIWithMICConfig
|
|
}
|
|
|
|
// AddHostKey adds a private key as a host key. If an existing host
|
|
// key exists with the same public key format, it is replaced. 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.
|
|
//
|
|
// The returned error may be of type *ServerAuthError for
|
|
// authentication errors.
|
|
func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewChannel, <-chan *Request, error) {
|
|
fullConf := *config
|
|
fullConf.SetDefaults()
|
|
if fullConf.MaxAuthTries == 0 {
|
|
fullConf.MaxAuthTries = 6
|
|
}
|
|
if len(fullConf.PublicKeyAuthAlgorithms) == 0 {
|
|
fullConf.PublicKeyAuthAlgorithms = supportedPubKeyAuthAlgos
|
|
} else {
|
|
for _, algo := range fullConf.PublicKeyAuthAlgorithms {
|
|
if !contains(supportedPubKeyAuthAlgos, algo) {
|
|
c.Close()
|
|
return nil, nil, nil, fmt.Errorf("ssh: unsupported public key authentication algorithm %s", algo)
|
|
}
|
|
}
|
|
}
|
|
// Check if the config contains any unsupported key exchanges
|
|
for _, kex := range fullConf.KeyExchanges {
|
|
if _, ok := serverForbiddenKexAlgos[kex]; ok {
|
|
c.Close()
|
|
return nil, nil, nil, fmt.Errorf("ssh: unsupported key exchange %s for server", kex)
|
|
}
|
|
}
|
|
|
|
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. algo is the negotiate
|
|
// algorithm and may be a certificate type.
|
|
func signAndMarshal(k AlgorithmSigner, rand io.Reader, data []byte, algo string) ([]byte, error) {
|
|
sig, err := k.SignWithAlgorithm(rand, data, underlyingAlgo(algo))
|
|
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 && (config.GSSAPIWithMICConfig == nil ||
|
|
config.GSSAPIWithMICConfig.AllowLogin == nil || config.GSSAPIWithMICConfig.Server == 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 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 gssExchangeToken(gssapiConfig *GSSAPIWithMICConfig, token []byte, s *connection,
|
|
sessionID []byte, userAuthReq userAuthRequestMsg) (authErr error, perms *Permissions, err error) {
|
|
gssAPIServer := gssapiConfig.Server
|
|
defer gssAPIServer.DeleteSecContext()
|
|
var srcName string
|
|
for {
|
|
var (
|
|
outToken []byte
|
|
needContinue bool
|
|
)
|
|
outToken, srcName, needContinue, err = gssAPIServer.AcceptSecContext(token)
|
|
if err != nil {
|
|
return err, nil, nil
|
|
}
|
|
if len(outToken) != 0 {
|
|
if err := s.transport.writePacket(Marshal(&userAuthGSSAPIToken{
|
|
Token: outToken,
|
|
})); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
if !needContinue {
|
|
break
|
|
}
|
|
packet, err := s.transport.readPacket()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
userAuthGSSAPITokenReq := &userAuthGSSAPIToken{}
|
|
if err := Unmarshal(packet, userAuthGSSAPITokenReq); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
token = userAuthGSSAPITokenReq.Token
|
|
}
|
|
packet, err := s.transport.readPacket()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
userAuthGSSAPIMICReq := &userAuthGSSAPIMIC{}
|
|
if err := Unmarshal(packet, userAuthGSSAPIMICReq); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
mic := buildMIC(string(sessionID), userAuthReq.User, userAuthReq.Service, userAuthReq.Method)
|
|
if err := gssAPIServer.VerifyMIC(mic, userAuthGSSAPIMICReq.MIC); err != nil {
|
|
return err, nil, nil
|
|
}
|
|
perms, authErr = gssapiConfig.AllowLogin(s, srcName)
|
|
return authErr, perms, nil
|
|
}
|
|
|
|
// isAlgoCompatible checks if the signature format is compatible with the
|
|
// selected algorithm taking into account edge cases that occur with old
|
|
// clients.
|
|
func isAlgoCompatible(algo, sigFormat string) bool {
|
|
// Compatibility for old clients.
|
|
//
|
|
// For certificate authentication with OpenSSH 7.2-7.7 signature format can
|
|
// be rsa-sha2-256 or rsa-sha2-512 for the algorithm
|
|
// ssh-rsa-cert-v01@openssh.com.
|
|
//
|
|
// With gpg-agent < 2.2.6 the algorithm can be rsa-sha2-256 or rsa-sha2-512
|
|
// for signature format ssh-rsa.
|
|
if isRSA(algo) && isRSA(sigFormat) {
|
|
return true
|
|
}
|
|
// Standard case: the underlying algorithm must match the signature format.
|
|
return underlyingAlgo(algo) == sigFormat
|
|
}
|
|
|
|
// ServerAuthError represents server authentication errors and is
|
|
// sometimes returned by NewServerConn. It appends any authentication
|
|
// errors that may occur, and is returned if all of the authentication
|
|
// methods provided by the user failed to authenticate.
|
|
type ServerAuthError struct {
|
|
// Errors contains authentication errors returned by the authentication
|
|
// callback methods. The first entry is typically ErrNoAuth.
|
|
Errors []error
|
|
}
|
|
|
|
func (l ServerAuthError) Error() string {
|
|
var errs []string
|
|
for _, err := range l.Errors {
|
|
errs = append(errs, err.Error())
|
|
}
|
|
return "[" + strings.Join(errs, ", ") + "]"
|
|
}
|
|
|
|
// ServerAuthCallbacks defines server-side authentication callbacks.
|
|
type ServerAuthCallbacks struct {
|
|
// PasswordCallback behaves like [ServerConfig.PasswordCallback].
|
|
PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error)
|
|
|
|
// PublicKeyCallback behaves like [ServerConfig.PublicKeyCallback].
|
|
PublicKeyCallback func(conn ConnMetadata, key PublicKey) (*Permissions, error)
|
|
|
|
// KeyboardInteractiveCallback behaves like [ServerConfig.KeyboardInteractiveCallback].
|
|
KeyboardInteractiveCallback func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error)
|
|
|
|
// GSSAPIWithMICConfig behaves like [ServerConfig.GSSAPIWithMICConfig].
|
|
GSSAPIWithMICConfig *GSSAPIWithMICConfig
|
|
}
|
|
|
|
// PartialSuccessError can be returned by any of the [ServerConfig]
|
|
// authentication callbacks to indicate to the client that authentication has
|
|
// partially succeeded, but further steps are required.
|
|
type PartialSuccessError struct {
|
|
// Next defines the authentication callbacks to apply to further steps. The
|
|
// available methods communicated to the client are based on the non-nil
|
|
// ServerAuthCallbacks fields.
|
|
Next ServerAuthCallbacks
|
|
}
|
|
|
|
func (p *PartialSuccessError) Error() string {
|
|
return "ssh: authenticated with partial success"
|
|
}
|
|
|
|
// ErrNoAuth is the error value returned if no
|
|
// authentication method has been passed yet. This happens as a normal
|
|
// part of the authentication loop, since the client first tries
|
|
// 'none' authentication to discover available methods.
|
|
// It is returned in ServerAuthError.Errors from NewServerConn.
|
|
var ErrNoAuth = errors.New("ssh: no auth passed yet")
|
|
|
|
// BannerError is an error that can be returned by authentication handlers in
|
|
// ServerConfig to send a banner message to the client.
|
|
type BannerError struct {
|
|
Err error
|
|
Message string
|
|
}
|
|
|
|
func (b *BannerError) Unwrap() error {
|
|
return b.Err
|
|
}
|
|
|
|
func (b *BannerError) Error() string {
|
|
if b.Err == nil {
|
|
return b.Message
|
|
}
|
|
return b.Err.Error()
|
|
}
|
|
|
|
func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) {
|
|
sessionID := s.transport.getSessionID()
|
|
var cache pubKeyCache
|
|
var perms *Permissions
|
|
|
|
authFailures := 0
|
|
noneAuthCount := 0
|
|
var authErrs []error
|
|
var displayedBanner bool
|
|
partialSuccessReturned := false
|
|
// Set the initial authentication callbacks from the config. They can be
|
|
// changed if a PartialSuccessError is returned.
|
|
authConfig := ServerAuthCallbacks{
|
|
PasswordCallback: config.PasswordCallback,
|
|
PublicKeyCallback: config.PublicKeyCallback,
|
|
KeyboardInteractiveCallback: config.KeyboardInteractiveCallback,
|
|
GSSAPIWithMICConfig: config.GSSAPIWithMICConfig,
|
|
}
|
|
|
|
userAuthLoop:
|
|
for {
|
|
if authFailures >= config.MaxAuthTries && config.MaxAuthTries > 0 {
|
|
discMsg := &disconnectMsg{
|
|
Reason: 2,
|
|
Message: "too many authentication failures",
|
|
}
|
|
|
|
if err := s.transport.writePacket(Marshal(discMsg)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, discMsg
|
|
}
|
|
|
|
var userAuthReq userAuthRequestMsg
|
|
if packet, err := s.transport.readPacket(); err != nil {
|
|
if err == io.EOF {
|
|
return nil, &ServerAuthError{Errors: authErrs}
|
|
}
|
|
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)
|
|
}
|
|
|
|
if s.user != userAuthReq.User && partialSuccessReturned {
|
|
return nil, fmt.Errorf("ssh: client changed the user after a partial success authentication, previous user %q, current user %q",
|
|
s.user, userAuthReq.User)
|
|
}
|
|
|
|
s.user = userAuthReq.User
|
|
|
|
if !displayedBanner && config.BannerCallback != nil {
|
|
displayedBanner = true
|
|
msg := config.BannerCallback(s)
|
|
if msg != "" {
|
|
bannerMsg := &userAuthBannerMsg{
|
|
Message: msg,
|
|
}
|
|
if err := s.transport.writePacket(Marshal(bannerMsg)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
perms = nil
|
|
authErr := ErrNoAuth
|
|
|
|
switch userAuthReq.Method {
|
|
case "none":
|
|
noneAuthCount++
|
|
// We don't allow none authentication after a partial success
|
|
// response.
|
|
if config.NoClientAuth && !partialSuccessReturned {
|
|
if config.NoClientAuthCallback != nil {
|
|
perms, authErr = config.NoClientAuthCallback(s)
|
|
} else {
|
|
authErr = nil
|
|
}
|
|
}
|
|
case "password":
|
|
if authConfig.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 = authConfig.PasswordCallback(s, password)
|
|
case "keyboard-interactive":
|
|
if authConfig.KeyboardInteractiveCallback == nil {
|
|
authErr = errors.New("ssh: keyboard-interactive auth not configured")
|
|
break
|
|
}
|
|
|
|
prompter := &sshClientKeyboardInteractive{s}
|
|
perms, authErr = authConfig.KeyboardInteractiveCallback(s, prompter.Challenge)
|
|
case "publickey":
|
|
if authConfig.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 !contains(config.PublicKeyAuthAlgorithms, underlyingAlgo(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 = authConfig.PublicKeyCallback(s, pubKey)
|
|
_, isPartialSuccessError := candidate.result.(*PartialSuccessError)
|
|
|
|
if (candidate.result == nil || isPartialSuccessError) &&
|
|
candidate.perms != nil &&
|
|
candidate.perms.CriticalOptions != nil &&
|
|
candidate.perms.CriticalOptions[sourceAddressCriticalOption] != "" {
|
|
if err := checkSourceAddress(
|
|
s.RemoteAddr(),
|
|
candidate.perms.CriticalOptions[sourceAddressCriticalOption]); err != nil {
|
|
candidate.result = err
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
_, isPartialSuccessError := candidate.result.(*PartialSuccessError)
|
|
if candidate.result == nil || isPartialSuccessError {
|
|
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 declared public key algo is compatible with the
|
|
// decoded one. This check will ensure we don't accept e.g.
|
|
// ssh-rsa-cert-v01@openssh.com algorithm with ssh-rsa public
|
|
// key type. The algorithm and public key type must be
|
|
// consistent: both must be certificate algorithms, or neither.
|
|
if !contains(algorithmsForKeyFormat(pubKey.Type()), algo) {
|
|
authErr = fmt.Errorf("ssh: public key type %q not compatible with selected algorithm %q",
|
|
pubKey.Type(), algo)
|
|
break
|
|
}
|
|
// 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 !contains(config.PublicKeyAuthAlgorithms, sig.Format) {
|
|
authErr = fmt.Errorf("ssh: algorithm %q not accepted", sig.Format)
|
|
break
|
|
}
|
|
if !isAlgoCompatible(algo, sig.Format) {
|
|
authErr = fmt.Errorf("ssh: signature %q not compatible with selected algorithm %q", sig.Format, algo)
|
|
break
|
|
}
|
|
|
|
signedData := buildDataSignedForAuth(sessionID, userAuthReq, algo, pubKeyData)
|
|
|
|
if err := pubKey.Verify(signedData, sig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
authErr = candidate.result
|
|
perms = candidate.perms
|
|
}
|
|
case "gssapi-with-mic":
|
|
if authConfig.GSSAPIWithMICConfig == nil {
|
|
authErr = errors.New("ssh: gssapi-with-mic auth not configured")
|
|
break
|
|
}
|
|
gssapiConfig := authConfig.GSSAPIWithMICConfig
|
|
userAuthRequestGSSAPI, err := parseGSSAPIPayload(userAuthReq.Payload)
|
|
if err != nil {
|
|
return nil, parseError(msgUserAuthRequest)
|
|
}
|
|
// OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication.
|
|
if userAuthRequestGSSAPI.N == 0 {
|
|
authErr = fmt.Errorf("ssh: Mechanism negotiation is not supported")
|
|
break
|
|
}
|
|
var i uint32
|
|
present := false
|
|
for i = 0; i < userAuthRequestGSSAPI.N; i++ {
|
|
if userAuthRequestGSSAPI.OIDS[i].Equal(krb5Mesh) {
|
|
present = true
|
|
break
|
|
}
|
|
}
|
|
if !present {
|
|
authErr = fmt.Errorf("ssh: GSSAPI authentication must use the Kerberos V5 mechanism")
|
|
break
|
|
}
|
|
// Initial server response, see RFC 4462 section 3.3.
|
|
if err := s.transport.writePacket(Marshal(&userAuthGSSAPIResponse{
|
|
SupportMech: krb5OID,
|
|
})); err != nil {
|
|
return nil, err
|
|
}
|
|
// Exchange token, see RFC 4462 section 3.4.
|
|
packet, err := s.transport.readPacket()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userAuthGSSAPITokenReq := &userAuthGSSAPIToken{}
|
|
if err := Unmarshal(packet, userAuthGSSAPITokenReq); err != nil {
|
|
return nil, err
|
|
}
|
|
authErr, perms, err = gssExchangeToken(gssapiConfig, userAuthGSSAPITokenReq.Token, s, sessionID,
|
|
userAuthReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
authErr = fmt.Errorf("ssh: unknown method %q", userAuthReq.Method)
|
|
}
|
|
|
|
authErrs = append(authErrs, authErr)
|
|
|
|
if config.AuthLogCallback != nil {
|
|
config.AuthLogCallback(s, userAuthReq.Method, authErr)
|
|
}
|
|
|
|
var bannerErr *BannerError
|
|
if errors.As(authErr, &bannerErr) {
|
|
if bannerErr.Message != "" {
|
|
bannerMsg := &userAuthBannerMsg{
|
|
Message: bannerErr.Message,
|
|
}
|
|
if err := s.transport.writePacket(Marshal(bannerMsg)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
if authErr == nil {
|
|
break userAuthLoop
|
|
}
|
|
|
|
var failureMsg userAuthFailureMsg
|
|
|
|
if partialSuccess, ok := authErr.(*PartialSuccessError); ok {
|
|
// After a partial success error we don't allow changing the user
|
|
// name and execute the NoClientAuthCallback.
|
|
partialSuccessReturned = true
|
|
|
|
// In case a partial success is returned, the server may send
|
|
// a new set of authentication methods.
|
|
authConfig = partialSuccess.Next
|
|
|
|
// Reset pubkey cache, as the new PublicKeyCallback might
|
|
// accept a different set of public keys.
|
|
cache = pubKeyCache{}
|
|
|
|
// Send back a partial success message to the user.
|
|
failureMsg.PartialSuccess = true
|
|
} else {
|
|
// Allow initial attempt of 'none' without penalty.
|
|
if authFailures > 0 || userAuthReq.Method != "none" || noneAuthCount != 1 {
|
|
authFailures++
|
|
}
|
|
if config.MaxAuthTries > 0 && authFailures >= config.MaxAuthTries {
|
|
// If we have hit the max attempts, don't bother sending the
|
|
// final SSH_MSG_USERAUTH_FAILURE message, since there are
|
|
// no more authentication methods which can be attempted,
|
|
// and this message may cause the client to re-attempt
|
|
// authentication while we send the disconnect message.
|
|
// Continue, and trigger the disconnect at the start of
|
|
// the loop.
|
|
//
|
|
// The SSH specification is somewhat confusing about this,
|
|
// RFC 4252 Section 5.1 requires each authentication failure
|
|
// be responded to with a respective SSH_MSG_USERAUTH_FAILURE
|
|
// message, but Section 4 says the server should disconnect
|
|
// after some number of attempts, but it isn't explicit which
|
|
// message should take precedence (i.e. should there be a failure
|
|
// message than a disconnect message, or if we are going to
|
|
// disconnect, should we only send that message.)
|
|
//
|
|
// Either way, OpenSSH disconnects immediately after the last
|
|
// failed authentication attempt, and given they are typically
|
|
// considered the golden implementation it seems reasonable
|
|
// to match that behavior.
|
|
continue
|
|
}
|
|
}
|
|
|
|
if authConfig.PasswordCallback != nil {
|
|
failureMsg.Methods = append(failureMsg.Methods, "password")
|
|
}
|
|
if authConfig.PublicKeyCallback != nil {
|
|
failureMsg.Methods = append(failureMsg.Methods, "publickey")
|
|
}
|
|
if authConfig.KeyboardInteractiveCallback != nil {
|
|
failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
|
|
}
|
|
if authConfig.GSSAPIWithMICConfig != nil && authConfig.GSSAPIWithMICConfig.Server != nil &&
|
|
authConfig.GSSAPIWithMICConfig.AllowLogin != nil {
|
|
failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic")
|
|
}
|
|
|
|
if len(failureMsg.Methods) == 0 {
|
|
return nil, errors.New("ssh: no authentication methods available")
|
|
}
|
|
|
|
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(name, 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{
|
|
Name: name,
|
|
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
|
|
}
|