sshpiper/ssh/sshpiper.go
2018-12-29 06:12:51 +00:00

574 lines
13 KiB
Go

// Copyright 2014, 2015 tgic<farmer1992@gmail.com>. All rights reserved.
// this file is governed by MIT-license
//
// https://github.com/tg123/sshpiper
package ssh
import (
"errors"
"fmt"
"net"
)
// SSHPiperConfig holds SSHPiper specific configuration data.
type SSHPiperConfig struct {
Config
hostKeys []Signer
// AdditionalChallenge, if non-nil, is called before calling FindUpstream.
// This allows you do a KeyboardInteractiveChallenge before connecting to upstream.
// It must return true if downstream passed the challenge, otherwise,
// the piped connection will be closed.
AdditionalChallenge func(conn ConnMetadata, client KeyboardInteractiveChallenge) (bool, error)
// FindUpstream, must not be nil, is called when SSHPiper decided to establish a
// ssh connection to upstream server. a connection, net.Conn, to upstream
// should be returned.
// If any error occurs, the piped connection will be closed.
FindUpstream func(conn ConnMetadata) (net.Conn, error)
// MapPublicKey, if non-nil, is called when downstream requests a publickey auth.
// SSHPiper will sign the auth packet message using the returned Signer.
// This func might be called twice, one is for query message, the other
// is real auth packet message.
// If any error occurs during this period, a NoneAuth packet will be sent to
// upstream ssh server instead.
//
// More info: https://github.com/tg123/sshpiper#publickey-sign-again
MapPublicKey func(conn ConnMetadata, key PublicKey) (Signer, error)
}
type upstream struct{ *connection }
type downstream struct{ *connection }
type pipedConn struct {
upstream *upstream
downstream *downstream
processAuthMsg func(msg *userAuthRequestMsg) (*userAuthRequestMsg, error)
}
// SSHPiperConn is a piped SSH connection, linking upstream ssh server and
// downstream ssh client together. After the piped connection was created,
// The downstream ssh client is authenticated by upstream ssh server and
// AdditionalChallenge from SSHPiper.
type SSHPiperConn struct {
*pipedConn
}
// Wait blocks until the piped connection has shut down, and returns the
// error causing the shutdown.
func (p *SSHPiperConn) Wait() error {
return p.pipedConn.loop()
}
// Close the piped connection create by SSHPiper
func (p *SSHPiperConn) Close() {
p.pipedConn.Close()
}
// AddHostKey adds a private key as a SSHPiper host key. If an existing host
// key exists with the same algorithm, it is overwritten. Each SSHPiper
// config must have at least one host key.
func (s *SSHPiperConfig) 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)
}
// NewSSHPiperConn starts a piped ssh connection witch conn as its downstream transport.
// It handshake with downstream ssh client and upstream ssh server provicde by FindUpstream.
// If either handshake is unsuccessful, the whole piped connection will be closed.
func NewSSHPiperConn(conn net.Conn, piper *SSHPiperConfig) (pipe *SSHPiperConn, err error) {
if piper.FindUpstream == nil {
panic("FindUpstream func not found")
}
d, err := newDownstream(conn, &ServerConfig{
Config: piper.Config,
hostKeys: piper.hostKeys,
})
if err != nil {
return nil, err
}
defer func() {
if pipe == nil {
d.Close()
}
}()
userAuthReq, err := d.nextAuthMsg()
if err != nil {
return nil, err
}
d.user = userAuthReq.User
// need additional challenge
if piper.AdditionalChallenge != nil {
for {
err := d.transport.writePacket(Marshal(&userAuthFailureMsg{
Methods: []string{"keyboard-interactive"},
}))
if err != nil {
return nil, err
}
userAuthReq, err := d.nextAuthMsg()
if err != nil {
return nil, err
}
if userAuthReq.Method == "keyboard-interactive" {
break
}
}
prompter := &sshClientKeyboardInteractive{d.connection}
ok, err := piper.AdditionalChallenge(d, prompter.Challenge)
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("additional challenge failed")
}
}
upconn, err := piper.FindUpstream(d)
if err != nil {
return nil, err
}
addr := upconn.RemoteAddr().String()
u, err := newUpstream(upconn, addr, &ClientConfig{})
if err != nil {
return nil, err
}
defer func() {
if pipe == nil {
u.Close()
}
}()
p := &pipedConn{
upstream: u,
downstream: d,
}
p.processAuthMsg = func(msg *userAuthRequestMsg) (*userAuthRequestMsg, error) {
// only public msg need
if msg.Method != "publickey" || piper.MapPublicKey == nil {
return msg, nil
}
user := msg.User
// pubKey MAP
downKey, isQuery, sig, err := parsePublicKeyMsg(msg)
if err != nil {
return nil, err
}
signer, err := piper.MapPublicKey(d, downKey)
// no mapped user change it to none or error occur
if err != nil || signer == nil {
return noneAuthMsg(user), nil
}
upKey := signer.PublicKey()
if isQuery {
// reply for query msg
msg, err = p.validAndAck(upKey, downKey)
} else {
ok, err := p.checkPublicKey(msg, downKey, sig)
if err != nil {
return nil, err
}
if !ok {
return noneAuthMsg(user), nil
}
msg, err = p.signAgain(msg, signer, downKey)
}
if err != nil {
return nil, err
}
return msg, nil
}
err = p.pipeAuth(userAuthReq)
if err != nil {
return nil, err
}
return &SSHPiperConn{p}, nil
}
func (pipe *pipedConn) validAndAck(upKey, downKey PublicKey) (*userAuthRequestMsg, error) {
user := pipe.downstream.User()
ok, err := validateKey(upKey, user, pipe.upstream.transport)
if ok {
okMsg := userAuthPubKeyOkMsg{
Algo: downKey.Type(),
PubKey: downKey.Marshal(),
}
if err = pipe.downstream.transport.writePacket(Marshal(&okMsg)); err != nil {
return nil, err
}
return nil, nil
}
return noneAuthMsg(user), nil
}
func (pipe *pipedConn) checkPublicKey(msg *userAuthRequestMsg, pubkey PublicKey, sig *Signature) (bool, error) {
if !isAcceptableAlgo(sig.Format) {
return false, nil
}
signedData := buildDataSignedForAuth(pipe.downstream.transport.getSessionID(), *msg, []byte(pubkey.Type()), pubkey.Marshal())
if err := pubkey.Verify(signedData, sig); err != nil {
return false, nil
}
return true, nil
}
func (pipe *pipedConn) signAgain(msg *userAuthRequestMsg, signer Signer, downKey PublicKey) (*userAuthRequestMsg, error) {
user := pipe.downstream.User()
rand := pipe.upstream.transport.config.Rand
session := pipe.upstream.transport.getSessionID()
upKey := signer.PublicKey()
upKeyData := upKey.Marshal()
sign, err := signer.Sign(rand, buildDataSignedForAuth(session, userAuthRequestMsg{
User: user,
Service: serviceSSH,
Method: "publickey",
}, []byte(upKey.Type()), upKeyData))
if err != nil {
return nil, err
}
// manually wrap the serialized signature in a string
s := Marshal(sign)
sig := make([]byte, stringLength(len(s)))
marshalString(sig, s)
pubkeyMsg := &publickeyAuthMsg{
User: user,
Service: serviceSSH,
Method: "publickey",
HasSig: true,
Algoname: upKey.Type(),
PubKey: upKeyData,
Sig: sig,
}
Unmarshal(Marshal(pubkeyMsg), msg)
return msg, nil
}
func parsePublicKeyMsg(userAuthReq *userAuthRequestMsg) (PublicKey, bool, *Signature, error) {
if userAuthReq.Method != "publickey" {
return nil, false, nil, fmt.Errorf("not a publickey auth msg")
}
payload := userAuthReq.Payload
if len(payload) < 1 {
return nil, false, nil, parseError(msgUserAuthRequest)
}
isQuery := payload[0] == 0
payload = payload[1:]
algoBytes, payload, ok := parseString(payload)
if !ok {
return nil, false, nil, parseError(msgUserAuthRequest)
}
algo := string(algoBytes)
if !isAcceptableAlgo(algo) {
return nil, false, nil, fmt.Errorf("ssh: algorithm %q not accepted", algo)
}
pubKeyData, payload, ok := parseString(payload)
if !ok {
return nil, false, nil, parseError(msgUserAuthRequest)
}
pubKey, err := ParsePublicKey(pubKeyData)
if err != nil {
return nil, false, nil, err
}
var sig *Signature
if !isQuery {
sig, payload, ok = parseSignature(payload)
if !ok || len(payload) > 0 {
return nil, false, nil, parseError(msgUserAuthRequest)
}
}
return pubKey, isQuery, sig, nil
}
func piping(dst, src packetConn) error {
for {
p, err := src.readPacket()
if err != nil {
return err
}
err = dst.writePacket(p)
if err != nil {
return err
}
}
}
func (pipe *pipedConn) loop() error {
c := make(chan error)
go func() {
c <- piping(pipe.upstream.transport, pipe.downstream.transport)
}()
go func() {
c <- piping(pipe.downstream.transport, pipe.upstream.transport)
}()
defer pipe.Close()
// wait until either connection closed
return <-c
}
func (pipe *pipedConn) Close() {
pipe.upstream.transport.Close()
pipe.downstream.transport.Close()
}
func (pipe *pipedConn) pipeAuth(initUserAuthMsg *userAuthRequestMsg) error {
err := pipe.upstream.sendAuthReq()
if err != nil {
return err
}
userAuthMsg := initUserAuthMsg
for {
// hook msg
userAuthMsg, err = pipe.processAuthMsg(userAuthMsg)
if err != nil {
return err
}
// nil for ignore
if userAuthMsg != nil {
err = pipe.upstream.transport.writePacket(Marshal(userAuthMsg))
if err != nil {
return err
}
packet, err := pipe.upstream.transport.readPacket()
if err != nil {
return err
}
success := packet[0] == msgUserAuthSuccess
if err = pipe.downstream.transport.writePacket(packet); err != nil {
return err
}
if success {
return nil
}
}
userAuthMsg, err = pipe.downstream.nextAuthMsg()
if err != nil {
return err
}
}
}
func (u *upstream) sendAuthReq() error {
if err := u.transport.writePacket(Marshal(&serviceRequestMsg{serviceUserAuth})); err != nil {
return err
}
packet, err := u.transport.readPacket()
if err != nil {
return err
}
var serviceAccept serviceAcceptMsg
if err := Unmarshal(packet, &serviceAccept); err != nil {
return err
}
return nil
}
func newDownstream(c net.Conn, config *ServerConfig) (*downstream, error) {
fullConf := *config
fullConf.SetDefaults()
s := &connection{
sshConn: sshConn{conn: c},
}
_, err := s.serverHandshakeNoAuth(&fullConf)
if err != nil {
c.Close()
return nil, err
}
return &downstream{s}, nil
}
func newUpstream(c net.Conn, addr string, config *ClientConfig) (*upstream, error) {
fullConf := *config
fullConf.SetDefaults()
conn := &connection{
sshConn: sshConn{conn: c},
}
if err := conn.clientHandshakeNoAuth(addr, &fullConf); err != nil {
c.Close()
return nil, err
}
return &upstream{conn}, nil
}
func (d *downstream) nextAuthMsg() (*userAuthRequestMsg, error) {
var userAuthReq userAuthRequestMsg
if packet, err := d.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)
}
return &userAuthReq, nil
}
func noneAuthMsg(user string) *userAuthRequestMsg {
return &userAuthRequestMsg{
User: user,
Service: serviceSSH,
Method: "none",
}
}
func (c *connection) clientHandshakeNoAuth(dialAddress string, config *ClientConfig) error {
c.clientVersion = []byte(packageVersion)
if config.ClientVersion != "" {
c.clientVersion = []byte(config.ClientVersion)
}
var err error
c.serverVersion, err = exchangeVersions(c.sshConn.conn, c.clientVersion)
if err != nil {
return err
}
c.transport = newClientTransport(
newTransport(c.sshConn.conn, config.Rand, true /* is client */),
c.clientVersion, c.serverVersion, config, dialAddress, c.sshConn.RemoteAddr())
if err := c.transport.requestKeyChange(); err != nil {
return err
}
if packet, err := c.transport.readPacket(); err != nil {
return err
} else if packet[0] != msgNewKeys {
return unexpectedMessageError(msgNewKeys, packet[0])
}
return nil
}
func (s *connection) serverHandshakeNoAuth(config *ServerConfig) (*Permissions, error) {
if len(config.hostKeys) == 0 {
return nil, errors.New("ssh: server has no host keys")
}
var err error
s.serverVersion = []byte("SSH-2.0-SSHPiper")
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.requestKeyChange(); err != nil {
return nil, err
}
if packet, err := s.transport.readPacket(); err != nil {
return nil, err
} else if packet[0] != msgNewKeys {
return nil, unexpectedMessageError(msgNewKeys, packet[0])
}
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
}
return nil, nil
}