adopt new sshpiper api

This commit is contained in:
Boshi Lian 2018-02-13 02:27:18 +08:00
parent b14a776f48
commit d8ba7b77ea
13 changed files with 936 additions and 171 deletions

7
Gopkg.lock generated
View file

@ -34,14 +34,15 @@
"ed25519/internal/edwards25519",
"internal/chacha20",
"poly1305",
"ssh"
"ssh",
"ssh/testdata"
]
revision = "743ed784ccc2ebde7f34698af76a6acdf4e0c251"
revision = "daf7a6932c427819b72906700cd3e9209ec7d70e"
source = "https://github.com/tg123/sshpiper.crypto"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "520641d8f3577123162ab4eca84b1d5765a60d84897a760e50b5073a0e7afddd"
inputs-digest = "a45bf0b707bd655f13d5cd1ac7586dbc57f2eaee25ffc08bf9c3746189c686f3"
solver-name = "gps-cdcl"
solver-version = 1

View file

@ -43,9 +43,7 @@ func main() {
// TODO make this pluggable
piper := &ssh.SSHPiperConfig{
FindUpstream: findUpstreamFromUserfile,
MapPublicKey: mapPublicKeyFromUserfile,
UpstreamHostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO should support by config
FindUpstream: findUpstreamFromUserfile,
}
if config.Challenger != "" {

View file

@ -121,37 +121,51 @@ func parseUpstreamFile(data string) (string, string) {
return line, user
}
func findUpstreamFromUserfile(conn ssh.ConnMetadata) (net.Conn, string, error) {
func findUpstreamFromUserfile(conn ssh.ConnMetadata) (net.Conn, *ssh.SSHPiperAuthPipe, error) {
user := conn.User()
if !checkUsername(user) {
return nil, "", fmt.Errorf("downstream is not using a valid username")
return nil, nil, fmt.Errorf("downstream is not using a valid username")
}
err := UserUpstreamFile.checkPerm(user)
if err != nil {
return nil, "", err
return nil, nil, err
}
data, err := UserUpstreamFile.read(user)
if err != nil {
return nil, "", err
return nil, nil, err
}
addr, mappedUser := parseUpstreamFile(string(data))
if addr == "" {
return nil, "", fmt.Errorf("empty addr")
return nil, nil, fmt.Errorf("empty addr")
}
logger.Printf("mapping user [%v] to [%v@%v]", user, mappedUser, addr)
c, err := net.Dial("tcp", addr)
if err != nil {
return nil, "", err
return nil, nil, err
}
return c, mappedUser, nil
return c, &ssh.SSHPiperAuthPipe{
User: mappedUser,
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (ssh.AuthPipeType, ssh.AuthMethod, error) {
signer, err := mapPublicKeyFromUserfile(conn, key)
if err != nil {
return ssh.AuthPipeTypeDiscard, nil, err
}
return ssh.AuthPipeTypeMap, ssh.PublicKeys(signer), nil
},
UpstreamHostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO should support by config
}, nil
}
func mapPublicKeyFromUserfile(conn ssh.ConnMetadata, key ssh.PublicKey) (signer ssh.Signer, err error) {

View file

@ -27,7 +27,6 @@ import (
"net"
"net/http"
"path"
"strconv"
"strings"
"sync"
"time"
@ -942,16 +941,6 @@ func validCert(domain string, der [][]byte, key crypto.Signer) (leaf *x509.Certi
return leaf, nil
}
func retryAfter(v string) time.Duration {
if i, err := strconv.Atoi(v); err == nil {
return time.Duration(i) * time.Second
}
if t, err := http.ParseTime(v); err == nil {
return t.Sub(timeNow())
}
return time.Second
}
type lockedMathRand struct {
sync.Mutex
rnd *mathrand.Rand

View file

@ -5,7 +5,35 @@
// Package argon2 implements the key derivation function Argon2.
// Argon2 was selected as the winner of the Password Hashing Competition and can
// be used to derive cryptographic keys from passwords.
// Argon2 is specfifed at https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
//
// For a detailed specification of Argon2 see [1].
//
// If you aren't sure which function you need, use Argon2id (IDKey) and
// the parameter recommendations for your scenario.
//
//
// Argon2i
//
// Argon2i (implemented by Key) is the side-channel resistant version of Argon2.
// It uses data-independent memory access, which is preferred for password
// hashing and password-based key derivation. Argon2i requires more passes over
// memory than Argon2id to protect from trade-off attacks. The recommended
// parameters (taken from [2]) for non-interactive operations are time=3 and to
// use the maximum available memory.
//
//
// Argon2id
//
// Argon2id (implemented by IDKey) is a hybrid version of Argon2 combining
// Argon2i and Argon2d. It uses data-independent memory access for the first
// half of the first iteration over the memory and data-dependent memory access
// for the rest. Argon2id is side-channel resistant and provides better brute-
// force cost savings due to time-memory tradeoffs than Argon2i. The recommended
// parameters for non-interactive operations (taken from [2]) are time=1 and to
// use the maximum available memory.
//
// [1] https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
// [2] https://tools.ietf.org/html/draft-irtf-cfrg-argon2-03#section-9.3
package argon2
import (
@ -25,23 +53,50 @@ const (
)
// Key derives a key from the password, salt, and cost parameters using Argon2i
// returning a byte slice of length keyLen that can be used as cryptographic key.
// The CPU cost and parallism degree must be greater than zero.
// returning a byte slice of length keyLen that can be used as cryptographic
// key. The CPU cost and parallism degree must be greater than zero.
//
// For example, you can get a derived key for e.g. AES-256 (which needs a 32-byte key) by doing:
// `key := argon2.Key([]byte("some password"), salt, 4, 32*1024, 4, 32)`
// For example, you can get a derived key for e.g. AES-256 (which needs a
// 32-byte key) by doing: `key := argon2.Key([]byte("some password"), salt, 3,
// 32*1024, 4, 32)`
//
// The recommended parameters for interactive logins as of 2017 are time=4, memory=32*1024.
// The number of threads can be adjusted to the numbers of available CPUs.
// The time parameter specifies the number of passes over the memory and the memory
// parameter specifies the size of the memory in KiB. For example memory=32*1024 sets the
// memory cost to ~32 MB.
// The cost parameters should be increased as memory latency and CPU parallelism increases.
// Remember to get a good random salt.
// The draft RFC recommends[2] time=3, and memory=32*1024 is a sensible number.
// If using that amount of memory (32 MB) is not possible in some contexts then
// the time parameter can be increased to compensate.
//
// The time parameter specifies the number of passes over the memory and the
// memory parameter specifies the size of the memory in KiB. For example
// memory=32*1024 sets the memory cost to ~32 MB. The number of threads can be
// adjusted to the number of available CPUs. The cost parameters should be
// increased as memory latency and CPU parallelism increases. Remember to get a
// good random salt.
func Key(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
return deriveKey(argon2i, password, salt, nil, nil, time, memory, threads, keyLen)
}
// IDKey derives a key from the password, salt, and cost parameters using
// Argon2id returning a byte slice of length keyLen that can be used as
// cryptographic key. The CPU cost and parallism degree must be greater than
// zero.
//
// For example, you can get a derived key for e.g. AES-256 (which needs a
// 32-byte key) by doing: `key := argon2.IDKey([]byte("some password"), salt, 1,
// 64*1024, 4, 32)`
//
// The draft RFC recommends[2] time=1, and memory=64*1024 is a sensible number.
// If using that amount of memory (64 MB) is not possible in some contexts then
// the time parameter can be increased to compensate.
//
// The time parameter specifies the number of passes over the memory and the
// memory parameter specifies the size of the memory in KiB. For example
// memory=64*1024 sets the memory cost to ~64 MB. The number of threads can be
// adjusted to the numbers of available CPUs. The cost parameters should be
// increased as memory latency and CPU parallelism increases. Remember to get a
// good random salt.
func IDKey(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
return deriveKey(argon2id, password, salt, nil, nil, time, memory, threads, keyLen)
}
func deriveKey(mode int, password, salt, secret, data []byte, time, memory uint32, threads uint8, keyLen uint32) []byte {
if time < 1 {
panic("argon2: number of rounds too small")

View file

@ -44,7 +44,9 @@ type Signature struct {
const CertTimeInfinity = 1<<64 - 1
// An Certificate represents an OpenSSH certificate as defined in
// [PROTOCOL.certkeys]?rev=1.8.
// [PROTOCOL.certkeys]?rev=1.8. The Certificate type implements the
// PublicKey interface, so it can be unmarshaled using
// ParsePublicKey.
type Certificate struct {
Nonce []byte
Key PublicKey

View file

@ -11,6 +11,14 @@ import (
"io"
)
type authResult int
const (
authFailure authResult = iota
authPartialSuccess
authSuccess
)
// clientAuthenticate authenticates with the remote server. See RFC 4252.
func (c *connection) clientAuthenticate(config *ClientConfig) error {
// initiate user auth session
@ -37,11 +45,12 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
if err != nil {
return err
}
if ok {
if ok == authSuccess {
// success
return nil
} else if ok == authFailure {
tried[auth.method()] = true
}
tried[auth.method()] = true
if methods == nil {
methods = lastMethods
}
@ -82,7 +91,7 @@ type AuthMethod interface {
// 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) (bool, []string, error)
auth(session []byte, user string, p packetConn, rand io.Reader) (authResult, []string, error)
// method returns the RFC 4252 method name.
method() string
@ -91,13 +100,13 @@ type AuthMethod interface {
// "none" authentication, RFC 4252 section 5.2.
type noneAuth int
func (n *noneAuth) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
func (n *noneAuth) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {
if err := c.writePacket(Marshal(&userAuthRequestMsg{
User: user,
Service: serviceSSH,
Method: "none",
})); err != nil {
return false, nil, err
return authFailure, nil, err
}
return handleAuthResponse(c)
@ -111,7 +120,7 @@ func (n *noneAuth) method() string {
// 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) (bool, []string, error) {
func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {
type passwordAuthMsg struct {
User string `sshtype:"50"`
Service string
@ -125,7 +134,7 @@ func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand
// The program may only find out that the user doesn't have a password
// when prompting.
if err != nil {
return false, nil, err
return authFailure, nil, err
}
if err := c.writePacket(Marshal(&passwordAuthMsg{
@ -135,7 +144,7 @@ func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand
Reply: false,
Password: pw,
})); err != nil {
return false, nil, err
return authFailure, nil, err
}
return handleAuthResponse(c)
@ -178,7 +187,7 @@ func (cb publicKeyCallback) method() string {
return "publickey"
}
func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (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
@ -186,13 +195,13 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
signers, err := cb()
if err != nil {
return false, nil, err
return authFailure, nil, err
}
var methods []string
for _, signer := range signers {
ok, err := validateKey(signer.PublicKey(), user, c)
if err != nil {
return false, nil, err
return authFailure, nil, err
}
if !ok {
continue
@ -206,7 +215,7 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
Method: cb.method(),
}, []byte(pub.Type()), pubKey))
if err != nil {
return false, nil, err
return authFailure, nil, err
}
// manually wrap the serialized signature in a string
@ -224,24 +233,24 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
}
p := Marshal(&msg)
if err := c.writePacket(p); err != nil {
return false, nil, err
return authFailure, nil, err
}
var success bool
var success authResult
success, methods, err = handleAuthResponse(c)
if err != nil {
return false, nil, err
return authFailure, nil, err
}
// If authentication succeeds or the list of available methods does not
// contain the "publickey" method, do not attempt to authenticate with any
// other keys. According to RFC 4252 Section 7, the latter can occur when
// additional authentication methods are required.
if success || !containsMethod(methods, cb.method()) {
if success == authSuccess || !containsMethod(methods, cb.method()) {
return success, methods, err
}
}
return false, methods, nil
return authFailure, methods, nil
}
func containsMethod(methods []string, method string) bool {
@ -318,28 +327,31 @@ func PublicKeysCallback(getSigners func() (signers []Signer, err error)) AuthMet
// 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) (bool, []string, error) {
func handleAuthResponse(c packetConn) (authResult, []string, error) {
for {
packet, err := c.readPacket()
if err != nil {
return false, nil, err
return authFailure, nil, err
}
switch packet[0] {
case msgUserAuthBanner:
if err := handleBannerResponse(c, packet); err != nil {
return false, nil, err
return authFailure, nil, err
}
case msgUserAuthFailure:
var msg userAuthFailureMsg
if err := Unmarshal(packet, &msg); err != nil {
return false, nil, err
return authFailure, nil, err
}
return false, msg.Methods, nil
if msg.PartialSuccess {
return authPartialSuccess, msg.Methods, nil
}
return authFailure, msg.Methods, nil
case msgUserAuthSuccess:
return true, nil, nil
return authSuccess, nil, nil
default:
return false, nil, unexpectedMessageError(msgUserAuthSuccess, packet[0])
return authFailure, nil, unexpectedMessageError(msgUserAuthSuccess, packet[0])
}
}
}
@ -381,7 +393,7 @@ func (cb KeyboardInteractiveChallenge) method() string {
return "keyboard-interactive"
}
func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {
type initiateMsg struct {
User string `sshtype:"50"`
Service string
@ -395,20 +407,20 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe
Service: serviceSSH,
Method: "keyboard-interactive",
})); err != nil {
return false, nil, err
return authFailure, nil, err
}
for {
packet, err := c.readPacket()
if err != nil {
return false, nil, err
return authFailure, nil, err
}
// like handleAuthResponse, but with less options.
switch packet[0] {
case msgUserAuthBanner:
if err := handleBannerResponse(c, packet); err != nil {
return false, nil, err
return authFailure, nil, err
}
continue
case msgUserAuthInfoRequest:
@ -416,18 +428,21 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe
case msgUserAuthFailure:
var msg userAuthFailureMsg
if err := Unmarshal(packet, &msg); err != nil {
return false, nil, err
return authFailure, nil, err
}
return false, msg.Methods, nil
if msg.PartialSuccess {
return authPartialSuccess, msg.Methods, nil
}
return authFailure, msg.Methods, nil
case msgUserAuthSuccess:
return true, nil, nil
return authSuccess, nil, nil
default:
return false, nil, unexpectedMessageError(msgUserAuthInfoRequest, packet[0])
return authFailure, nil, unexpectedMessageError(msgUserAuthInfoRequest, packet[0])
}
var msg userAuthInfoRequestMsg
if err := Unmarshal(packet, &msg); err != nil {
return false, nil, err
return authFailure, nil, err
}
// Manually unpack the prompt/echo pairs.
@ -437,7 +452,7 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe
for i := 0; i < int(msg.NumPrompts); i++ {
prompt, r, ok := parseString(rest)
if !ok || len(r) == 0 {
return false, nil, errors.New("ssh: prompt format error")
return authFailure, nil, errors.New("ssh: prompt format error")
}
prompts = append(prompts, string(prompt))
echos = append(echos, r[0] != 0)
@ -445,16 +460,16 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe
}
if len(rest) != 0 {
return false, nil, errors.New("ssh: extra data following keyboard-interactive pairs")
return authFailure, nil, errors.New("ssh: extra data following keyboard-interactive pairs")
}
answers, err := cb(msg.User, msg.Instruction, prompts, echos)
if err != nil {
return false, nil, err
return authFailure, nil, err
}
if len(answers) != len(prompts) {
return false, nil, errors.New("ssh: not enough answers from keyboard-interactive callback")
return authFailure, nil, errors.New("ssh: not enough answers from keyboard-interactive callback")
}
responseLength := 1 + 4
for _, a := range answers {
@ -470,7 +485,7 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe
}
if err := c.writePacket(serialized); err != nil {
return false, nil, err
return authFailure, nil, err
}
}
}
@ -480,10 +495,10 @@ type retryableAuthMethod struct {
maxTries int
}
func (r *retryableAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader) (ok bool, methods []string, err error) {
func (r *retryableAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader) (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)
if ok || err != nil { // either success or error terminate
if ok != authFailure || err != nil { // either success, partial success or error terminate
return ok, methods, err
}
}

View file

@ -234,7 +234,7 @@ func TestMarshalParsePublicKey(t *testing.T) {
}
}
type authResult struct {
type testAuthResult struct {
pubKey PublicKey
options []string
comments string
@ -242,11 +242,11 @@ type authResult struct {
ok bool
}
func testAuthorizedKeys(t *testing.T, authKeys []byte, expected []authResult) {
func testAuthorizedKeys(t *testing.T, authKeys []byte, expected []testAuthResult) {
rest := authKeys
var values []authResult
var values []testAuthResult
for len(rest) > 0 {
var r authResult
var r testAuthResult
var err error
r.pubKey, r.comments, r.options, rest, err = ParseAuthorizedKey(rest)
r.ok = (err == nil)
@ -264,7 +264,7 @@ func TestAuthorizedKeyBasic(t *testing.T) {
pub, pubSerialized := getTestKey()
line := "ssh-rsa " + pubSerialized + " user@host"
testAuthorizedKeys(t, []byte(line),
[]authResult{
[]testAuthResult{
{pub, nil, "user@host", "", true},
})
}
@ -286,7 +286,7 @@ func TestAuth(t *testing.T) {
authOptions := strings.Join(authWithOptions, eol)
rest2 := strings.Join(authWithOptions[3:], eol)
rest3 := strings.Join(authWithOptions[6:], eol)
testAuthorizedKeys(t, []byte(authOptions), []authResult{
testAuthorizedKeys(t, []byte(authOptions), []testAuthResult{
{pub, []string{`env="HOME=/home/root"`, "no-port-forwarding"}, "user@host", rest2, true},
{pub, []string{`env="HOME=/home/root2"`}, "user2@host2", rest3, true},
{nil, nil, "", "", false},
@ -297,7 +297,7 @@ func TestAuth(t *testing.T) {
func TestAuthWithQuotedSpaceInEnv(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithQuotedSpaceInEnv := []byte(`env="HOME=/home/root dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`)
testAuthorizedKeys(t, []byte(authWithQuotedSpaceInEnv), []authResult{
testAuthorizedKeys(t, []byte(authWithQuotedSpaceInEnv), []testAuthResult{
{pub, []string{`env="HOME=/home/root dir"`, "no-port-forwarding"}, "user@host", "", true},
})
}
@ -305,7 +305,7 @@ func TestAuthWithQuotedSpaceInEnv(t *testing.T) {
func TestAuthWithQuotedCommaInEnv(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithQuotedCommaInEnv := []byte(`env="HOME=/home/root,dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`)
testAuthorizedKeys(t, []byte(authWithQuotedCommaInEnv), []authResult{
testAuthorizedKeys(t, []byte(authWithQuotedCommaInEnv), []testAuthResult{
{pub, []string{`env="HOME=/home/root,dir"`, "no-port-forwarding"}, "user@host", "", true},
})
}
@ -314,11 +314,11 @@ func TestAuthWithQuotedQuoteInEnv(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithQuotedQuoteInEnv := []byte(`env="HOME=/home/\"root dir",no-port-forwarding` + "\t" + `ssh-rsa` + "\t" + pubSerialized + ` user@host`)
authWithDoubleQuotedQuote := []byte(`no-port-forwarding,env="HOME=/home/ \"root dir\"" ssh-rsa ` + pubSerialized + "\t" + `user@host`)
testAuthorizedKeys(t, []byte(authWithQuotedQuoteInEnv), []authResult{
testAuthorizedKeys(t, []byte(authWithQuotedQuoteInEnv), []testAuthResult{
{pub, []string{`env="HOME=/home/\"root dir"`, "no-port-forwarding"}, "user@host", "", true},
})
testAuthorizedKeys(t, []byte(authWithDoubleQuotedQuote), []authResult{
testAuthorizedKeys(t, []byte(authWithDoubleQuotedQuote), []testAuthResult{
{pub, []string{"no-port-forwarding", `env="HOME=/home/ \"root dir\""`}, "user@host", "", true},
})
}
@ -327,7 +327,7 @@ func TestAuthWithInvalidSpace(t *testing.T) {
_, pubSerialized := getTestKey()
authWithInvalidSpace := []byte(`env="HOME=/home/root dir", no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host
#more to follow but still no valid keys`)
testAuthorizedKeys(t, []byte(authWithInvalidSpace), []authResult{
testAuthorizedKeys(t, []byte(authWithInvalidSpace), []testAuthResult{
{nil, nil, "", "", false},
})
}
@ -337,7 +337,7 @@ func TestAuthWithMissingQuote(t *testing.T) {
authWithMissingQuote := []byte(`env="HOME=/home/root,no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host
env="HOME=/home/root",shared-control ssh-rsa ` + pubSerialized + ` user@host`)
testAuthorizedKeys(t, []byte(authWithMissingQuote), []authResult{
testAuthorizedKeys(t, []byte(authWithMissingQuote), []testAuthResult{
{pub, []string{`env="HOME=/home/root"`, `shared-control`}, "user@host", "", true},
})
}

View file

@ -11,6 +11,31 @@ import (
"net"
)
type AuthPipeType int
const (
AuthPipeTypePassThrough AuthPipeType = iota
AuthPipeTypeMap
AuthPipeTypeDiscard
AuthPipeTypeNone
)
type SSHPiperAuthPipe struct {
User string
PasswordCallback func(conn ConnMetadata, password []byte) (AuthPipeType, AuthMethod, error)
PublicKeyCallback func(conn ConnMetadata, key PublicKey) (AuthPipeType, AuthMethod, error)
// HostKeyCallback is called during the cryptographic
// handshake to validate the uptream server's host key. The piper
// configuration must supply this callback for the connection
// to succeed. The functions InsecureIgnoreHostKey or
// FixedHostKey can be used for simplistic host key checks.
// UpstreamHostKeyCallback HostKeyCallback
UpstreamHostKeyCallback HostKeyCallback
}
// SSHPiperConfig holds SSHPiper specific configuration data.
type SSHPiperConfig struct {
Config
@ -28,7 +53,7 @@ type SSHPiperConfig struct {
// and upstream username should be returned.
// SSHPiper will use the username from downstream if empty username is returned.
// If any error occurs, the piped connection will be closed.
FindUpstream func(conn ConnMetadata) (net.Conn, string, error)
FindUpstream func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error)
// MapPublicKey, if non-nil, is called when downstream requests a publickey auth.
// SSHPiper will sign the auth packet message using the returned Signer.
@ -38,14 +63,8 @@ type SSHPiperConfig struct {
// upstream ssh server instead.
//
// More info: https://github.com/tg123/sshpiper#publickey-sign-again
MapPublicKey func(conn ConnMetadata, key PublicKey) (Signer, error)
// MapPublicKey func(conn ConnMetadata, key PublicKey) (Signer, error)
// HostKeyCallback is called during the cryptographic
// handshake to validate the uptream server's host key. The piper
// configuration must supply this callback for the connection
// to succeed. The functions InsecureIgnoreHostKey or
// FixedHostKey can be used for simplistic host key checks.
UpstreamHostKeyCallback HostKeyCallback
}
type upstream struct{ *connection }
@ -132,9 +151,10 @@ func NewSSHPiperConn(conn net.Conn, piper *SSHPiperConfig) (pipe *SSHPiperConn,
return nil, errors.New("sshpiper: must specify FindUpstream")
}
if piper.UpstreamHostKeyCallback == nil {
return nil, errors.New("sshpiper: must specify UpstreamHostKeyCallback")
}
// TODO
//if piper.UpstreamHostKeyCallback == nil {
// return nil, errors.New("sshpiper: must specify UpstreamHostKeyCallback")
//}
d, err := newDownstream(conn, &ServerConfig{
Config: piper.Config,
@ -191,11 +211,16 @@ func NewSSHPiperConn(conn net.Conn, piper *SSHPiperConfig) (pipe *SSHPiperConn,
}
}
upconn, mappedUser, err := piper.FindUpstream(d)
upconn, pipeauth, err := piper.FindUpstream(d)
if err != nil {
return nil, err
}
if pipeauth.UpstreamHostKeyCallback == nil {
return nil, errors.New("sshpiper: must specify UpstreamHostKeyCallback")
}
mappedUser := pipeauth.User
addr := upconn.RemoteAddr().String()
if mappedUser == "" {
@ -203,7 +228,7 @@ func NewSSHPiperConn(conn net.Conn, piper *SSHPiperConfig) (pipe *SSHPiperConn,
}
u, err := newUpstream(upconn, addr, &ClientConfig{
HostKeyCallback: piper.UpstreamHostKeyCallback,
HostKeyCallback: pipeauth.UpstreamHostKeyCallback,
})
if err != nil {
return nil, err
@ -221,50 +246,143 @@ func NewSSHPiperConn(conn net.Conn, piper *SSHPiperConfig) (pipe *SSHPiperConn,
p.processAuthMsg = func(msg *userAuthRequestMsg) (*userAuthRequestMsg, error) {
// only public msg need
if msg.Method != "publickey" || piper.MapPublicKey == nil {
msg.User = mappedUser
msg.User = mappedUser
var authType AuthPipeType = AuthPipeTypePassThrough
var authMethod AuthMethod
switch msg.Method {
case "publickey":
if pipeauth.PublicKeyCallback == nil {
break
}
// pubKey MAP
downKey, isQuery, sig, err := parsePublicKeyMsg(msg)
if err != nil {
return nil, err
}
authType, authMethod, err = pipeauth.PublicKeyCallback(d, downKey)
if err != nil {
return nil, err
}
if isQuery {
// reply for query msg
// skip query from upstream
err = p.ack(downKey)
if err != nil {
return nil, err
}
// discard msg
return nil, nil
} else {
ok, err := p.checkPublicKey(msg, downKey, sig)
if err != nil {
return nil, err
}
if !ok {
return noneAuthMsg(mappedUser), nil
}
}
case "password":
if pipeauth.PasswordCallback == nil {
break
}
payload := msg.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)
}
authType, authMethod, err = pipeauth.PasswordCallback(d, password)
if err != nil {
return nil, err
}
default:
}
switch authType {
case AuthPipeTypePassThrough:
return msg, nil
}
// 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 {
case AuthPipeTypeDiscard:
return msg, nil
case AuthPipeTypeNone:
return noneAuthMsg(mappedUser), nil
case AuthPipeTypeMap:
}
upKey := signer.PublicKey()
// map publickey, password, etc to auth
if isQuery {
// reply for query msg
msg, err = p.validAndAck(mappedUser, upKey, downKey)
if err != nil {
return nil, err
}
switch authMethod.method() {
case "publickey":
} else {
ok, err := p.checkPublicKey(msg, downKey, sig)
if err != nil {
return nil, err
}
f, ok := authMethod.(publicKeyCallback)
if !ok {
return noneAuthMsg(mappedUser), nil
return nil, errors.New("sshpiper: publicKeyCallback type assertions failed")
}
msg, err = p.signAgain(mappedUser, msg, signer, downKey)
signers, err := f()
// no mapped user change it to none or error occur
//if err != nil || len(signers) == 0 {
if err != nil {
return nil, err
}
for _, signer := range signers {
msg, err = p.signAgain(mappedUser, msg, signer)
if err != nil {
return nil, err
}
}
case "password":
f, ok := authMethod.(passwordCallback)
if !ok {
return nil, errors.New("sshpiper: passwordCallback type assertions failed")
}
pw, err := f()
if err != nil {
return nil, err
}
type passwordAuthMsg struct {
User string `sshtype:"50"`
Service string
Method string
Reply bool
Password string
}
Unmarshal(Marshal(passwordAuthMsg{
User: mappedUser,
Service: serviceSSH,
Method: "password",
Reply: false,
Password: pw,
}), msg)
return msg, nil
default:
}
return msg, nil
@ -278,17 +396,27 @@ func NewSSHPiperConn(conn net.Conn, piper *SSHPiperConfig) (pipe *SSHPiperConn,
return &SSHPiperConn{pipedConn: p}, nil
}
func (pipe *pipedConn) ack(key PublicKey) error {
okMsg := userAuthPubKeyOkMsg{
Algo: key.Type(),
PubKey: key.Marshal(),
}
if err := pipe.downstream.transport.writePacket(Marshal(&okMsg)); err != nil {
return err
}
return nil
}
// not used after method to method map enable
func (pipe *pipedConn) validAndAck(user string, upKey, downKey PublicKey) (*userAuthRequestMsg, error) {
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 {
if err = pipe.ack(downKey); err != nil {
return nil, err
}
@ -312,7 +440,7 @@ func (pipe *pipedConn) checkPublicKey(msg *userAuthRequestMsg, pubkey PublicKey,
return true, nil
}
func (pipe *pipedConn) signAgain(user string, msg *userAuthRequestMsg, signer Signer, downKey PublicKey) (*userAuthRequestMsg, error) {
func (pipe *pipedConn) signAgain(user string, msg *userAuthRequestMsg, signer Signer) (*userAuthRequestMsg, error) {
rand := pipe.upstream.transport.config.Rand
session := pipe.upstream.transport.getSessionID()

View file

@ -22,16 +22,18 @@ func ExampleNewSSHPiperConn() {
piper := &SSHPiperConfig{
// return conn dial to serverAddr
FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) {
FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) {
c, err := net.Dial("tcp", serverAddr)
if err != nil {
return nil, "", err
return nil, nil, err
}
// change upstream username to root
return c, "root", nil
return c, &SSHPiperAuthPipe{
User: "root",
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
}, nil
},
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
}
// add private key
@ -94,14 +96,14 @@ func dialPiper(piper *SSHPiperConfig) (net.Conn, error) {
return c, nil
}
func TestFindUpstreamCallback(t *testing.T) {
func TestPiperFindUpstreamCallback(t *testing.T) {
const username = "testuser"
var called bool
c, err := dialPiper(&SSHPiperConfig{
FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) {
FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) {
if username != conn.User() {
t.Errorf("different username")
}
@ -114,13 +116,18 @@ func TestFindUpstreamCallback(t *testing.T) {
t.Errorf("default username changed")
}
if string(password) != "password" {
t.Errorf("password not equal")
}
return nil, nil
},
}, t)
return s, "", err
return s, &SSHPiperAuthPipe{
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
}, err
},
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
})
if err != nil {
@ -140,14 +147,14 @@ func TestFindUpstreamCallback(t *testing.T) {
}
// TODO clean up duplicate code
func TestFindUpstreamWithUserCallback(t *testing.T) {
func TestPiperFindUpstreamWithUserCallback(t *testing.T) {
const username = "testuser"
const mappedname = "mappedname"
var called bool
c, err := dialPiper(&SSHPiperConfig{
FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) {
FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) {
s, err := dialUpstream(simpleEchoHandler, &ServerConfig{
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
@ -161,9 +168,11 @@ func TestFindUpstreamWithUserCallback(t *testing.T) {
},
}, t)
return s, mappedname, err
return s, &SSHPiperAuthPipe{
User: mappedname,
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
}, err
},
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
})
if err != nil {
@ -181,7 +190,7 @@ func TestFindUpstreamWithUserCallback(t *testing.T) {
}
}
func TestMapPublicKey(t *testing.T) {
func TestPiperMapPublicKey(t *testing.T) {
certChecker := CertChecker{
IsUserAuthority: func(k PublicKey) bool {
@ -200,17 +209,19 @@ func TestMapPublicKey(t *testing.T) {
}
c, err := dialPiper(&SSHPiperConfig{
FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) {
FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) {
s, err := dialUpstream(simpleEchoHandler, &ServerConfig{
PublicKeyCallback: certChecker.Authenticate,
}, t)
return s, "", err
},
return s, &SSHPiperAuthPipe{
MapPublicKey: func(conn ConnMetadata, key PublicKey) (Signer, error) {
return testSigners["rsa"], nil
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (AuthPipeType, AuthMethod, error) {
return AuthPipeTypeMap, PublicKeys(testSigners["rsa"]), nil
},
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
}, err
},
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
})
if err != nil {
@ -230,7 +241,177 @@ func TestMapPublicKey(t *testing.T) {
}
}
func TestAdditionalChallenge(t *testing.T) {
func TestPiperMapPublicKeyToPassword(t *testing.T) {
certChecker := CertChecker{
IsUserAuthority: func(k PublicKey) bool {
return bytes.Equal(k.Marshal(), testPublicKeys["ecdsa"].Marshal())
},
UserKeyFallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, nil
}
return nil, fmt.Errorf("pubkey for %q not acceptable", conn.User())
},
IsRevoked: func(c *Certificate) bool {
return c.Serial == 666
},
}
var called bool
c, err := dialPiper(&SSHPiperConfig{
FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) {
s, err := dialUpstream(simpleEchoHandler, &ServerConfig{
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
t.Errorf("PasswordCallback should not be called")
return nil, nil
},
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
called = true
return certChecker.Authenticate(conn, key)
},
}, t)
return s, &SSHPiperAuthPipe{
PasswordCallback: func(conn ConnMetadata, password []byte) (AuthPipeType, AuthMethod, error) {
if string(password) != "mypassword" {
t.Errorf("password not equal")
}
return AuthPipeTypeMap, PublicKeys(testSigners["rsa"]), nil
},
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
}, err
},
})
if err != nil {
t.Fatalf("connect dial to piper: %v", err)
}
_, _, _, err = NewClientConn(c, "", &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
Password("mypassword"),
},
HostKeyCallback: InsecureIgnoreHostKey(),
})
if err != nil {
t.Fatalf("can connect to piper %v", err)
}
if !called {
t.Fatalf("PublicKeyCallback not called")
}
}
func TestPiperPasswordToMapPublicKey(t *testing.T) {
var called bool
c, err := dialPiper(&SSHPiperConfig{
FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) {
s, err := dialUpstream(simpleEchoHandler, &ServerConfig{
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
called = true
if string(password) != "mypassword" {
t.Errorf("password not equal")
}
return nil, nil
},
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
t.Errorf("PublicKeyCallback should not be called")
return nil, nil
},
}, t)
return s, &SSHPiperAuthPipe{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (AuthPipeType, AuthMethod, error) {
return AuthPipeTypeMap, Password("mypassword"), nil
},
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
}, err
},
})
if err != nil {
t.Fatalf("connect dial to piper: %v", err)
}
_, _, _, err = NewClientConn(c, "", &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
},
HostKeyCallback: InsecureIgnoreHostKey(),
})
if err != nil {
t.Fatalf("can connect to piper %v", err)
}
if !called {
t.Fatalf("PasswordCallback not called")
}
}
func TestPiperServerWithBanner(t *testing.T) {
const username = "testuser"
var called bool
c, err := dialPiper(&SSHPiperConfig{
FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) {
if username != conn.User() {
t.Errorf("different username")
}
s, err := dialUpstream(simpleEchoHandler, &ServerConfig{
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
return nil, nil
},
BannerCallback: func(conn ConnMetadata) string {
called = true
return "banner"
},
}, t)
return s, &SSHPiperAuthPipe{
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
}, err
},
})
if err != nil {
t.Fatalf("connect dial to piper: %v", err)
}
NewClientConn(c, "", &ClientConfig{
User: username,
Auth: []AuthMethod{Password("password")},
HostKeyCallback: InsecureIgnoreHostKey(),
BannerCallback: func(message string) error {
if message != "banner" {
t.Errorf("bad banner string")
}
return nil
},
})
if !called {
t.Fatalf("FindUpstream not called")
}
}
func TestPiperAdditionalChallenge(t *testing.T) {
c, err := dialPiper(&SSHPiperConfig{
AdditionalChallenge: func(conn ConnMetadata, challenge KeyboardInteractiveChallenge) (bool, error) {
ans, err := challenge("user",
@ -249,11 +430,12 @@ func TestAdditionalChallenge(t *testing.T) {
}
return false, fmt.Errorf("keyboard-interactive failed")
},
FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) {
FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) {
s, err := dialUpstream(simpleEchoHandler, &ServerConfig{NoClientAuth: true}, t)
return s, "", err
return s, &SSHPiperAuthPipe{
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
}, err
},
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
})
if err != nil {
@ -320,14 +502,15 @@ func dialUpstream(handler serverType, upstream *ServerConfig, t *testing.T) (net
return c, nil
}
func TestPipeData(t *testing.T) {
func TestPiperPipeData(t *testing.T) {
c, err := dialPiper(&SSHPiperConfig{
FindUpstream: func(conn ConnMetadata) (net.Conn, string, error) {
FindUpstream: func(conn ConnMetadata) (net.Conn, *SSHPiperAuthPipe, error) {
s, err := dialUpstream(simpleEchoHandler, &ServerConfig{NoClientAuth: true}, t)
return s, "", err
return s, &SSHPiperAuthPipe{
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
}, err
},
UpstreamHostKeyCallback: InsecureIgnoreHostKey(),
})
if err != nil {

144
vendor/golang.org/x/crypto/ssh/test/multi_auth_test.go generated vendored Normal file
View file

@ -0,0 +1,144 @@
// Copyright 2017 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.
// Tests for ssh client multi-auth
//
// These tests run a simple go ssh client against OpenSSH server
// over unix domain sockets. The tests use multiple combinations
// of password, keyboard-interactive and publickey authentication
// methods.
//
// A wrapper library for making sshd PAM authentication use test
// passwords is required in ./sshd_test_pw.so. If the library does
// not exist these tests will be skipped. See compile instructions
// (for linux) in file ./sshd_test_pw.c.
// +build linux
package test
import (
"fmt"
"strings"
"testing"
"golang.org/x/crypto/ssh"
)
// test cases
type multiAuthTestCase struct {
authMethods []string
expectedPasswordCbs int
expectedKbdIntCbs int
}
// test context
type multiAuthTestCtx struct {
password string
numPasswordCbs int
numKbdIntCbs int
}
// create test context
func newMultiAuthTestCtx(t *testing.T) *multiAuthTestCtx {
password, err := randomPassword()
if err != nil {
t.Fatalf("Failed to generate random test password: %s", err.Error())
}
return &multiAuthTestCtx{
password: password,
}
}
// password callback
func (ctx *multiAuthTestCtx) passwordCb() (secret string, err error) {
ctx.numPasswordCbs++
return ctx.password, nil
}
// keyboard-interactive callback
func (ctx *multiAuthTestCtx) kbdIntCb(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
if len(questions) == 0 {
return nil, nil
}
ctx.numKbdIntCbs++
if len(questions) == 1 {
return []string{ctx.password}, nil
}
return nil, fmt.Errorf("unsupported keyboard-interactive flow")
}
// TestMultiAuth runs several subtests for different combinations of password, keyboard-interactive and publickey authentication methods
func TestMultiAuth(t *testing.T) {
testCases := []multiAuthTestCase{
// Test password,publickey authentication, assert that password callback is called 1 time
multiAuthTestCase{
authMethods: []string{"password", "publickey"},
expectedPasswordCbs: 1,
},
// Test keyboard-interactive,publickey authentication, assert that keyboard-interactive callback is called 1 time
multiAuthTestCase{
authMethods: []string{"keyboard-interactive", "publickey"},
expectedKbdIntCbs: 1,
},
// Test publickey,password authentication, assert that password callback is called 1 time
multiAuthTestCase{
authMethods: []string{"publickey", "password"},
expectedPasswordCbs: 1,
},
// Test publickey,keyboard-interactive authentication, assert that keyboard-interactive callback is called 1 time
multiAuthTestCase{
authMethods: []string{"publickey", "keyboard-interactive"},
expectedKbdIntCbs: 1,
},
// Test password,password authentication, assert that password callback is called 2 times
multiAuthTestCase{
authMethods: []string{"password", "password"},
expectedPasswordCbs: 2,
},
}
for _, testCase := range testCases {
t.Run(strings.Join(testCase.authMethods, ","), func(t *testing.T) {
ctx := newMultiAuthTestCtx(t)
server := newServerForConfig(t, "MultiAuth", map[string]string{"AuthMethods": strings.Join(testCase.authMethods, ",")})
defer server.Shutdown()
clientConfig := clientConfig()
server.setTestPassword(clientConfig.User, ctx.password)
publicKeyAuthMethod := clientConfig.Auth[0]
clientConfig.Auth = nil
for _, authMethod := range testCase.authMethods {
switch authMethod {
case "publickey":
clientConfig.Auth = append(clientConfig.Auth, publicKeyAuthMethod)
case "password":
clientConfig.Auth = append(clientConfig.Auth,
ssh.RetryableAuthMethod(ssh.PasswordCallback(ctx.passwordCb), 5))
case "keyboard-interactive":
clientConfig.Auth = append(clientConfig.Auth,
ssh.RetryableAuthMethod(ssh.KeyboardInteractive(ctx.kbdIntCb), 5))
default:
t.Fatalf("Unknown authentication method %s", authMethod)
}
}
conn := server.Dial(clientConfig)
defer conn.Close()
if ctx.numPasswordCbs != testCase.expectedPasswordCbs {
t.Fatalf("passwordCallback was called %d times, expected %d times", ctx.numPasswordCbs, testCase.expectedPasswordCbs)
}
if ctx.numKbdIntCbs != testCase.expectedKbdIntCbs {
t.Fatalf("keyboardInteractiveCallback was called %d times, expected %d times", ctx.numKbdIntCbs, testCase.expectedKbdIntCbs)
}
})
}
}

173
vendor/golang.org/x/crypto/ssh/test/sshd_test_pw.c generated vendored Normal file
View file

@ -0,0 +1,173 @@
// Copyright 2017 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.
// sshd_test_pw.c
// Wrapper to inject test password data for sshd PAM authentication
//
// This wrapper implements custom versions of getpwnam, getpwnam_r,
// getspnam and getspnam_r. These functions first call their real
// libc versions, then check if the requested user matches test user
// specified in env variable TEST_USER and if so replace the password
// with crypted() value of TEST_PASSWD env variable.
//
// Compile:
// gcc -Wall -shared -o sshd_test_pw.so -fPIC sshd_test_pw.c
//
// Compile with debug:
// gcc -DVERBOSE -Wall -shared -o sshd_test_pw.so -fPIC sshd_test_pw.c
//
// Run sshd:
// LD_PRELOAD="sshd_test_pw.so" TEST_USER="..." TEST_PASSWD="..." sshd ...
// +build ignore
#define _GNU_SOURCE
#include <string.h>
#include <pwd.h>
#include <shadow.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#ifdef VERBOSE
#define DEBUG(X...) fprintf(stderr, X)
#else
#define DEBUG(X...) while (0) { }
#endif
/* crypt() password */
static char *
pwhash(char *passwd) {
return strdup(crypt(passwd, "$6$"));
}
/* Pointers to real functions in libc */
static struct passwd * (*real_getpwnam)(const char *) = NULL;
static int (*real_getpwnam_r)(const char *, struct passwd *, char *, size_t, struct passwd **) = NULL;
static struct spwd * (*real_getspnam)(const char *) = NULL;
static int (*real_getspnam_r)(const char *, struct spwd *, char *, size_t, struct spwd **) = NULL;
/* Cached test user and test password */
static char *test_user = NULL;
static char *test_passwd_hash = NULL;
static void
init(void) {
/* Fetch real libc function pointers */
real_getpwnam = dlsym(RTLD_NEXT, "getpwnam");
real_getpwnam_r = dlsym(RTLD_NEXT, "getpwnam_r");
real_getspnam = dlsym(RTLD_NEXT, "getspnam");
real_getspnam_r = dlsym(RTLD_NEXT, "getspnam_r");
/* abort if env variables are not defined */
if (getenv("TEST_USER") == NULL || getenv("TEST_PASSWD") == NULL) {
fprintf(stderr, "env variables TEST_USER and TEST_PASSWD are missing\n");
abort();
}
/* Fetch test user and test password from env */
test_user = strdup(getenv("TEST_USER"));
test_passwd_hash = pwhash(getenv("TEST_PASSWD"));
DEBUG("sshd_test_pw init():\n");
DEBUG("\treal_getpwnam: %p\n", real_getpwnam);
DEBUG("\treal_getpwnam_r: %p\n", real_getpwnam_r);
DEBUG("\treal_getspnam: %p\n", real_getspnam);
DEBUG("\treal_getspnam_r: %p\n", real_getspnam_r);
DEBUG("\tTEST_USER: '%s'\n", test_user);
DEBUG("\tTEST_PASSWD: '%s'\n", getenv("TEST_PASSWD"));
DEBUG("\tTEST_PASSWD_HASH: '%s'\n", test_passwd_hash);
}
static int
is_test_user(const char *name) {
if (test_user != NULL && strcmp(test_user, name) == 0)
return 1;
return 0;
}
/* getpwnam */
struct passwd *
getpwnam(const char *name) {
struct passwd *pw;
DEBUG("sshd_test_pw getpwnam(%s)\n", name);
if (real_getpwnam == NULL)
init();
if ((pw = real_getpwnam(name)) == NULL)
return NULL;
if (is_test_user(name))
pw->pw_passwd = strdup(test_passwd_hash);
return pw;
}
/* getpwnam_r */
int
getpwnam_r(const char *name,
struct passwd *pwd,
char *buf,
size_t buflen,
struct passwd **result) {
int r;
DEBUG("sshd_test_pw getpwnam_r(%s)\n", name);
if (real_getpwnam_r == NULL)
init();
if ((r = real_getpwnam_r(name, pwd, buf, buflen, result)) != 0 || *result == NULL)
return r;
if (is_test_user(name))
pwd->pw_passwd = strdup(test_passwd_hash);
return 0;
}
/* getspnam */
struct spwd *
getspnam(const char *name) {
struct spwd *sp;
DEBUG("sshd_test_pw getspnam(%s)\n", name);
if (real_getspnam == NULL)
init();
if ((sp = real_getspnam(name)) == NULL)
return NULL;
if (is_test_user(name))
sp->sp_pwdp = strdup(test_passwd_hash);
return sp;
}
/* getspnam_r */
int
getspnam_r(const char *name,
struct spwd *spbuf,
char *buf,
size_t buflen,
struct spwd **spbufp) {
int r;
DEBUG("sshd_test_pw getspnam_r(%s)\n", name);
if (real_getspnam_r == NULL)
init();
if ((r = real_getspnam_r(name, spbuf, buf, buflen, spbufp)) != 0)
return r;
if (is_test_user(name))
spbuf->sp_pwdp = strdup(test_passwd_hash);
return r;
}

View file

@ -10,6 +10,8 @@ package test
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
@ -25,7 +27,8 @@ import (
"golang.org/x/crypto/ssh/testdata"
)
const sshdConfig = `
const (
defaultSshdConfig = `
Protocol 2
Banner {{.Dir}}/banner
HostKey {{.Dir}}/id_rsa
@ -50,8 +53,17 @@ RhostsRSAAuthentication no
HostbasedAuthentication no
PubkeyAcceptedKeyTypes=*
`
multiAuthSshdConfigTail = `
UsePAM yes
PasswordAuthentication yes
ChallengeResponseAuthentication yes
AuthenticationMethods {{.AuthMethods}}
`
)
var configTmpl = template.Must(template.New("").Parse(sshdConfig))
var configTmpl = map[string]*template.Template{
"default": template.Must(template.New("").Parse(defaultSshdConfig)),
"MultiAuth": template.Must(template.New("").Parse(defaultSshdConfig + multiAuthSshdConfigTail))}
type server struct {
t *testing.T
@ -60,6 +72,10 @@ type server struct {
cmd *exec.Cmd
output bytes.Buffer // holds stderr from sshd process
testUser string // test username for sshd
testPasswd string // test password for sshd
sshdTestPwSo string // dynamic library to inject a custom password into sshd
// Client half of the network connection.
clientConn net.Conn
}
@ -186,6 +202,20 @@ func (s *server) TryDialWithAddr(config *ssh.ClientConfig, addr string) (*ssh.Cl
s.cmd.Stdin = f
s.cmd.Stdout = f
s.cmd.Stderr = &s.output
if s.sshdTestPwSo != "" {
if s.testUser == "" {
s.t.Fatal("user missing from sshd_test_pw.so config")
}
if s.testPasswd == "" {
s.t.Fatal("password missing from sshd_test_pw.so config")
}
s.cmd.Env = append(os.Environ(),
fmt.Sprintf("LD_PRELOAD=%s", s.sshdTestPwSo),
fmt.Sprintf("TEST_USER=%s", s.testUser),
fmt.Sprintf("TEST_PASSWD=%s", s.testPasswd))
}
if err := s.cmd.Start(); err != nil {
s.t.Fail()
s.Shutdown()
@ -236,8 +266,39 @@ func writeFile(path string, contents []byte) {
}
}
// generate random password
func randomPassword() (string, error) {
b := make([]byte, 12)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
// setTestPassword is used for setting user and password data for sshd_test_pw.so
// This function also checks that ./sshd_test_pw.so exists and if not calls s.t.Skip()
func (s *server) setTestPassword(user, passwd string) error {
wd, _ := os.Getwd()
wrapper := filepath.Join(wd, "sshd_test_pw.so")
if _, err := os.Stat(wrapper); err != nil {
s.t.Skip(fmt.Errorf("sshd_test_pw.so is not available"))
return err
}
s.sshdTestPwSo = wrapper
s.testUser = user
s.testPasswd = passwd
return nil
}
// newServer returns a new mock ssh server.
func newServer(t *testing.T) *server {
return newServerForConfig(t, "default", map[string]string{})
}
// newServerForConfig returns a new mock ssh server.
func newServerForConfig(t *testing.T, config string, configVars map[string]string) *server {
if testing.Short() {
t.Skip("skipping test due to -short")
}
@ -249,9 +310,11 @@ func newServer(t *testing.T) *server {
if err != nil {
t.Fatal(err)
}
err = configTmpl.Execute(f, map[string]string{
"Dir": dir,
})
if _, ok := configTmpl[config]; ok == false {
t.Fatal(fmt.Errorf("Invalid server config '%s'", config))
}
configVars["Dir"] = dir
err = configTmpl[config].Execute(f, configVars)
if err != nil {
t.Fatal(err)
}