Files
bare-operating-system/packages/bare-os-openssh/vendor/bare-ssh2/test/test-integration-openssh.js
T

550 lines
16 KiB
JavaScript

// TODO: add more rekey tests that at least include switching from no
// compression to compression and vice versa
'use strict'
const assert = require('./assert-compat.js')
const { spawn, spawnSync } = require('child_process')
const { chmodSync, readdirSync } = require('fs')
const { join } = require('path')
const readline = require('readline')
const Server = require('../lib/server.js')
const { fixture, fixtureKey, FIXTURES_DIR, mustCall, mustCallAtLeast } = require('./common.js')
const SPAWN_OPTS = { windowsHide: true }
const CLIENT_TIMEOUT = 5000
const debug = false
const opensshPath = 'ssh'
let opensshVer
// TODO: figure out why this test is failing on Windows
if (process.platform === 'win32') {
console.log('Skipping OpenSSH integration tests on Windows')
process.exit(0)
}
// Fix file modes to avoid OpenSSH client complaints about keys' permissions
for (const file of readdirSync(FIXTURES_DIR, { withFileTypes: true })) {
if (file.isFile()) chmodSync(join(FIXTURES_DIR, file.name), 0o600)
}
{
// Get OpenSSH client version first
const { error, stderr, stdout } = spawnSync(opensshPath, ['-V'], SPAWN_OPTS)
if (error) {
console.error('OpenSSH client is required for these tests')
process.exitCode = 5
return
}
const re = /^OpenSSH_([\d.]+)/
let m = re.exec(stdout.toString())
if (!m || !m[1]) {
m = re.exec(stderr.toString())
if (!m || !m[1]) {
console.error('OpenSSH client is required for these tests')
process.exitCode = 5
return
}
}
opensshVer = m[1]
console.log(`Testing with OpenSSH version: ${opensshVer}`)
}
// Key-based authentication
;[
{ desc: 'RSA user key (old OpenSSH)', clientKey: fixtureKey('id_rsa') },
{ desc: 'RSA user key (new OpenSSH)', clientKey: fixtureKey('openssh_new_rsa') },
{ desc: 'DSA user key', clientKey: fixtureKey('id_dsa') },
{ desc: 'ECDSA user key', clientKey: fixtureKey('id_ecdsa') }
].forEach((test) => {
const { desc, clientKey } = test
const username = 'KeyUser'
const { server } = setup(desc, {
client: {
username,
privateKeyPath: clientKey.fullPath
},
server: { hostKeys: [fixture('ssh_host_rsa_key')] },
debug
})
server.on(
'connection',
mustCall((conn) => {
let authAttempt = 0
conn
.on(
'authentication',
mustCallAtLeast((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:
case 3:
if (authAttempt === 3) assert(ctx.signature, 'Missing publickey signature')
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
default:
assert(false, 'Unexpected number of auth attempts')
}
if (ctx.signature) {
const result = clientKey.key.verify(ctx.blob, ctx.signature, ctx.hashAlgo)
assert(result === true, 'Could not verify publickey signature')
// We should not expect any further auth attempts after we verify a
// signature
authAttempt = Infinity
}
ctx.accept()
}, 2)
)
.on(
'ready',
mustCall(() => {
conn.on(
'session',
mustCall((accept, reject) => {
accept().on(
'exec',
mustCall((accept, reject) => {
const stream = accept()
stream.exit(0)
stream.end()
})
)
})
)
})
)
})
)
})
// Different host key types
;[
{ desc: 'RSA host key (old OpenSSH)', hostKey: fixture('id_rsa') },
{ desc: 'RSA host key (new OpenSSH)', hostKey: fixture('openssh_new_rsa') },
{ desc: 'DSA host key', hostKey: fixture('ssh_host_dsa_key') },
{ desc: 'ECDSA host key', hostKey: fixture('ssh_host_ecdsa_key') },
{ desc: 'PPK', hostKey: fixture('id_rsa.ppk') }
].forEach((test) => {
const { desc, hostKey } = test
const clientKey = fixtureKey('openssh_new_rsa')
const username = 'KeyUser'
const { server } = setup(desc, {
client: {
username,
privateKeyPath: clientKey.fullPath
},
server: { hostKeys: [hostKey] },
debug
})
server.on(
'connection',
mustCall((conn) => {
let authAttempt = 0
conn
.on(
'authentication',
mustCallAtLeast((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:
case 3:
if (authAttempt === 3) assert(ctx.signature, 'Missing publickey signature')
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
default:
assert(false, 'Unexpected number of auth attempts')
}
if (ctx.signature) {
const result = clientKey.key.verify(ctx.blob, ctx.signature, ctx.hashAlgo)
assert(result === true, 'Could not verify publickey signature')
// We should not expect any further auth attempts after we verify a
// signature
authAttempt = Infinity
}
ctx.accept()
}, 2)
)
.on(
'ready',
mustCall(() => {
conn.on(
'session',
mustCall((accept, reject) => {
accept().on(
'exec',
mustCall((accept, reject) => {
const stream = accept()
stream.exit(0)
stream.end()
})
)
})
)
})
)
})
)
})
// Various edge cases
{
const clientKey = fixtureKey('openssh_new_rsa')
const username = 'KeyUser'
const { server } = setup('Server closes stdin too early', {
client: {
username,
privateKeyPath: clientKey.fullPath
},
server: { hostKeys: [fixture('ssh_host_rsa_key')] },
debug
})
server
.on(
'_child',
mustCall((childProc) => {
childProc.stderr.once(
'data',
mustCall((data) => {
childProc.stdin.end()
})
)
childProc.stdin.write('ping')
})
)
.on(
'connection',
mustCall((conn) => {
let authAttempt = 0
conn
.on(
'authentication',
mustCallAtLeast((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:
case 3:
if (authAttempt === 3) assert(ctx.signature, 'Missing publickey signature')
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
default:
assert(false, 'Unexpected number of auth attempts')
}
if (ctx.signature) {
const result = clientKey.key.verify(ctx.blob, ctx.signature, ctx.hashAlgo)
assert(result === true, 'Could not verify publickey signature')
// We should not expect any further auth attempts after we verify a
// signature
authAttempt = Infinity
}
ctx.accept()
}, 2)
)
.on(
'ready',
mustCall(() => {
conn.on(
'session',
mustCall((accept, reject) => {
accept().on(
'exec',
mustCall((accept, reject) => {
const stream = accept()
stream.stdin
.on(
'data',
mustCallAtLeast((data) => {
stream.stdout.write('pong on stdout')
stream.stderr.write('pong on stderr')
})
)
.on(
'end',
mustCall(() => {
stream.stdout.write('pong on stdout')
stream.stderr.write('pong on stderr')
stream.exit(0)
stream.close()
})
)
})
)
})
)
})
)
})
)
}
{
const clientKey = fixtureKey('openssh_new_rsa')
const username = 'KeyUser'
const { server } = setup('Rekey', {
client: {
username,
privateKeyPath: clientKey.fullPath
},
server: { hostKeys: [fixture('ssh_host_rsa_key')] },
debug
})
server.on(
'connection',
mustCall((conn) => {
let authAttempt = 0
conn
.on(
'authentication',
mustCallAtLeast((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:
case 3:
if (authAttempt === 3) assert(ctx.signature, 'Missing publickey signature')
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
default:
assert(false, 'Unexpected number of auth attempts')
}
if (ctx.signature) {
const result = clientKey.key.verify(ctx.blob, ctx.signature, ctx.hashAlgo)
assert(result === true, 'Could not verify publickey signature')
// We should not expect any further auth attempts after we verify a
// signature
authAttempt = Infinity
}
ctx.accept()
}, 2)
)
.on(
'ready',
mustCall(() => {
conn.on(
'session',
mustCall((accept, reject) => {
const session = accept()
conn.rekey()
session.on(
'exec',
mustCall((accept, reject) => {
const stream = accept()
stream.exit(0)
stream.end()
})
)
})
)
})
)
})
)
}
function setup(title, configs) {
const {
client: clientCfg,
server: serverCfg,
allReady: allReady_,
timeout: timeout_,
debug,
noForceServerReady
} = configs
let clientClose = false
let serverClose = false
let serverReady = false
let client
const msg = (text) => {
return `${title}: ${text}`
}
const timeout = typeof timeout_ === 'number' ? timeout_ : CLIENT_TIMEOUT
const allReady = typeof allReady_ === 'function' ? allReady_ : undefined
if (debug) {
serverCfg.debug = (...args) => {
console.log(`[${title}][SERVER]`, ...args)
}
}
const serverReadyFn = noForceServerReady ? onReady : mustCall(onReady)
const server = new Server(serverCfg)
server
.on('error', onError)
.on(
'connection',
mustCall((conn) => {
conn.on('error', onError).on('ready', serverReadyFn)
server.close()
})
)
.on('close', mustCall(onClose))
function onError(err) {
const which = arguments.length >= 3 ? 'client' : 'server'
assert(false, msg(`Unexpected ${which} error: ${err}`))
}
function onReady() {
assert(!serverReady, msg('Received multiple ready events for server'))
serverReady = true
allReady && allReady()
}
function onClose() {
if (arguments.length >= 3) {
assert(!clientClose, msg('Received multiple close events for client'))
clientClose = true
} else {
assert(!serverClose, msg('Received multiple close events for server'))
serverClose = true
}
}
process.nextTick(
mustCall(() => {
server.listen(
0,
'localhost',
mustCall(() => {
const args = [
'-o',
'UserKnownHostsFile=/dev/null',
'-o',
'StrictHostKeyChecking=no',
'-o',
'CheckHostIP=no',
'-o',
'ConnectTimeout=3',
'-o',
'GlobalKnownHostsFile=/dev/null',
'-o',
'GSSAPIAuthentication=no',
'-o',
'IdentitiesOnly=yes',
'-o',
'BatchMode=yes',
'-o',
'VerifyHostKeyDNS=no',
'-vvvvvv',
'-T',
'-o',
'KbdInteractiveAuthentication=no',
'-o',
'HostbasedAuthentication=no',
'-o',
'PasswordAuthentication=no',
'-o',
'PubkeyAuthentication=yes',
'-o',
'PreferredAuthentications=publickey'
]
if (clientCfg.privateKeyPath) args.push('-o', `IdentityFile=${clientCfg.privateKeyPath}`)
if (!/^[0-6]\./.test(opensshVer)) {
// OpenSSH 7.0+ disables DSS/DSA host (and user) key support by
// default, so we explicitly enable it here
args.push('-o', 'HostKeyAlgorithms=+ssh-dss')
args.push('-o', 'PubkeyAcceptedKeyTypes=+ssh-dss')
args.push('-o', 'PubkeyAcceptedAlgorithms=+ssh-dss')
}
args.push(
'-p',
server.address().port.toString(),
'-l',
clientCfg.username,
'localhost',
'uptime'
)
client = spawn(opensshPath, args, SPAWN_OPTS)
server.emit('_child', client)
if (debug) {
readline
.createInterface({
input: client.stdout
})
.on('line', (line) => {
console.log(`[${title}][CLIENT][STDOUT]`, line)
})
readline
.createInterface({
input: client.stderr
})
.on('line', (line) => {
console.error(`[${title}][CLIENT][STDERR]`, line)
})
} else {
client.stdout.resume()
client.stderr.resume()
}
client
.on('error', (err) => {
onError(err, null, null)
})
.on('exit', (code) => {
clearTimeout(client.timer)
if (code !== 0) return onError(new Error(`Non-zero exit code ${code}`), null, null)
onClose(null, null, null)
})
client.timer = setTimeout(() => {
assert(false, msg('Client timeout'))
}, timeout)
})
)
})
)
return { server }
}