123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481 |
- // TODO: add more rekey tests that at least include switching from no
- // compression to compression and vice versa
- 'use strict';
- const assert = require('assert');
- 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) {
- assert(clientKey.key.verify(ctx.blob, ctx.signature) === 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) {
- assert(clientKey.key.verify(ctx.blob, ctx.signature) === 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) {
- assert(clientKey.key.verify(ctx.blob, ctx.signature) === 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) {
- assert(clientKey.key.verify(ctx.blob, ctx.signature) === 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('-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 };
- }
|