UNPKG

13.4 kBJavaScriptView Raw
1// TODO: add more rekey tests that at least include switching from no
2// compression to compression and vice versa
3'use strict';
4
5const assert = require('assert');
6const { spawn, spawnSync } = require('child_process');
7const { chmodSync, readdirSync } = require('fs');
8const { join } = require('path');
9const readline = require('readline');
10
11const Server = require('../lib/server.js');
12
13const {
14 fixture,
15 fixtureKey,
16 FIXTURES_DIR,
17 mustCall,
18 mustCallAtLeast,
19} = require('./common.js');
20
21const SPAWN_OPTS = { windowsHide: true };
22const CLIENT_TIMEOUT = 5000;
23
24const debug = false;
25const opensshPath = 'ssh';
26let opensshVer;
27
28// TODO: figure out why this test is failing on Windows
29if (process.platform === 'win32') {
30 console.log('Skipping OpenSSH integration tests on Windows');
31 process.exit(0);
32}
33
34// Fix file modes to avoid OpenSSH client complaints about keys' permissions
35for (const file of readdirSync(FIXTURES_DIR, { withFileTypes: true })) {
36 if (file.isFile())
37 chmodSync(join(FIXTURES_DIR, file.name), 0o600);
38}
39
40{
41 // Get OpenSSH client version first
42 const {
43 error, stderr, stdout
44 } = spawnSync(opensshPath, ['-V'], SPAWN_OPTS);
45
46 if (error) {
47 console.error('OpenSSH client is required for these tests');
48 process.exitCode = 5;
49 return;
50 }
51
52 const re = /^OpenSSH_([\d.]+)/;
53 let m = re.exec(stdout.toString());
54 if (!m || !m[1]) {
55 m = re.exec(stderr.toString());
56 if (!m || !m[1]) {
57 console.error('OpenSSH client is required for these tests');
58 process.exitCode = 5;
59 return;
60 }
61 }
62
63 opensshVer = m[1];
64 console.log(`Testing with OpenSSH version: ${opensshVer}`);
65}
66
67
68// Key-based authentication
69[
70 { desc: 'RSA user key (old OpenSSH)',
71 clientKey: fixtureKey('id_rsa') },
72 { desc: 'RSA user key (new OpenSSH)',
73 clientKey: fixtureKey('openssh_new_rsa') },
74 { desc: 'DSA user key',
75 clientKey: fixtureKey('id_dsa') },
76 { desc: 'ECDSA user key',
77 clientKey: fixtureKey('id_ecdsa') },
78].forEach((test) => {
79 const { desc, clientKey } = test;
80 const username = 'KeyUser';
81 const { server } = setup(
82 desc,
83 {
84 client: {
85 username,
86 privateKeyPath: clientKey.fullPath,
87 },
88 server: { hostKeys: [ fixture('ssh_host_rsa_key') ] },
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 switch (++authAttempt) {
99 case 1:
100 assert(ctx.method === 'none',
101 `Wrong auth method: ${ctx.method}`);
102 return ctx.reject();
103 case 2:
104 assert(ctx.signature, 'Missing publickey signature');
105 assert(ctx.method === 'publickey',
106 `Wrong auth method: ${ctx.method}`);
107 assert(ctx.key.algo === clientKey.key.type,
108 `Wrong key algo: ${ctx.key.algo}`);
109 assert.deepStrictEqual(clientKey.key.getPublicSSH(),
110 ctx.key.data,
111 'Public key mismatch');
112 break;
113 }
114 if (ctx.signature) {
115 assert(clientKey.key.verify(ctx.blob, ctx.signature) === true,
116 'Could not verify publickey signature');
117 }
118 ctx.accept();
119 }, 2)).on('ready', mustCall(() => {
120 conn.on('session', mustCall((accept, reject) => {
121 accept().on('exec', mustCall((accept, reject) => {
122 const stream = accept();
123 stream.exit(0);
124 stream.end();
125 }));
126 }));
127 }));
128 }));
129});
130
131
132// Different host key types
133[
134 { desc: 'RSA host key (old OpenSSH)',
135 hostKey: fixture('id_rsa') },
136 { desc: 'RSA host key (new OpenSSH)',
137 hostKey: fixture('openssh_new_rsa') },
138 { desc: 'DSA host key',
139 hostKey: fixture('ssh_host_dsa_key') },
140 { desc: 'ECDSA host key',
141 hostKey: fixture('ssh_host_ecdsa_key') },
142 { desc: 'PPK',
143 hostKey: fixture('id_rsa.ppk') },
144].forEach((test) => {
145 const { desc, hostKey } = test;
146 const clientKey = fixtureKey('openssh_new_rsa');
147 const username = 'KeyUser';
148 const { server } = setup(
149 desc,
150 {
151 client: {
152 username,
153 privateKeyPath: clientKey.fullPath,
154 },
155 server: { hostKeys: [ hostKey ] },
156 debug,
157 }
158 );
159
160 server.on('connection', mustCall((conn) => {
161 let authAttempt = 0;
162 conn.on('authentication', mustCall((ctx) => {
163 assert(ctx.username === username,
164 `Wrong username: ${ctx.username}`);
165 switch (++authAttempt) {
166 case 1:
167 assert(ctx.method === 'none',
168 `Wrong auth method: ${ctx.method}`);
169 return ctx.reject();
170 case 2:
171 assert(ctx.signature, 'Missing publickey signature');
172 assert(ctx.method === 'publickey',
173 `Wrong auth method: ${ctx.method}`);
174 assert(ctx.key.algo === clientKey.key.type,
175 `Wrong key algo: ${ctx.key.algo}`);
176 assert.deepStrictEqual(clientKey.key.getPublicSSH(),
177 ctx.key.data,
178 'Public key mismatch');
179 break;
180 }
181 if (ctx.signature) {
182 assert(clientKey.key.verify(ctx.blob, ctx.signature) === true,
183 'Could not verify publickey signature');
184 }
185 ctx.accept();
186 }, 2)).on('ready', mustCall(() => {
187 conn.on('session', mustCall((accept, reject) => {
188 accept().on('exec', mustCall((accept, reject) => {
189 const stream = accept();
190 stream.exit(0);
191 stream.end();
192 }));
193 }));
194 }));
195 }));
196});
197
198
199// Various edge cases
200{
201 const clientKey = fixtureKey('openssh_new_rsa');
202 const username = 'KeyUser';
203 const { server } = setup(
204 'Server closes stdin too early',
205 {
206 client: {
207 username,
208 privateKeyPath: clientKey.fullPath,
209 },
210 server: { hostKeys: [ fixture('ssh_host_rsa_key') ] },
211 debug,
212 }
213 );
214
215 server.on('_child', mustCall((childProc) => {
216 childProc.stderr.once('data', mustCall((data) => {
217 childProc.stdin.end();
218 }));
219 childProc.stdin.write('ping');
220 })).on('connection', mustCall((conn) => {
221 let authAttempt = 0;
222 conn.on('authentication', mustCall((ctx) => {
223 assert(ctx.username === username,
224 `Wrong username: ${ctx.username}`);
225 switch (++authAttempt) {
226 case 1:
227 assert(ctx.method === 'none',
228 `Wrong auth method: ${ctx.method}`);
229 return ctx.reject();
230 case 2:
231 assert(ctx.signature, 'Missing publickey signature');
232 assert(ctx.method === 'publickey',
233 `Wrong auth method: ${ctx.method}`);
234 assert(ctx.key.algo === clientKey.key.type,
235 `Wrong key algo: ${ctx.key.algo}`);
236 assert.deepStrictEqual(clientKey.key.getPublicSSH(),
237 ctx.key.data,
238 'Public key mismatch');
239 break;
240 }
241 if (ctx.signature) {
242 assert(clientKey.key.verify(ctx.blob, ctx.signature) === true,
243 'Could not verify publickey signature');
244 }
245 ctx.accept();
246 }, 2)).on('ready', mustCall(() => {
247 conn.on('session', mustCall((accept, reject) => {
248 accept().on('exec', mustCall((accept, reject) => {
249 const stream = accept();
250 stream.stdin.on('data', mustCallAtLeast((data) => {
251 stream.stdout.write('pong on stdout');
252 stream.stderr.write('pong on stderr');
253 })).on('end', mustCall(() => {
254 stream.stdout.write('pong on stdout');
255 stream.stderr.write('pong on stderr');
256 stream.exit(0);
257 stream.close();
258 }));
259 }));
260 }));
261 }));
262 }));
263}
264{
265 const clientKey = fixtureKey('openssh_new_rsa');
266 const username = 'KeyUser';
267 const { server } = setup(
268 'Rekey',
269 {
270 client: {
271 username,
272 privateKeyPath: clientKey.fullPath,
273 },
274 server: { hostKeys: [ fixture('ssh_host_rsa_key') ] },
275 debug,
276 }
277 );
278
279 server.on('connection', mustCall((conn) => {
280 let authAttempt = 0;
281 conn.on('authentication', mustCall((ctx) => {
282 assert(ctx.username === username,
283 `Wrong username: ${ctx.username}`);
284 switch (++authAttempt) {
285 case 1:
286 assert(ctx.method === 'none',
287 `Wrong auth method: ${ctx.method}`);
288 return ctx.reject();
289 case 2:
290 assert(ctx.signature, 'Missing publickey signature');
291 assert(ctx.method === 'publickey',
292 `Wrong auth method: ${ctx.method}`);
293 assert(ctx.key.algo === clientKey.key.type,
294 `Wrong key algo: ${ctx.key.algo}`);
295 assert.deepStrictEqual(clientKey.key.getPublicSSH(),
296 ctx.key.data,
297 'Public key mismatch');
298 break;
299 }
300 if (ctx.signature) {
301 assert(clientKey.key.verify(ctx.blob, ctx.signature) === true,
302 'Could not verify publickey signature');
303 }
304 ctx.accept();
305 }, 2)).on('ready', mustCall(() => {
306 conn.on('session', mustCall((accept, reject) => {
307 const session = accept();
308 conn.rekey();
309 session.on('exec', mustCall((accept, reject) => {
310 const stream = accept();
311 stream.exit(0);
312 stream.end();
313 }));
314 }));
315 }));
316 }));
317}
318
319
320function setup(title, configs) {
321 const {
322 client: clientCfg,
323 server: serverCfg,
324 allReady: allReady_,
325 timeout: timeout_,
326 debug,
327 noForceServerReady,
328 } = configs;
329 let clientClose = false;
330 let serverClose = false;
331 let serverReady = false;
332 let client;
333 const msg = (text) => {
334 return `${title}: ${text}`;
335 };
336
337 const timeout = (typeof timeout_ === 'number'
338 ? timeout_
339 : CLIENT_TIMEOUT);
340
341 const allReady = (typeof allReady_ === 'function' ? allReady_ : undefined);
342
343 if (debug) {
344 serverCfg.debug = (...args) => {
345 console.log(`[${title}][SERVER]`, ...args);
346 };
347 }
348
349 const serverReadyFn = (noForceServerReady ? onReady : mustCall(onReady));
350 const server = new Server(serverCfg);
351
352 server.on('error', onError)
353 .on('connection', mustCall((conn) => {
354 conn.on('error', onError)
355 .on('ready', serverReadyFn);
356 server.close();
357 }))
358 .on('close', mustCall(onClose));
359
360 function onError(err) {
361 const which = (arguments.length >= 3 ? 'client' : 'server');
362 assert(false, msg(`Unexpected ${which} error: ${err}`));
363 }
364
365 function onReady() {
366 assert(!serverReady, msg('Received multiple ready events for server'));
367 serverReady = true;
368 allReady && allReady();
369 }
370
371 function onClose() {
372 if (arguments.length >= 3) {
373 assert(!clientClose, msg('Received multiple close events for client'));
374 clientClose = true;
375 } else {
376 assert(!serverClose, msg('Received multiple close events for server'));
377 serverClose = true;
378 }
379 }
380
381 process.nextTick(mustCall(() => {
382 server.listen(0, 'localhost', mustCall(() => {
383 const args = [
384 '-o', 'UserKnownHostsFile=/dev/null',
385 '-o', 'StrictHostKeyChecking=no',
386 '-o', 'CheckHostIP=no',
387 '-o', 'ConnectTimeout=3',
388 '-o', 'GlobalKnownHostsFile=/dev/null',
389 '-o', 'GSSAPIAuthentication=no',
390 '-o', 'IdentitiesOnly=yes',
391 '-o', 'BatchMode=yes',
392 '-o', 'VerifyHostKeyDNS=no',
393
394 '-vvvvvv',
395 '-T',
396 '-o', 'KbdInteractiveAuthentication=no',
397 '-o', 'HostbasedAuthentication=no',
398 '-o', 'PasswordAuthentication=no',
399 '-o', 'PubkeyAuthentication=yes',
400 '-o', 'PreferredAuthentications=publickey'
401 ];
402
403 if (clientCfg.privateKeyPath)
404 args.push('-o', `IdentityFile=${clientCfg.privateKeyPath}`);
405
406 if (!/^[0-6]\./.test(opensshVer)) {
407 // OpenSSH 7.0+ disables DSS/DSA host (and user) key support by
408 // default, so we explicitly enable it here
409 args.push('-o', 'HostKeyAlgorithms=+ssh-dss');
410 args.push('-o', 'PubkeyAcceptedKeyTypes=+ssh-dss');
411 }
412
413 args.push('-p', server.address().port.toString(),
414 '-l', clientCfg.username,
415 'localhost',
416 'uptime');
417
418 client = spawn(opensshPath, args, SPAWN_OPTS);
419 server.emit('_child', client);
420
421 if (debug) {
422 readline.createInterface({
423 input: client.stdout
424 }).on('line', (line) => {
425 console.log(`[${title}][CLIENT][STDOUT]`, line);
426 });
427 readline.createInterface({
428 input: client.stderr
429 }).on('line', (line) => {
430 console.error(`[${title}][CLIENT][STDERR]`, line);
431 });
432 } else {
433 client.stdout.resume();
434 client.stderr.resume();
435 }
436
437 client.on('error', (err) => {
438 onError(err, null, null);
439 }).on('exit', (code) => {
440 clearTimeout(client.timer);
441 if (code !== 0)
442 return onError(new Error(`Non-zero exit code ${code}`), null, null);
443 onClose(null, null, null);
444 });
445
446 client.timer = setTimeout(() => {
447 assert(false, msg('Client timeout'));
448 }, timeout);
449 }));
450 }));
451
452 return { server };
453}