1 | 'use strict';
|
2 |
|
3 | const assert = require('assert');
|
4 | const { inspect } = require('util');
|
5 |
|
6 | const {
|
7 | fixtureKey,
|
8 | mustCall,
|
9 | mustNotCall,
|
10 | setup,
|
11 | } = require('./common.js');
|
12 |
|
13 | const serverCfg = { hostKeys: [ fixtureKey('ssh_host_rsa_key').raw ] };
|
14 |
|
15 | const debug = false;
|
16 |
|
17 |
|
18 | [
|
19 | { desc: 'RSA (old OpenSSH)',
|
20 | clientKey: fixtureKey('id_rsa') },
|
21 | { desc: 'RSA (new OpenSSH)',
|
22 | clientKey: fixtureKey('openssh_new_rsa') },
|
23 | { desc: 'RSA (encrypted)',
|
24 | clientKey: fixtureKey('id_rsa_enc', 'foobarbaz'),
|
25 | passphrase: 'foobarbaz' },
|
26 | { desc: 'DSA',
|
27 | clientKey: fixtureKey('id_dsa') },
|
28 | { desc: 'ECDSA',
|
29 | clientKey: fixtureKey('id_ecdsa') },
|
30 | { desc: 'PPK',
|
31 | clientKey: fixtureKey('id_rsa.ppk') },
|
32 | ].forEach((test) => {
|
33 | const { desc, clientKey, passphrase } = test;
|
34 | const username = 'Key User';
|
35 | const { server } = setup(
|
36 | desc,
|
37 | {
|
38 | client: { username, privateKey: clientKey.raw, passphrase },
|
39 | server: serverCfg,
|
40 |
|
41 | debug,
|
42 | }
|
43 | );
|
44 |
|
45 | server.on('connection', mustCall((conn) => {
|
46 | let authAttempt = 0;
|
47 | conn.on('authentication', mustCall((ctx) => {
|
48 | assert(ctx.username === username,
|
49 | `Wrong username: ${ctx.username}`);
|
50 | switch (++authAttempt) {
|
51 | case 1:
|
52 | assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`);
|
53 | return ctx.reject();
|
54 | case 3:
|
55 | assert(ctx.signature, 'Missing publickey signature');
|
56 |
|
57 | case 2:
|
58 | assert(ctx.method === 'publickey',
|
59 | `Wrong auth method: ${ctx.method}`);
|
60 | assert(ctx.key.algo === clientKey.key.type,
|
61 | `Wrong key algo: ${ctx.key.algo}`);
|
62 | assert.deepStrictEqual(clientKey.key.getPublicSSH(),
|
63 | ctx.key.data,
|
64 | 'Public key mismatch');
|
65 | break;
|
66 | }
|
67 | if (ctx.signature) {
|
68 | assert(clientKey.key.verify(ctx.blob, ctx.signature) === true,
|
69 | 'Could not verify publickey signature');
|
70 | }
|
71 | ctx.accept();
|
72 | }, 3)).on('ready', mustCall(() => {
|
73 | conn.end();
|
74 | }));
|
75 | }));
|
76 | });
|
77 |
|
78 |
|
79 |
|
80 | {
|
81 | const username = 'Password User';
|
82 | const password = 'hi mom';
|
83 | const { server } = setup(
|
84 | 'Password',
|
85 | {
|
86 | client: { username, password },
|
87 | server: serverCfg,
|
88 |
|
89 | debug,
|
90 | }
|
91 | );
|
92 |
|
93 | server.on('connection', mustCall((conn) => {
|
94 | let authAttempt = 0;
|
95 | conn.on('authentication', mustCall((ctx) => {
|
96 | assert(ctx.username === username,
|
97 | `Wrong username: ${ctx.username}`);
|
98 | if (++authAttempt === 1) {
|
99 | assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`);
|
100 | return ctx.reject();
|
101 | }
|
102 | assert(ctx.method === 'password',
|
103 | `Wrong auth method: ${ctx.method}`);
|
104 | assert(ctx.password === password,
|
105 | `Wrong password: ${ctx.password}`);
|
106 | ctx.accept();
|
107 | }, 2)).on('ready', mustCall(() => {
|
108 | conn.end();
|
109 | }));
|
110 | }));
|
111 | }
|
112 | {
|
113 | const username = '';
|
114 | const password = 'hi mom';
|
115 | const { server } = setup(
|
116 | 'Password (empty username)',
|
117 | {
|
118 | client: { username, password },
|
119 | server: serverCfg,
|
120 |
|
121 | debug,
|
122 | }
|
123 | );
|
124 |
|
125 | server.on('connection', mustCall((conn) => {
|
126 | let authAttempt = 0;
|
127 | conn.on('authentication', mustCall((ctx) => {
|
128 | assert(ctx.username === username,
|
129 | `Wrong username: ${ctx.username}`);
|
130 | if (++authAttempt === 1) {
|
131 | assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`);
|
132 | return ctx.reject();
|
133 | }
|
134 | assert(ctx.method === 'password',
|
135 | `Wrong auth method: ${ctx.method}`);
|
136 | assert(ctx.password === password,
|
137 | `Wrong password: ${ctx.password}`);
|
138 | ctx.accept();
|
139 | }, 2)).on('ready', mustCall(() => {
|
140 | conn.end();
|
141 | }));
|
142 | }));
|
143 | }
|
144 | {
|
145 | const username = 'foo';
|
146 | const oldPassword = 'bar';
|
147 | const newPassword = 'baz';
|
148 | const changePrompt = 'Prithee changeth thy password';
|
149 | const { client, server } = setup(
|
150 | 'Password (change requested)',
|
151 | {
|
152 | client: { username, password: oldPassword },
|
153 | server: serverCfg,
|
154 |
|
155 | debug,
|
156 | }
|
157 | );
|
158 |
|
159 | server.on('connection', mustCall((conn) => {
|
160 | let authAttempt = 0;
|
161 | conn.on('authentication', mustCall((ctx) => {
|
162 | assert(ctx.username === username,
|
163 | `Wrong username: ${ctx.username}`);
|
164 | if (++authAttempt === 1) {
|
165 | assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`);
|
166 | return ctx.reject();
|
167 | }
|
168 | assert(ctx.method === 'password',
|
169 | `Wrong auth method: ${ctx.method}`);
|
170 | assert(ctx.password === oldPassword,
|
171 | `Wrong old password: ${ctx.password}`);
|
172 | ctx.requestChange(changePrompt, mustCall((newPassword_) => {
|
173 | assert(newPassword_ === newPassword,
|
174 | `Wrong new password: ${newPassword_}`);
|
175 | ctx.accept();
|
176 | }));
|
177 | }, 2)).on('ready', mustCall(() => {
|
178 | conn.end();
|
179 | }));
|
180 | }));
|
181 |
|
182 | client.on('change password', mustCall((prompt, done) => {
|
183 | assert(prompt === changePrompt, `Wrong password change prompt: ${prompt}`);
|
184 | process.nextTick(done, newPassword);
|
185 | }));
|
186 | }
|
187 |
|
188 |
|
189 |
|
190 | {
|
191 | const localUsername = 'Local User Foo';
|
192 | const localHostname = 'Local Host Bar';
|
193 | const username = 'Hostbased User';
|
194 | const clientKey = fixtureKey('id_rsa');
|
195 | const { server } = setup(
|
196 | 'Hostbased',
|
197 | {
|
198 | client: {
|
199 | username,
|
200 | privateKey: clientKey.raw,
|
201 | localUsername,
|
202 | localHostname,
|
203 | },
|
204 | server: serverCfg,
|
205 |
|
206 | debug,
|
207 | }
|
208 | );
|
209 |
|
210 | server.on('connection', mustCall((conn) => {
|
211 | let authAttempt = 0;
|
212 | conn.on('authentication', mustCall((ctx) => {
|
213 | assert(ctx.username === username,
|
214 | `Wrong username: ${ctx.username}`);
|
215 | switch (++authAttempt) {
|
216 | case 1:
|
217 | assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`);
|
218 | return ctx.reject();
|
219 | case 2:
|
220 | assert(ctx.method === 'publickey',
|
221 | `Wrong auth method: ${ctx.method}`);
|
222 | return ctx.reject();
|
223 | case 3:
|
224 | assert(ctx.method === 'hostbased',
|
225 | `Wrong auth method: ${ctx.method}`);
|
226 | assert(ctx.key.algo === clientKey.key.type,
|
227 | `Wrong key algo: ${ctx.key.algo}`);
|
228 | assert.deepStrictEqual(clientKey.key.getPublicSSH(),
|
229 | ctx.key.data,
|
230 | 'Public key mismatch');
|
231 | assert(ctx.signature, 'Expected signature');
|
232 | assert(ctx.localHostname === localHostname, 'Wrong local hostname');
|
233 | assert(ctx.localUsername === localUsername, 'Wrong local username');
|
234 | assert(clientKey.key.verify(ctx.blob, ctx.signature) === true,
|
235 | 'Could not verify hostbased signature');
|
236 |
|
237 | break;
|
238 | }
|
239 | ctx.accept();
|
240 | }, 3)).on('ready', mustCall(() => {
|
241 | conn.end();
|
242 | }));
|
243 | }));
|
244 | }
|
245 |
|
246 |
|
247 |
|
248 | {
|
249 | const username = 'Keyboard-Interactive User';
|
250 | const request = {
|
251 | name: 'SSH2 Authentication',
|
252 | instructions: 'These are instructions',
|
253 | prompts: [
|
254 | { prompt: 'Password: ', echo: false },
|
255 | { prompt: 'Is the cake a lie? ', echo: true },
|
256 | ],
|
257 | };
|
258 | const responses = [
|
259 | 'foobarbaz',
|
260 | 'yes',
|
261 | ];
|
262 | const { client, server } = setup(
|
263 | 'Password (empty username)',
|
264 | {
|
265 | client: {
|
266 | username,
|
267 | tryKeyboard: true,
|
268 | },
|
269 | server: serverCfg,
|
270 |
|
271 | debug,
|
272 | }
|
273 | );
|
274 |
|
275 | server.on('connection', mustCall((conn) => {
|
276 | let authAttempt = 0;
|
277 | conn.on('authentication', mustCall((ctx) => {
|
278 | assert(ctx.username === username,
|
279 | `Wrong username: ${ctx.username}`);
|
280 | if (++authAttempt === 1) {
|
281 | assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`);
|
282 | return ctx.reject();
|
283 | }
|
284 | assert(ctx.method === 'keyboard-interactive',
|
285 | `Wrong auth method: ${ctx.method}`);
|
286 | ctx.prompt(request.prompts,
|
287 | request.name,
|
288 | request.instructions,
|
289 | mustCall((responses_) => {
|
290 | assert.deepStrictEqual(responses_, responses);
|
291 | ctx.accept();
|
292 | }));
|
293 | }, 2)).on('ready', mustCall(() => {
|
294 | conn.end();
|
295 | }));
|
296 | }));
|
297 |
|
298 | client.on('keyboard-interactive',
|
299 | mustCall((name, instructions, lang, prompts, finish) => {
|
300 | assert(name === request.name, `Wrong prompt name: ${name}`);
|
301 | assert(instructions === request.instructions,
|
302 | `Wrong prompt instructions: ${instructions}`);
|
303 | assert.deepStrictEqual(
|
304 | prompts,
|
305 | request.prompts,
|
306 | `Wrong prompts: ${inspect(prompts)}`
|
307 | );
|
308 | process.nextTick(finish, responses);
|
309 | }));
|
310 | }
|
311 |
|
312 |
|
313 | {
|
314 | const username = 'foo';
|
315 | const password = '1234';
|
316 | const clientKey = fixtureKey('id_rsa');
|
317 | const { server } = setup(
|
318 | 'authHandler() (sync)',
|
319 | {
|
320 | client: {
|
321 | username,
|
322 | password,
|
323 | privateKey: clientKey.raw,
|
324 |
|
325 | authHandler: mustCall((methodsLeft, partial, cb) => {
|
326 | assert(methodsLeft === null, 'expected null methodsLeft');
|
327 | assert(partial === null, 'expected null partial');
|
328 | return 'none';
|
329 | }),
|
330 | },
|
331 | server: serverCfg,
|
332 |
|
333 | debug,
|
334 | }
|
335 | );
|
336 |
|
337 | server.on('connection', mustCall((conn) => {
|
338 | conn.on('authentication', mustCall((ctx) => {
|
339 | assert(ctx.username === username, `Wrong username: ${ctx.username}`);
|
340 | assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`);
|
341 | ctx.accept();
|
342 | })).on('ready', mustCall(() => {
|
343 | conn.end();
|
344 | }));
|
345 | }));
|
346 | }
|
347 | {
|
348 | const username = 'foo';
|
349 | const password = '1234';
|
350 | const clientKey = fixtureKey('id_rsa');
|
351 | const { server } = setup(
|
352 | 'authHandler() (async)',
|
353 | {
|
354 | client: {
|
355 | username,
|
356 | password,
|
357 | privateKey: clientKey.raw,
|
358 |
|
359 | authHandler: mustCall((methodsLeft, partial, cb) => {
|
360 | assert(methodsLeft === null, 'expected null methodsLeft');
|
361 | assert(partial === null, 'expected null partial');
|
362 | process.nextTick(mustCall(cb), 'none');
|
363 | }),
|
364 | },
|
365 | server: serverCfg,
|
366 |
|
367 | debug,
|
368 | }
|
369 | );
|
370 |
|
371 | server.on('connection', mustCall((conn) => {
|
372 | conn.on('authentication', mustCall((ctx) => {
|
373 | assert(ctx.username === username, `Wrong username: ${ctx.username}`);
|
374 | assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`);
|
375 | ctx.accept();
|
376 | })).on('ready', mustCall(() => {
|
377 | conn.end();
|
378 | }));
|
379 | }));
|
380 | }
|
381 | {
|
382 | const username = 'foo';
|
383 | const password = '1234';
|
384 | const clientKey = fixtureKey('id_rsa');
|
385 | const { client, server } = setup(
|
386 | 'authHandler() (no methods left -- sync)',
|
387 | {
|
388 | client: {
|
389 | username,
|
390 | password,
|
391 | privateKey: clientKey.raw,
|
392 |
|
393 | authHandler: mustCall((methodsLeft, partial, cb) => {
|
394 | assert(methodsLeft === null, 'expected null methodsLeft');
|
395 | assert(partial === null, 'expected null partial');
|
396 | return false;
|
397 | }),
|
398 | },
|
399 | server: serverCfg,
|
400 |
|
401 | debug,
|
402 | noForceClientReady: true,
|
403 | noForceServerReady: true,
|
404 | }
|
405 | );
|
406 |
|
407 |
|
408 |
|
409 | client.removeAllListeners('error');
|
410 |
|
411 | client.on('error', mustCall((err) => {
|
412 | assert.strictEqual(err.level, 'client-authentication');
|
413 | assert(/configured authentication methods failed/i.test(err.message),
|
414 | 'Wrong error message');
|
415 | }));
|
416 |
|
417 | server.on('connection', mustCall((conn) => {
|
418 | conn.on('authentication', mustNotCall())
|
419 | .on('ready', mustNotCall());
|
420 | }));
|
421 | }
|
422 | {
|
423 | const username = 'foo';
|
424 | const password = '1234';
|
425 | const clientKey = fixtureKey('id_rsa');
|
426 | const { client, server } = setup(
|
427 | 'authHandler() (no methods left -- async)',
|
428 | {
|
429 | client: {
|
430 | username,
|
431 | password,
|
432 | privateKey: clientKey.raw,
|
433 |
|
434 | authHandler: mustCall((methodsLeft, partial, cb) => {
|
435 | assert(methodsLeft === null, 'expected null methodsLeft');
|
436 | assert(partial === null, 'expected null partial');
|
437 | process.nextTick(mustCall(cb), false);
|
438 | }),
|
439 | },
|
440 | server: serverCfg,
|
441 |
|
442 | debug,
|
443 | noForceClientReady: true,
|
444 | noForceServerReady: true,
|
445 | }
|
446 | );
|
447 |
|
448 |
|
449 |
|
450 | client.removeAllListeners('error');
|
451 |
|
452 | client.on('error', mustCall((err) => {
|
453 | assert.strictEqual(err.level, 'client-authentication');
|
454 | assert(/configured authentication methods failed/i.test(err.message),
|
455 | 'Wrong error message');
|
456 | }));
|
457 |
|
458 | server.on('connection', mustCall((conn) => {
|
459 | conn.on('authentication', mustNotCall())
|
460 | .on('ready', mustNotCall());
|
461 | }));
|
462 | }
|
463 | {
|
464 | const username = 'foo';
|
465 | const password = '1234';
|
466 | const clientKey = fixtureKey('id_rsa');
|
467 | const events = [];
|
468 | const expectedEvents = [
|
469 | 'client', 'server', 'client', 'server'
|
470 | ];
|
471 | let clientCalls = 0;
|
472 | const { client, server } = setup(
|
473 | 'authHandler() (multi-step)',
|
474 | {
|
475 | client: {
|
476 | username,
|
477 | password,
|
478 | privateKey: clientKey.raw,
|
479 |
|
480 | authHandler: mustCall((methodsLeft, partial, cb) => {
|
481 | events.push('client');
|
482 | switch (++clientCalls) {
|
483 | case 1:
|
484 | assert(methodsLeft === null, 'expected null methodsLeft');
|
485 | assert(partial === null, 'expected null partial');
|
486 | return 'publickey';
|
487 | case 2:
|
488 | assert.deepStrictEqual(
|
489 | methodsLeft,
|
490 | ['password'],
|
491 | `expected 'password' method left, saw: ${methodsLeft}`
|
492 | );
|
493 | assert(partial === true, 'expected partial success');
|
494 | return 'password';
|
495 | }
|
496 | }, 2),
|
497 | },
|
498 | server: serverCfg,
|
499 |
|
500 | debug,
|
501 | }
|
502 | );
|
503 |
|
504 | server.on('connection', mustCall((conn) => {
|
505 | let attempts = 0;
|
506 | conn.on('authentication', mustCall((ctx) => {
|
507 | assert(++attempts === clientCalls, 'server<->client state mismatch');
|
508 | assert(ctx.username === username,
|
509 | `Unexpected username: ${ctx.username}`);
|
510 | events.push('server');
|
511 | switch (attempts) {
|
512 | case 1:
|
513 | assert(ctx.method === 'publickey',
|
514 | `Wrong auth method: ${ctx.method}`);
|
515 | assert(ctx.key.algo === clientKey.key.type,
|
516 | `Unexpected key algo: ${ctx.key.algo}`);
|
517 | assert.deepEqual(clientKey.key.getPublicSSH(),
|
518 | ctx.key.data,
|
519 | 'Public key mismatch');
|
520 | ctx.reject(['password'], true);
|
521 | break;
|
522 | case 2:
|
523 | assert(ctx.method === 'password',
|
524 | `Wrong auth method: ${ctx.method}`);
|
525 | assert(ctx.password === password,
|
526 | `Unexpected password: ${ctx.password}`);
|
527 | ctx.accept();
|
528 | break;
|
529 | }
|
530 | }, 2)).on('ready', mustCall(() => {
|
531 | conn.end();
|
532 | }));
|
533 | }));
|
534 |
|
535 | client.on('close', mustCall(() => {
|
536 | assert.deepStrictEqual(events, expectedEvents);
|
537 | }));
|
538 | }
|
539 | {
|
540 | const username = 'foo';
|
541 | const password = '1234';
|
542 | const { server } = setup(
|
543 | 'authHandler() (custom auth configuration)',
|
544 | {
|
545 | client: {
|
546 | username: 'bar',
|
547 | password: '5678',
|
548 |
|
549 | authHandler: mustCall((methodsLeft, partial, cb) => {
|
550 | assert(methodsLeft === null, 'expected null methodsLeft');
|
551 | assert(partial === null, 'expected null partial');
|
552 | return {
|
553 | type: 'password',
|
554 | username,
|
555 | password,
|
556 | };
|
557 | }),
|
558 | },
|
559 | server: serverCfg,
|
560 |
|
561 | debug,
|
562 | }
|
563 | );
|
564 |
|
565 | server.on('connection', mustCall((conn) => {
|
566 | conn.on('authentication', mustCall((ctx) => {
|
567 | assert(ctx.username === username, `Wrong username: ${ctx.username}`);
|
568 | assert(ctx.method === 'password', `Wrong auth method: ${ctx.method}`);
|
569 | assert(ctx.password === password, `Unexpected password: ${ctx.password}`);
|
570 | ctx.accept();
|
571 | })).on('ready', mustCall(() => {
|
572 | conn.end();
|
573 | }));
|
574 | }));
|
575 | }
|
576 | {
|
577 | const username = 'foo';
|
578 | const password = '1234';
|
579 | const { server } = setup(
|
580 | 'authHandler() (simple construction with custom auth configuration)',
|
581 | {
|
582 | client: {
|
583 | username: 'bar',
|
584 | password: '5678',
|
585 |
|
586 | authHandler: [{
|
587 | type: 'password',
|
588 | username,
|
589 | password,
|
590 | }],
|
591 | },
|
592 | server: serverCfg,
|
593 |
|
594 | debug,
|
595 | }
|
596 | );
|
597 |
|
598 | server.on('connection', mustCall((conn) => {
|
599 | conn.on('authentication', mustCall((ctx) => {
|
600 | assert(ctx.username === username, `Wrong username: ${ctx.username}`);
|
601 | assert(ctx.method === 'password', `Wrong auth method: ${ctx.method}`);
|
602 | assert(ctx.password === password, `Unexpected password: ${ctx.password}`);
|
603 | ctx.accept();
|
604 | })).on('ready', mustCall(() => {
|
605 | conn.end();
|
606 | }));
|
607 | }));
|
608 | }
|