forked from nodejs/node
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathtest-crypto-timing-safe-equal.js
148 lines (121 loc) · 5.48 KB
/
test-crypto-timing-safe-equal.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Flags: --allow_natives_syntax --expose-gc
'use strict';
const common = require('../common');
const assert = require('assert');
if (!common.hasCrypto) {
common.skip('missing crypto');
return;
}
assert.equal(typeof global.gc, 'function', 'Run this test with --expose-gc');
const crypto = require('crypto');
assert.strictEqual(
crypto.timingSafeEqual(Buffer.from('foo'), Buffer.from('foo')),
true,
'should consider equal strings to be equal'
);
assert.strictEqual(
crypto.timingSafeEqual(Buffer.from('foo'), Buffer.from('bar')),
false,
'should consider unequal strings to be unequal'
);
assert.throws(function() {
crypto.timingSafeEqual(Buffer.from([1, 2, 3]), Buffer.from([1, 2]));
}, 'should throw when given buffers with different lengths');
assert.throws(function() {
crypto.timingSafeEqual('not a buffer', Buffer.from([1, 2]));
}, 'should throw if the first argument is not a buffer');
assert.throws(function() {
crypto.timingSafeEqual(Buffer.from([1, 2]), 'not a buffer');
}, 'should throw if the second argument is not a buffer');
function runBenchmark(compareFunc, bufferA, bufferB, expectedResult) {
// Make sure garbage collection does not interfere with timing
global.gc();
const startTime = process.hrtime();
const result = compareFunc(bufferA, bufferB);
const endTime = process.hrtime(startTime);
// Ensure that the result of the function call gets used, so that it doesn't
// get discarded due to engine optimizations.
assert.strictEqual(result, expectedResult);
return endTime[0] * 1e9 + endTime[1];
}
function getTValue(compareFunc) {
const numTrials = 10000;
const testBufferSize = 10000;
// Perform benchmarks to verify that timingSafeEqual is actually timing-safe.
const bufferA1 = Buffer.alloc(testBufferSize, 'A');
const bufferA2 = Buffer.alloc(testBufferSize, 'A');
const bufferB = Buffer.alloc(testBufferSize, 'B');
const bufferC = Buffer.alloc(testBufferSize, 'C');
const rawEqualBenches = Array(numTrials);
const rawUnequalBenches = Array(numTrials);
for (let i = 0; i < numTrials; i++) {
// First benchmark: comparing two equal buffers
rawEqualBenches[i] = runBenchmark(compareFunc, bufferA1, bufferA2, true);
// Second benchmark: comparing two unequal buffers
rawUnequalBenches[i] = runBenchmark(compareFunc, bufferB, bufferC, false);
}
const equalBenches = filterOutliers(rawEqualBenches);
const unequalBenches = filterOutliers(rawUnequalBenches);
// Use a two-sample t-test to determine whether the timing difference between
// the benchmarks is statistically significant.
// https://wikipedia.org/wiki/Student%27s_t-test#Independent_two-sample_t-test
const equalMean = mean(equalBenches);
const unequalMean = mean(unequalBenches);
const equalLen = equalBenches.length;
const unequalLen = unequalBenches.length;
const combinedStd = combinedStandardDeviation(equalBenches, unequalBenches);
const standardErr = combinedStd * Math.sqrt(1 / equalLen + 1 / unequalLen);
return (equalMean - unequalMean) / standardErr;
}
// Returns the mean of an array
function mean(array) {
return array.reduce((sum, val) => sum + val, 0) / array.length;
}
// Returns the sample standard deviation of an array
function standardDeviation(array) {
const arrMean = mean(array);
const total = array.reduce((sum, val) => sum + Math.pow(val - arrMean, 2), 0);
return Math.sqrt(total / (array.length - 1));
}
// Returns the common standard deviation of two arrays
function combinedStandardDeviation(array1, array2) {
const sum1 = Math.pow(standardDeviation(array1), 2) * (array1.length - 1);
const sum2 = Math.pow(standardDeviation(array2), 2) * (array2.length - 1);
return Math.sqrt((sum1 + sum2) / (array1.length + array2.length - 2));
}
// Filter large outliers from an array. A 'large outlier' is a value that is at
// least 50 times larger than the mean. This prevents the tests from failing
// due to the standard deviation increase when a function unexpectedly takes
// a very long time to execute.
function filterOutliers(array) {
const arrMean = mean(array);
return array.filter((value) => value / arrMean < 50);
}
// Force optimization before starting the benchmark
runBenchmark(crypto.timingSafeEqual, Buffer.from('A'), Buffer.from('A'), true);
eval('%OptimizeFunctionOnNextCall(runBenchmark)');
runBenchmark(crypto.timingSafeEqual, Buffer.from('A'), Buffer.from('A'), true);
// t_(0.9995, ∞)
// i.e. If a given comparison function is indeed timing-safe, the t-test result
// has a 99.9% chance to be below this threshold. Unfortunately, this means that
// this test will be a bit flakey and will fail 0.1% of the time even if
// crypto.timingSafeEqual is working properly.
// t-table ref: http://www.sjsu.edu/faculty/gerstman/StatPrimer/t-table.pdf
// Note that in reality there are roughly `2 * numTrials - 2` degrees of
// freedom, not ∞. However, assuming `numTrials` is large, this doesn't
// significantly affect the threshold.
const T_THRESHOLD = 3.291;
const t = getTValue(crypto.timingSafeEqual);
assert(
Math.abs(t) < T_THRESHOLD,
`timingSafeEqual should not leak information from its execution time (t=${t})`
);
// As a sanity check to make sure the statistical tests are working, run the
// same benchmarks again, this time with an unsafe comparison function. In this
// case the t-value should be above the threshold.
const unsafeCompare = (bufA, bufB) => bufA.equals(bufB);
const t2 = getTValue(unsafeCompare);
assert(
Math.abs(t2) > T_THRESHOLD,
`Buffer#equals should leak information from its execution time (t=${t2})`
);