'use strict'; const assert = require('assert'); 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) { assert(clientKey.key.verify(ctx.blob, ctx.signature) === 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'); assert(clientKey.key.verify(ctx.blob, ctx.signature) === 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(); })); })); }