672 lines
18 KiB
JavaScript
672 lines
18 KiB
JavaScript
'use strict'
|
|
|
|
const assert = require('./assert-compat.js')
|
|
const { inspect } = require('util')
|
|
|
|
const { fixtureKey, mustCall, mustNotCall, setup } = require('./common.js')
|
|
|
|
const serverCfg = { hostKeys: [fixtureKey('ssh_host_rsa_key').raw] }
|
|
|
|
const debug = false
|
|
|
|
// Keys ========================================================================
|
|
;[
|
|
{ desc: 'RSA (old OpenSSH)', clientKey: fixtureKey('id_rsa') },
|
|
{ desc: 'RSA (new OpenSSH)', clientKey: fixtureKey('openssh_new_rsa') },
|
|
{
|
|
desc: 'RSA (encrypted)',
|
|
clientKey: fixtureKey('id_rsa_enc', 'foobarbaz'),
|
|
passphrase: 'foobarbaz'
|
|
},
|
|
{ desc: 'DSA', clientKey: fixtureKey('id_dsa') },
|
|
{ desc: 'ECDSA', clientKey: fixtureKey('id_ecdsa') },
|
|
{ desc: 'PPK', clientKey: fixtureKey('id_rsa.ppk') }
|
|
].forEach((test) => {
|
|
const { desc, clientKey, passphrase } = test
|
|
const username = 'Key User'
|
|
const { server } = setup(desc, {
|
|
client: { username, privateKey: clientKey.raw, passphrase },
|
|
server: serverCfg,
|
|
|
|
debug
|
|
})
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
let authAttempt = 0
|
|
conn
|
|
.on(
|
|
'authentication',
|
|
mustCall((ctx) => {
|
|
assert(ctx.username === username, `Wrong username: ${ctx.username}`)
|
|
switch (++authAttempt) {
|
|
case 1:
|
|
assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`)
|
|
return ctx.reject()
|
|
case 3:
|
|
assert(ctx.signature, 'Missing publickey signature')
|
|
// FALLTHROUGH
|
|
case 2:
|
|
assert(ctx.method === 'publickey', `Wrong auth method: ${ctx.method}`)
|
|
assert(ctx.key.algo === clientKey.key.type, `Wrong key algo: ${ctx.key.algo}`)
|
|
assert.deepStrictEqual(
|
|
clientKey.key.getPublicSSH(),
|
|
ctx.key.data,
|
|
'Public key mismatch'
|
|
)
|
|
break
|
|
}
|
|
if (ctx.signature) {
|
|
const result = clientKey.key.verify(ctx.blob, ctx.signature, ctx.hashAlgo)
|
|
assert(result === true, 'Could not verify publickey signature')
|
|
}
|
|
ctx.accept()
|
|
}, 3)
|
|
)
|
|
.on(
|
|
'ready',
|
|
mustCall(() => {
|
|
conn.end()
|
|
})
|
|
)
|
|
})
|
|
)
|
|
})
|
|
|
|
// Password ====================================================================
|
|
{
|
|
const username = 'Password User'
|
|
const password = 'hi mom'
|
|
const { server } = setup('Password', {
|
|
client: { username, password },
|
|
server: serverCfg,
|
|
|
|
debug
|
|
})
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
let authAttempt = 0
|
|
conn
|
|
.on(
|
|
'authentication',
|
|
mustCall((ctx) => {
|
|
assert(ctx.username === username, `Wrong username: ${ctx.username}`)
|
|
if (++authAttempt === 1) {
|
|
assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`)
|
|
return ctx.reject()
|
|
}
|
|
assert(ctx.method === 'password', `Wrong auth method: ${ctx.method}`)
|
|
assert(ctx.password === password, `Wrong password: ${ctx.password}`)
|
|
ctx.accept()
|
|
}, 2)
|
|
)
|
|
.on(
|
|
'ready',
|
|
mustCall(() => {
|
|
conn.end()
|
|
})
|
|
)
|
|
})
|
|
)
|
|
}
|
|
{
|
|
const username = ''
|
|
const password = 'hi mom'
|
|
const { server } = setup('Password (empty username)', {
|
|
client: { username, password },
|
|
server: serverCfg,
|
|
|
|
debug
|
|
})
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
let authAttempt = 0
|
|
conn
|
|
.on(
|
|
'authentication',
|
|
mustCall((ctx) => {
|
|
assert(ctx.username === username, `Wrong username: ${ctx.username}`)
|
|
if (++authAttempt === 1) {
|
|
assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`)
|
|
return ctx.reject()
|
|
}
|
|
assert(ctx.method === 'password', `Wrong auth method: ${ctx.method}`)
|
|
assert(ctx.password === password, `Wrong password: ${ctx.password}`)
|
|
ctx.accept()
|
|
}, 2)
|
|
)
|
|
.on(
|
|
'ready',
|
|
mustCall(() => {
|
|
conn.end()
|
|
})
|
|
)
|
|
})
|
|
)
|
|
}
|
|
{
|
|
const username = 'foo'
|
|
const oldPassword = 'bar'
|
|
const newPassword = 'baz'
|
|
const changePrompt = 'Prithee changeth thy password'
|
|
const { client, server } = setup('Password (change requested)', {
|
|
client: { username, password: oldPassword },
|
|
server: serverCfg,
|
|
|
|
debug
|
|
})
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
let authAttempt = 0
|
|
conn
|
|
.on(
|
|
'authentication',
|
|
mustCall((ctx) => {
|
|
assert(ctx.username === username, `Wrong username: ${ctx.username}`)
|
|
if (++authAttempt === 1) {
|
|
assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`)
|
|
return ctx.reject()
|
|
}
|
|
assert(ctx.method === 'password', `Wrong auth method: ${ctx.method}`)
|
|
assert(ctx.password === oldPassword, `Wrong old password: ${ctx.password}`)
|
|
ctx.requestChange(
|
|
changePrompt,
|
|
mustCall((newPassword_) => {
|
|
assert(newPassword_ === newPassword, `Wrong new password: ${newPassword_}`)
|
|
ctx.accept()
|
|
})
|
|
)
|
|
}, 2)
|
|
)
|
|
.on(
|
|
'ready',
|
|
mustCall(() => {
|
|
conn.end()
|
|
})
|
|
)
|
|
})
|
|
)
|
|
|
|
client.on(
|
|
'change password',
|
|
mustCall((prompt, done) => {
|
|
assert(prompt === changePrompt, `Wrong password change prompt: ${prompt}`)
|
|
process.nextTick(done, newPassword)
|
|
})
|
|
)
|
|
}
|
|
|
|
// Hostbased ===================================================================
|
|
{
|
|
const localUsername = 'Local User Foo'
|
|
const localHostname = 'Local Host Bar'
|
|
const username = 'Hostbased User'
|
|
const clientKey = fixtureKey('id_rsa')
|
|
const { server } = setup('Hostbased', {
|
|
client: {
|
|
username,
|
|
privateKey: clientKey.raw,
|
|
localUsername,
|
|
localHostname
|
|
},
|
|
server: serverCfg,
|
|
|
|
debug
|
|
})
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
let authAttempt = 0
|
|
conn
|
|
.on(
|
|
'authentication',
|
|
mustCall((ctx) => {
|
|
assert(ctx.username === username, `Wrong username: ${ctx.username}`)
|
|
switch (++authAttempt) {
|
|
case 1:
|
|
assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`)
|
|
return ctx.reject()
|
|
case 2:
|
|
assert(ctx.method === 'publickey', `Wrong auth method: ${ctx.method}`)
|
|
return ctx.reject()
|
|
case 3: {
|
|
assert(ctx.method === 'hostbased', `Wrong auth method: ${ctx.method}`)
|
|
assert(ctx.key.algo === clientKey.key.type, `Wrong key algo: ${ctx.key.algo}`)
|
|
assert.deepStrictEqual(
|
|
clientKey.key.getPublicSSH(),
|
|
ctx.key.data,
|
|
'Public key mismatch'
|
|
)
|
|
assert(ctx.signature, 'Expected signature')
|
|
assert(ctx.localHostname === localHostname, 'Wrong local hostname')
|
|
assert(ctx.localUsername === localUsername, 'Wrong local username')
|
|
const result = clientKey.key.verify(ctx.blob, ctx.signature, ctx.hashAlgo)
|
|
assert(result === true, 'Could not verify hostbased signature')
|
|
|
|
break
|
|
}
|
|
}
|
|
ctx.accept()
|
|
}, 3)
|
|
)
|
|
.on(
|
|
'ready',
|
|
mustCall(() => {
|
|
conn.end()
|
|
})
|
|
)
|
|
})
|
|
)
|
|
}
|
|
|
|
// keyboard-interactive ========================================================
|
|
{
|
|
const username = 'Keyboard-Interactive User'
|
|
const request = {
|
|
name: 'SSH2 Authentication',
|
|
instructions: 'These are instructions',
|
|
prompts: [
|
|
{ prompt: 'Password: ', echo: false },
|
|
{ prompt: 'Is the cake a lie? ', echo: true }
|
|
]
|
|
}
|
|
const responses = ['foobarbaz', 'yes']
|
|
const { client, server } = setup('Password (empty username)', {
|
|
client: {
|
|
username,
|
|
tryKeyboard: true
|
|
},
|
|
server: serverCfg,
|
|
|
|
debug
|
|
})
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
let authAttempt = 0
|
|
conn
|
|
.on(
|
|
'authentication',
|
|
mustCall((ctx) => {
|
|
assert(ctx.username === username, `Wrong username: ${ctx.username}`)
|
|
if (++authAttempt === 1) {
|
|
assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`)
|
|
return ctx.reject()
|
|
}
|
|
assert(ctx.method === 'keyboard-interactive', `Wrong auth method: ${ctx.method}`)
|
|
ctx.prompt(
|
|
request.prompts,
|
|
request.name,
|
|
request.instructions,
|
|
mustCall((responses_) => {
|
|
assert.deepStrictEqual(responses_, responses)
|
|
ctx.accept()
|
|
})
|
|
)
|
|
}, 2)
|
|
)
|
|
.on(
|
|
'ready',
|
|
mustCall(() => {
|
|
conn.end()
|
|
})
|
|
)
|
|
})
|
|
)
|
|
|
|
client.on(
|
|
'keyboard-interactive',
|
|
mustCall((name, instructions, lang, prompts, finish) => {
|
|
assert(name === request.name, `Wrong prompt name: ${name}`)
|
|
assert(instructions === request.instructions, `Wrong prompt instructions: ${instructions}`)
|
|
assert.deepStrictEqual(prompts, request.prompts, `Wrong prompts: ${inspect(prompts)}`)
|
|
process.nextTick(finish, responses)
|
|
})
|
|
)
|
|
}
|
|
|
|
// authHandler() tests =========================================================
|
|
{
|
|
const username = 'foo'
|
|
const password = '1234'
|
|
const clientKey = fixtureKey('id_rsa')
|
|
const { server } = setup('authHandler() (sync)', {
|
|
client: {
|
|
username,
|
|
password,
|
|
privateKey: clientKey.raw,
|
|
|
|
authHandler: mustCall((methodsLeft, partial, cb) => {
|
|
assert(methodsLeft === null, 'expected null methodsLeft')
|
|
assert(partial === null, 'expected null partial')
|
|
return 'none'
|
|
})
|
|
},
|
|
server: serverCfg,
|
|
|
|
debug
|
|
})
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
conn
|
|
.on(
|
|
'authentication',
|
|
mustCall((ctx) => {
|
|
assert(ctx.username === username, `Wrong username: ${ctx.username}`)
|
|
assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`)
|
|
ctx.accept()
|
|
})
|
|
)
|
|
.on(
|
|
'ready',
|
|
mustCall(() => {
|
|
conn.end()
|
|
})
|
|
)
|
|
})
|
|
)
|
|
}
|
|
{
|
|
const username = 'foo'
|
|
const password = '1234'
|
|
const clientKey = fixtureKey('id_rsa')
|
|
const { server } = setup('authHandler() (async)', {
|
|
client: {
|
|
username,
|
|
password,
|
|
privateKey: clientKey.raw,
|
|
|
|
authHandler: mustCall((methodsLeft, partial, cb) => {
|
|
assert(methodsLeft === null, 'expected null methodsLeft')
|
|
assert(partial === null, 'expected null partial')
|
|
process.nextTick(mustCall(cb), 'none')
|
|
})
|
|
},
|
|
server: serverCfg,
|
|
|
|
debug
|
|
})
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
conn
|
|
.on(
|
|
'authentication',
|
|
mustCall((ctx) => {
|
|
assert(ctx.username === username, `Wrong username: ${ctx.username}`)
|
|
assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`)
|
|
ctx.accept()
|
|
})
|
|
)
|
|
.on(
|
|
'ready',
|
|
mustCall(() => {
|
|
conn.end()
|
|
})
|
|
)
|
|
})
|
|
)
|
|
}
|
|
{
|
|
const username = 'foo'
|
|
const password = '1234'
|
|
const clientKey = fixtureKey('id_rsa')
|
|
const { client, server } = setup('authHandler() (no methods left -- sync)', {
|
|
client: {
|
|
username,
|
|
password,
|
|
privateKey: clientKey.raw,
|
|
|
|
authHandler: mustCall((methodsLeft, partial, cb) => {
|
|
assert(methodsLeft === null, 'expected null methodsLeft')
|
|
assert(partial === null, 'expected null partial')
|
|
return false
|
|
})
|
|
},
|
|
server: serverCfg,
|
|
|
|
debug,
|
|
noForceClientReady: true,
|
|
noForceServerReady: true
|
|
})
|
|
|
|
// Remove default client error handler added by `setup()` since we are
|
|
// expecting an error in this case
|
|
client.removeAllListeners('error')
|
|
|
|
client.on(
|
|
'error',
|
|
mustCall((err) => {
|
|
assert.strictEqual(err.level, 'client-authentication')
|
|
assert(/configured authentication methods failed/i.test(err.message), 'Wrong error message')
|
|
})
|
|
)
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
conn.on('authentication', mustNotCall()).on('ready', mustNotCall())
|
|
})
|
|
)
|
|
}
|
|
{
|
|
const username = 'foo'
|
|
const password = '1234'
|
|
const clientKey = fixtureKey('id_rsa')
|
|
const { client, server } = setup('authHandler() (no methods left -- async)', {
|
|
client: {
|
|
username,
|
|
password,
|
|
privateKey: clientKey.raw,
|
|
|
|
authHandler: mustCall((methodsLeft, partial, cb) => {
|
|
assert(methodsLeft === null, 'expected null methodsLeft')
|
|
assert(partial === null, 'expected null partial')
|
|
process.nextTick(mustCall(cb), false)
|
|
})
|
|
},
|
|
server: serverCfg,
|
|
|
|
debug,
|
|
noForceClientReady: true,
|
|
noForceServerReady: true
|
|
})
|
|
|
|
// Remove default client error handler added by `setup()` since we are
|
|
// expecting an error in this case
|
|
client.removeAllListeners('error')
|
|
|
|
client.on(
|
|
'error',
|
|
mustCall((err) => {
|
|
assert.strictEqual(err.level, 'client-authentication')
|
|
assert(/configured authentication methods failed/i.test(err.message), 'Wrong error message')
|
|
})
|
|
)
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
conn.on('authentication', mustNotCall()).on('ready', mustNotCall())
|
|
})
|
|
)
|
|
}
|
|
{
|
|
const username = 'foo'
|
|
const password = '1234'
|
|
const clientKey = fixtureKey('id_rsa')
|
|
const events = []
|
|
const expectedEvents = ['client', 'server', 'client', 'server']
|
|
let clientCalls = 0
|
|
const { client, server } = setup('authHandler() (multi-step)', {
|
|
client: {
|
|
username,
|
|
password,
|
|
privateKey: clientKey.raw,
|
|
|
|
authHandler: mustCall((methodsLeft, partial, cb) => {
|
|
events.push('client')
|
|
switch (++clientCalls) {
|
|
case 1:
|
|
assert(methodsLeft === null, 'expected null methodsLeft')
|
|
assert(partial === null, 'expected null partial')
|
|
return 'publickey'
|
|
case 2:
|
|
assert.deepStrictEqual(
|
|
methodsLeft,
|
|
['password'],
|
|
`expected 'password' method left, saw: ${methodsLeft}`
|
|
)
|
|
assert(partial === true, 'expected partial success')
|
|
return 'password'
|
|
}
|
|
}, 2)
|
|
},
|
|
server: serverCfg,
|
|
|
|
debug
|
|
})
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
let attempts = 0
|
|
conn
|
|
.on(
|
|
'authentication',
|
|
mustCall((ctx) => {
|
|
assert(++attempts === clientCalls, 'server<->client state mismatch')
|
|
assert(ctx.username === username, `Unexpected username: ${ctx.username}`)
|
|
events.push('server')
|
|
switch (attempts) {
|
|
case 1:
|
|
assert(ctx.method === 'publickey', `Wrong auth method: ${ctx.method}`)
|
|
assert(ctx.key.algo === clientKey.key.type, `Unexpected key algo: ${ctx.key.algo}`)
|
|
assert.deepEqual(clientKey.key.getPublicSSH(), ctx.key.data, 'Public key mismatch')
|
|
ctx.reject(['password'], true)
|
|
break
|
|
case 2:
|
|
assert(ctx.method === 'password', `Wrong auth method: ${ctx.method}`)
|
|
assert(ctx.password === password, `Unexpected password: ${ctx.password}`)
|
|
ctx.accept()
|
|
break
|
|
}
|
|
}, 2)
|
|
)
|
|
.on(
|
|
'ready',
|
|
mustCall(() => {
|
|
conn.end()
|
|
})
|
|
)
|
|
})
|
|
)
|
|
|
|
client.on(
|
|
'close',
|
|
mustCall(() => {
|
|
assert.deepStrictEqual(events, expectedEvents)
|
|
})
|
|
)
|
|
}
|
|
{
|
|
const username = 'foo'
|
|
const password = '1234'
|
|
const { server } = setup('authHandler() (custom auth configuration)', {
|
|
client: {
|
|
username: 'bar',
|
|
password: '5678',
|
|
|
|
authHandler: mustCall((methodsLeft, partial, cb) => {
|
|
assert(methodsLeft === null, 'expected null methodsLeft')
|
|
assert(partial === null, 'expected null partial')
|
|
return {
|
|
type: 'password',
|
|
username,
|
|
password
|
|
}
|
|
})
|
|
},
|
|
server: serverCfg,
|
|
|
|
debug
|
|
})
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
conn
|
|
.on(
|
|
'authentication',
|
|
mustCall((ctx) => {
|
|
assert(ctx.username === username, `Wrong username: ${ctx.username}`)
|
|
assert(ctx.method === 'password', `Wrong auth method: ${ctx.method}`)
|
|
assert(ctx.password === password, `Unexpected password: ${ctx.password}`)
|
|
ctx.accept()
|
|
})
|
|
)
|
|
.on(
|
|
'ready',
|
|
mustCall(() => {
|
|
conn.end()
|
|
})
|
|
)
|
|
})
|
|
)
|
|
}
|
|
{
|
|
const username = 'foo'
|
|
const password = '1234'
|
|
const { server } = setup('authHandler() (simple construction with custom auth configuration)', {
|
|
client: {
|
|
username: 'bar',
|
|
password: '5678',
|
|
|
|
authHandler: [
|
|
{
|
|
type: 'password',
|
|
username,
|
|
password
|
|
}
|
|
]
|
|
},
|
|
server: serverCfg,
|
|
|
|
debug
|
|
})
|
|
|
|
server.on(
|
|
'connection',
|
|
mustCall((conn) => {
|
|
conn
|
|
.on(
|
|
'authentication',
|
|
mustCall((ctx) => {
|
|
assert(ctx.username === username, `Wrong username: ${ctx.username}`)
|
|
assert(ctx.method === 'password', `Wrong auth method: ${ctx.method}`)
|
|
assert(ctx.password === password, `Unexpected password: ${ctx.password}`)
|
|
ctx.accept()
|
|
})
|
|
)
|
|
.on(
|
|
'ready',
|
|
mustCall(() => {
|
|
conn.end()
|
|
})
|
|
)
|
|
})
|
|
)
|
|
}
|