Skip to content
File

Blob: src/workerd/api/node/tests/crypto_dh-test.js

javascript546 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4//
5// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
6//
7// Permission is hereby granted, free of charge, to any person obtaining a
8// copy of this software and associated documentation files (the
9// "Software"), to deal in the Software without restriction, including
10// without limitation the rights to use, copy, modify, merge, publish,
11// distribute, sublicense, and/or sell copies of the Software, and to permit
12// persons to whom the Software is furnished to do so, subject to the
13// following conditions:
14//
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24// USE OR OTHER DEALINGS IN THE SOFTWARE.
25
26'use strict';
27 
28import { Buffer } from 'node:buffer';
29 
30import * as assert from 'node:assert';
31import * as crypto from 'node:crypto';
32 
33export const dh_test = {
34 test(ctrl, env, ctx) {
35 // https://github.com/nodejs/node/issues/32738
36 // XXX(bnoordhuis) validateInt32() throwing ERR_OUT_OF_RANGE and RangeError
37 // instead of ERR_INVALID_ARG_TYPE and TypeError is questionable, IMO.
38 assert.throws(() => crypto.createDiffieHellman(13.37), {
39 code: 'ERR_OUT_OF_RANGE',
40 name: 'RangeError',
41 message:
42 'The value of "sizeOrKey" is out of range. ' +
43 'It must be an integer. Received 13.37',
44 });
45 
46 assert.throws(() => crypto.createDiffieHellman('abcdef', 13.37), {
47 code: 'ERR_OUT_OF_RANGE',
48 name: 'RangeError',
49 message:
50 'The value of "generator" is out of range. ' +
51 'It must be an integer. Received 13.37',
52 });
53 // BoringSSL throws when key sizes > 10000 bits are requested, we proactively return a
54 // RangeError instead which is more descriptive.
55 assert.throws(() => crypto.createDiffieHellman(10001), {
56 name: 'RangeError',
57 });
58 
59 for (const bits of [-1, 0, 1]) {
60 assert.throws(() => crypto.createDiffieHellman(bits), {
61 name: 'Error',
62 });
63 }
64 
65 // Through a fluke of history, g=0 defaults to DH_GENERATOR (2).
66 {
67 const g = 0;
68 crypto.createDiffieHellman('abcdef', 'hex', g);
69 }
70 
71 for (const g of [-1, 1]) {
72 const ex = {
73 name: 'RangeError',
74 };
75 assert.throws(() => crypto.createDiffieHellman('abcdef', g), ex);
76 assert.throws(() => crypto.createDiffieHellman('abcdef', 'hex', g), ex);
77 }
78 
79 // Calls with even p, or p values that are interpreted as <= g will be rejected.
80 crypto.createDiffieHellman('abcdef', 'hex', Buffer.from([2])); // OK
81 for (const bits of ['abcdef', Buffer.from([1]), Buffer.from([4])]) {
82 assert.throws(() => crypto.createDiffieHellman(bits), {
83 name: 'Error',
84 });
85 }
86 
87 for (const g of [Buffer.from([]), Buffer.from([0]), Buffer.from([1])]) {
88 const ex = {
89 name: 'Error',
90 };
91 assert.throws(() => crypto.createDiffieHellman('abcdef', g), ex);
92 assert.throws(() => crypto.createDiffieHellman('abcdef', 'hex', g), ex);
93 }
94 
95 [[0x1, 0x2], () => {}, /abc/, {}].forEach((input) => {
96 assert.throws(() => crypto.createDiffieHellman(input), {
97 code: 'ERR_INVALID_ARG_TYPE',
98 name: 'TypeError',
99 });
100 });
101 
102 assert.throws(
103 function () {
104 crypto.getDiffieHellman('unknown-group');
105 },
106 {
107 name: 'Error',
108 },
109 "crypto.getDiffieHellman('unknown-group') " +
110 'failed to throw the expected error.'
111 );
112 
113 assert.throws(() => crypto.createDiffieHellman('', true), {
114 code: 'ERR_INVALID_ARG_TYPE',
115 });
116 
117 [true, Symbol(), {}, () => {}, []].forEach((generator) =>
118 assert.throws(() => crypto.createDiffieHellman('', 'base64', generator), {
119 name: 'TypeError',
120 })
121 );
122 },
123};
124 
125///////////////
126 
127export const dh_verify_error_test = {
128 test(ctrl, env, ctx) {
129 // Second OAKLEY group, see
130 // https://github.com/nodejs/node-v0.x-archive/issues/2338 and
131 // https://xml2rfc.tools.ietf.org/public/rfc/html/rfc2412.html#anchor49
132 const p =
133 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' +
134 '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' +
135 '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' +
136 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF';
137 crypto.createDiffieHellman(p, 'hex');
138 
139 // Confirm DH_check() results are exposed for optional examination. Use an odd value for prime
140 // since even values are rejected immediately.
141 const bad_dh = crypto.createDiffieHellman('03', 'hex');
142 assert.notStrictEqual(bad_dh.verifyError, 0);
143 },
144};
145 
146/////////////////////
147 
148export const dh_constructor_test = {
149 test(ctrl, env, ctx) {
150 const DiffieHellmanGroup = crypto.DiffieHellmanGroup;
151 const dhg = DiffieHellmanGroup('modp14');
152 assert.ok(
153 dhg instanceof DiffieHellmanGroup,
154 'DiffieHellmanGroup is expected ' +
155 'to return a new instance when ' +
156 'called without `new`'
157 );
158 
159 const p1 = dhg.getPrime('buffer');
160 const DiffieHellman = crypto.DiffieHellman;
161 const dh = DiffieHellman(p1, 'buffer');
162 assert.ok(
163 dh instanceof DiffieHellman,
164 'DiffieHellman is expected to return a ' +
165 'new instance when called without `new`'
166 );
167 },
168};
169////////////////////
170 
171// This test will fail if BoringSSL runs in FIPS mode and succeed otherwise; disable it for now.
172 
173/*
174function test() {
175 const odd = Buffer.alloc(39, 'A');
176
177 const c = crypto.createDiffieHellman(size);
178 c.setPrivateKey(odd);
179 c.generateKeys();
180}
181
182// FIPS requires a length of at least 1024
183if (!common.hasFipsCrypto) {
184 test();
185} else {
186 assert.throws(function() { test(); }, /key size too small/);
187}
188*/
189 
190//////////////////
191 
192export const dh_group_test = {
193 test(ctrl, env, ctx) {
194 assert.throws(
195 function () {
196 crypto.getDiffieHellman('modp14').setPrivateKey('');
197 },
198 new RegExp(
199 '^TypeError: crypto\\.getDiffieHellman\\(\\.\\.\\.\\)\\.' +
200 'setPrivateKey is not a function$'
201 ),
202 "crypto.getDiffieHellman('modp14').setPrivateKey('') " +
203 'failed to throw the expected error.'
204 );
205 assert.throws(
206 function () {
207 crypto.getDiffieHellman('modp14').setPublicKey('');
208 },
209 new RegExp(
210 '^TypeError: crypto\\.getDiffieHellman\\(\\.\\.\\.\\)\\.' +
211 'setPublicKey is not a function$'
212 ),
213 "crypto.getDiffieHellman('modp14').setPublicKey('') " +
214 'failed to throw the expected error.'
215 );
216 },
217};
218 
219////////////////
220 
221export const dh_exchange_test = {
222 test(ctrl, env, ctx) {
223 const alice = crypto.createDiffieHellmanGroup('modp14');
224 const bob = crypto.createDiffieHellmanGroup('modp14');
225 alice.generateKeys();
226 bob.generateKeys();
227 const aSecret = alice.computeSecret(bob.getPublicKey()).toString('hex');
228 const bSecret = bob.computeSecret(alice.getPublicKey()).toString('hex');
229 assert.strictEqual(aSecret, bSecret);
230 },
231};
232 
233////////////////
234 
235// This test verifies padding with leading zeroes for shared
236// secrets that are strictly smaller than the modulus (prime).
237// See:
238// RFC 4346: https://www.ietf.org/rfc/rfc4346.txt
239// https://github.com/nodejs/node-v0.x-archive/issues/7906
240// https://github.com/nodejs/node-v0.x-archive/issues/5239
241//
242// In FIPS mode OPENSSL_DH_FIPS_MIN_MODULUS_BITS = 1024, meaning we need
243// a FIPS-friendly >= 1024 bit prime, we can use MODP 14 from RFC 3526:
244// https://www.ietf.org/rfc/rfc3526.txt
245//
246// We can generate appropriate values with this code:
247//
248// crypto = require('crypto');
249//
250// for (;;) {
251// var a = crypto.getDiffieHellman('modp14'),
252// var b = crypto.getDiffieHellman('modp14');
253//
254// a.generateKeys();
255// b.generateKeys();
256//
257// var aSecret = a.computeSecret(b.getPublicKey()).toString('hex');
258// console.log("A public: " + a.getPublicKey().toString('hex'));
259// console.log("A private: " + a.getPrivateKey().toString('hex'));
260// console.log("B public: " + b.getPublicKey().toString('hex'));
261// console.log("B private: " + b.getPrivateKey().toString('hex'));
262// console.log("A secret: " + aSecret);
263// console.log('-------------------------------------------------');
264// if(aSecret.substring(0,2) === "00") {
265// console.log("found short key!");
266// return;
267// }
268// }
269 
270const apub =
271 '5484455905d3eff34c70980e871f27f05448e66f5a6efbb97cbcba4e927196c2bd9ea272cded91\
27210a4977afa8d9b16c9139a444ed2d954a794650e5d7cb525204f385e1af81530518563822ecd0f9\
273524a958d02b3c269e79d6d69850f0968ad567a4404fbb0b19efc8bc73e267b6136b88cafb33299f\
274f7c7cace3ffab1a88c2c9ee841f88b4c3679b4efc465f5c93cca11d487be57373e4c5926f634c4e\
275efee6721d01db91cd66321615b2522f96368dbc818875d422140d0edf30bdb97d9721feddcb9ff6\
276453741a4f687ee46fc54bf1198801f1210ac789879a5ee123f79e2d2ce1209df2445d32166bc9e4\
2778f89e944ec9c3b2e16c8066cd8eebd4e33eb941';
278const bpub =
279 '3fca64510e36bc7da8a3a901c7b74c2eabfa25deaf7cbe1d0c50235866136ad677317279e1fb0\
28006e9c0a07f63e14a3363c8e016fbbde2b2c7e79fed1cc3e08e95f7459f547a8cd0523ee9dc744d\
281e5a956d92b937db4448917e1f6829437f05e408ee7aea70c0362b37370c7c75d14449d8b2d2133\
28204ac972302d349975e2265ca7103cfebd019d9e91234d638611abd049014f7abf706c1c5da6c88\
283788a1fdc6cdf17f5fffaf024ce8711a2ebde0b52e9f1cb56224483826d6e5ac6ecfaae07b75d20\
2846e8ac97f5be1a5b68f20382f2a7dac189cf169325c4cf845b26a0cd616c31fec905c5d9035e5f7\
2858e9880c812374ac0f3ca3d365f06e4be526b5affd4b79';
286const apriv =
287 '62411e34704637d99c6c958a7db32ac22fcafafbe1c33d2cfdb76e12ded41f38fc16b792b9041\
2882e4c82755a3815ba52f780f0ee296ad46e348fc4d1dcd6b64f4eea1b231b2b7d95c5b1c2e26d34\
28983520558b9860a6eb668f01422a54e6604aa7702b4e67511397ef3ecb912bff1a83899c5a5bfb2\
2900ee29249a91b8a698e62486f7009a0e9eaebda69d77ecfa2ca6ba2db6c8aa81759c8c90c675979\
29108c3b3e6fc60668f7be81cce6784482af228dd7f489005253a165e292802cfd0399924f6c56827\
2927012f68255207722355634290acc7fddeefbba75650a85ece95b6a12de67eac016ba78960108dd\
2935dbadfaa43cc9fed515a1f307b7d90ae0623bc7b8cefb';
294const secret =
295 '00c37b1e06a436d6717816a40e6d72907a6f255638b93032267dcb9a5f0b4a9aa0236f3dce63b\
2961c418c60978a00acd1617dfeecf1661d8a3fafb4d0d8824386750f4853313400e7e4afd22847e4\
297fa56bc9713872021265111906673b38db83d10cbfa1dea3b6b4c97c8655f4ae82125281af7f234\
2988916a15c6f95649367d169d587697480df4d10b381479e86d5518b520d9d8fb764084eab518224\
299dc8fe984ddaf532fc1531ce43155fa0ab32532bf1ece5356b8a3447b5267798a904f16f3f4e635\
300597adc0179d011132dcffc0bbcb0dd2c8700872f8663ec7ddd897c659cc2efebccc73f38f0ec96\
3018612314311231f905f91c63a1aea52e0b60cead8b57df';
302 
303export const dh_padding_test = {
304 test(ctrl, env, ctx) {
305 /* FIPS-friendly 2048 bit prime */
306 const p = crypto.createDiffieHellman(
307 crypto.getDiffieHellman('modp14').getPrime()
308 );
309 
310 p.setPublicKey(apub, 'hex');
311 p.setPrivateKey(apriv, 'hex');
312 
313 assert.strictEqual(
314 p.computeSecret(bpub, 'hex', 'hex').toString('hex'),
315 secret
316 );
317 },
318};
319 
320export const dhKeygenTest = {
321 test() {
322 // FIPS-mode BoringSSL mandates keys of at least 1024 bits. RFC 8270 recommends that sizes of
323 // at least 2048 bits should be used, 1024-bit primes are sufficient for these tests though.
324 const size = 1024;
325 const dh1 = crypto.createDiffieHellman(size);
326 const p1 = dh1.getPrime('buffer');
327 const dh2 = crypto.createDiffieHellman(p1, 'buffer');
328 const key1 = dh1.generateKeys();
329 const key2 = dh2.generateKeys('hex');
330 const secret1 = dh1.computeSecret(key2, 'hex', 'base64');
331 const secret2 = dh2.computeSecret(key1, 'latin1', 'buffer');
332 
333 // Test Diffie-Hellman with two parties sharing a secret,
334 // using various encodings as we go along
335 assert.strictEqual(secret2.toString('base64'), secret1);
336 
337 assert.strictEqual(dh1.verifyError, 0);
338 assert.strictEqual(dh2.verifyError, 0);
339 
340 // Create "another dh1" using generated keys from dh1,
341 // and compute secret again
342 const dh3 = crypto.createDiffieHellman(p1, 'buffer');
343 const privkey1 = dh1.getPrivateKey();
344 dh3.setPublicKey(key1);
345 dh3.setPrivateKey(privkey1);
346 
347 assert.deepStrictEqual(dh1.getPrime(), dh3.getPrime());
348 assert.deepStrictEqual(dh1.getGenerator(), dh3.getGenerator());
349 assert.deepStrictEqual(dh1.getPublicKey(), dh3.getPublicKey());
350 assert.deepStrictEqual(dh1.getPrivateKey(), dh3.getPrivateKey());
351 assert.strictEqual(dh3.verifyError, 0);
352 
353 const secret3 = dh3.computeSecret(key2, 'hex', 'base64');
354 
355 assert.strictEqual(secret1, secret3);
356 
357 // computeSecret works without a public key set at all.
358 const dh4 = crypto.createDiffieHellman(p1, 'buffer');
359 dh4.setPrivateKey(privkey1);
360 
361 assert.deepStrictEqual(dh1.getPrime(), dh4.getPrime());
362 assert.deepStrictEqual(dh1.getGenerator(), dh4.getGenerator());
363 assert.deepStrictEqual(dh1.getPrivateKey(), dh4.getPrivateKey());
364 assert.strictEqual(dh4.verifyError, 0);
365 
366 const secret4 = dh4.computeSecret(key2, 'hex', 'base64');
367 
368 assert.strictEqual(secret1, secret4);
369 
370 assert.throws(
371 () => {
372 dh3.computeSecret('');
373 },
374 { name: 'Error' }
375 );
376 },
377};
378 
379export const ecdh = {
380 test() {
381 const curves = crypto.getCurves();
382 curves.forEach((i) => {
383 const alice = crypto.createECDH(i);
384 const bob = crypto.createECDH(i);
385 
386 alice.generateKeys();
387 bob.generateKeys();
388 
389 const aliceSecret = alice.computeSecret(bob.getPublicKey(), null, 'hex');
390 const bobSecret = bob.computeSecret(alice.getPublicKey(), null, 'hex');
391 
392 assert.strictEqual(aliceSecret, bobSecret);
393 });
394 },
395};
396 
397export const ecdhConvertKey = {
398 test() {
399 const ecdh = crypto.createECDH('prime256v1');
400 ecdh.generateKeys();
401 
402 const compressedKey = ecdh.getPublicKey('hex', 'compressed');
403 
404 const uncompressedKey = crypto.ECDH.convertKey(
405 compressedKey,
406 'prime256v1',
407 'hex',
408 'hex',
409 'uncompressed'
410 );
411 
412 // The converted key and the uncompressed public key should be the same
413 assert.strictEqual(uncompressedKey, ecdh.getPublicKey('hex'));
414 },
415};
416 
417export const statelessDh = {
418 test() {
419 // DH keygen is currently unsupported by the boringssl+fips
420 // we use internally. This test works for workerd but fails
421 // on the internal run.
422 // const pair1 = crypto.generateKeyPairSync('dh', {
423 // prime: Buffer.from([31, 0, 0, 0, 0, 1]),
424 // });
425 // const pair2 = crypto.generateKeyPairSync('dh', {
426 // prime: Buffer.from([31, 0, 0, 0, 0, 1]),
427 // });
428 // const sec1 = crypto.diffieHellman({
429 // publicKey: pair1.publicKey,
430 // privateKey: pair2.privateKey,
431 // });
432 // const sec2 = crypto.diffieHellman({
433 // publicKey: pair2.publicKey,
434 // privateKey: pair1.privateKey,
435 // });
436 // assert.deepStrictEqual(sec1, sec2);
437 },
438};
439 
440export const statelessDhX25591 = {
441 test() {
442 const pair1 = crypto.generateKeyPairSync('x25519');
443 const pair2 = crypto.generateKeyPairSync('x25519');
444 const sec1 = crypto.diffieHellman({
445 publicKey: pair1.publicKey,
446 privateKey: pair2.privateKey,
447 });
448 const sec2 = crypto.diffieHellman({
449 publicKey: pair2.publicKey,
450 privateKey: pair1.privateKey,
451 });
452 assert.deepStrictEqual(sec1, sec2);
453 },
454};
455 
456export const statelessDhEc = {
457 test() {
458 // The Boringssl-based implementation currently does not support
459 // the mechanisms for stateless dh using named EC curve.
460 const pair1 = crypto.generateKeyPairSync('ec', { namedCurve: 'secp224r1' });
461 const pair2 = crypto.generateKeyPairSync('ec', { namedCurve: 'secp224r1' });
462 assert.throws(
463 () => {
464 crypto.diffieHellman({
465 publicKey: pair1.publicKey,
466 privateKey: pair2.privateKey,
467 });
468 },
469 {
470 message: /Failed to derive/,
471 }
472 );
473 },
474};
475 
476export const helloTest = {
477 test() {
478 const v2 = crypto.createDiffieHellman('hello');
479 assert.throws(() => v2.getPrivateKey(v2, 'hello', v2, v2, 'hello'), {
480 message: /No private key/,
481 });
482 },
483};
484 
485export const DhLargeGeneratorParam = {
486 async test() {
487 const prime = Buffer.from([
488 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda,
489 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c,
490 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74,
491 ]);
492 
493 const generator = new Uint8Array(100 * 1024 * 1024);
494 generator.fill(0x02);
495 
496 assert.throws(() => crypto.createDiffieHellman(prime, generator), {
497 name: 'RangeError',
498 message: /generator is too large/,
499 });
500 },
501};
502 
503// Test that invalid public keys are rejected by setPublicKey and computeSecret.
504export const dhPubKeyValidation = {
505 test() {
506 const dh = crypto.createDiffieHellman(1024);
507 dh.generateKeys();
508 const prime = dh.getPrime();
509 
510 // Public key of 0 should be rejected (too small)
511 const dh2 = crypto.createDiffieHellman(prime);
512 dh2.generateKeys();
513 assert.throws(() => dh2.setPublicKey(Buffer.from([0x00])), {
514 message: /too small|invalid public key/,
515 });
516 
517 // Public key of 1 should be rejected (too small)
518 assert.throws(() => dh2.setPublicKey(Buffer.from([0x01])), {
519 message: /too small|invalid public key/,
520 });
521 
522 // Public key equal to the prime should be rejected (too large)
523 assert.throws(() => dh2.setPublicKey(prime), {
524 message: /too large|invalid public key/,
525 });
526 
527 // computeSecret should also reject invalid peer keys proactively
528 assert.throws(() => dh2.computeSecret(Buffer.from([0x00])), {
529 message: /too small|invalid peer public key/,
530 });
531 assert.throws(() => dh2.computeSecret(Buffer.from([0x01])), {
532 message: /too small|invalid peer public key/,
533 });
534 assert.throws(() => dh2.computeSecret(prime), {
535 message: /too large|invalid peer public key/,
536 });
537 
538 // Valid exchange should still work
539 const dh3 = crypto.createDiffieHellman(prime);
540 dh3.generateKeys();
541 const secret1 = dh.computeSecret(dh3.getPublicKey());
542 const secret2 = dh3.computeSecret(dh.getPublicKey());
543 assert.deepStrictEqual(secret1, secret2);
544 },
545};