Skip to content
This repository was archived by the owner on Oct 23, 2020. It is now read-only.

Commit 7d87f8c

Browse files
committed
Add ECDSA prototype
1 parent f86642e commit 7d87f8c

3 files changed

Lines changed: 319 additions & 0 deletions

File tree

lib/algorithms.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const { AES_CTR, AES_CBC, AES_GCM, AES_KW } = require('./algorithms/aes');
4+
const { ECDSA } = require('./algorithms/ecdsa');
45
const { HKDF } = require('./algorithms/hkdf');
56
const { HMAC } = require('./algorithms/hmac');
67
const { PBKDF2 } = require('./algorithms/pbkdf2');
@@ -14,6 +15,8 @@ const algorithms = [
1415
AES_GCM,
1516
AES_KW,
1617

18+
ECDSA,
19+
1720
HKDF,
1821

1922
HMAC,

lib/algorithms/ecdsa.js

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
'use strict';
2+
3+
const crypto = require('crypto');
4+
const { promisify } = require('util');
5+
6+
const {
7+
DataError,
8+
InvalidAccessError,
9+
NotSupportedError,
10+
OperationError
11+
} = require('../errors');
12+
const { kKeyMaterial, CryptoKey } = require('../key');
13+
const { limitUsages, opensslHashFunctionName, toBuffer } = require('../util');
14+
15+
const generateKeyPair = promisify(crypto.generateKeyPair);
16+
17+
const curveBasePointOrderSizes = {
18+
'P-256': 32,
19+
'P-384': 48,
20+
'P-521': 66
21+
};
22+
23+
const byte = (b) => Buffer.from([b]);
24+
25+
function convertSignatureToASN1(signature, n) {
26+
if (signature.length !== 2 * n)
27+
throw new OperationError();
28+
29+
const r = signature.slice(0, n);
30+
const s = signature.slice(n);
31+
32+
function encodeLength(len) {
33+
// Short form.
34+
if (len < 128)
35+
return byte(len);
36+
37+
// Long form.
38+
const buffer = Buffer.alloc(5);
39+
buffer.writeUInt32BE(len, 1);
40+
let offset = 1;
41+
while (buffer[offset] === 0)
42+
offset++;
43+
buffer[offset - 1] = 0x80 | (5 - offset);
44+
return buffer.slice(offset - 1);
45+
}
46+
47+
function encodeUnsignedInteger(integer) {
48+
// ASN.1 integers are signed, so in order to encode unsigned integers, we
49+
// need to make sure that the MSB is not set.
50+
if (integer[0] & 0x80) {
51+
return Buffer.concat([
52+
byte(0x02),
53+
encodeLength(integer.length + 1),
54+
byte(0x00), integer
55+
]);
56+
} else {
57+
// If the MSB is not set, enforce a minimal representation of the integer.
58+
let i = 0;
59+
while (integer[i] === 0 && (integer[i + 1] & 0x80) === 0)
60+
i++;
61+
return Buffer.concat([
62+
byte(0x02),
63+
encodeLength(integer.length - i),
64+
integer.slice(i)
65+
]);
66+
}
67+
}
68+
69+
const seq = Buffer.concat([
70+
encodeUnsignedInteger(r),
71+
encodeUnsignedInteger(s)
72+
]);
73+
74+
return Buffer.concat([byte(0x30), encodeLength(seq.length), seq]);
75+
}
76+
77+
function convertSignatureFromASN1(signature, n) {
78+
let offset = 2;
79+
if (signature[1] & 0x80)
80+
offset += signature[1] & ~0x80;
81+
82+
function decodeUnsignedInteger() {
83+
let length = signature[offset + 1];
84+
offset += 2;
85+
if (length & 0x80) {
86+
// Long form.
87+
const nBytes = length & ~0x80;
88+
length = 0;
89+
for (let i = 0; i < nBytes; i++)
90+
length = (length << 8) | signature[offset + 2 + i];
91+
offset += nBytes;
92+
}
93+
94+
// There may be exactly one leading zero (if the next byte's MSB is set).
95+
if (signature[offset] === 0) {
96+
offset++;
97+
length--;
98+
}
99+
100+
const result = signature.slice(offset, offset + length);
101+
offset += length;
102+
return result;
103+
}
104+
105+
const r = decodeUnsignedInteger();
106+
const s = decodeUnsignedInteger();
107+
108+
const result = Buffer.alloc(2 * n, 0);
109+
r.copy(result, n - r.length);
110+
s.copy(result, 2 * n - s.length);
111+
return result;
112+
}
113+
114+
// Spec: https://www.w3.org/TR/WebCryptoAPI/#ecdsa
115+
module.exports.ECDSA = {
116+
name: 'ECDSA',
117+
118+
async generateKey(algorithm, extractable, usages) {
119+
limitUsages(usages, ['sign', 'verify']);
120+
const privateUsages = usages.includes('sign') ? ['sign'] : [];
121+
const publicUsages = usages.includes('verify') ? ['verify'] : [];
122+
123+
const { namedCurve } = algorithm;
124+
if (!curveBasePointOrderSizes[namedCurve])
125+
throw new NotSupportedError();
126+
127+
const { privateKey, publicKey } = await generateKeyPair('ec', {
128+
namedCurve
129+
});
130+
131+
const alg = {
132+
name: this.name,
133+
namedCurve
134+
};
135+
136+
return {
137+
privateKey: new CryptoKey('private', alg, extractable, privateUsages,
138+
privateKey),
139+
publicKey: new CryptoKey('public', alg, extractable, publicUsages,
140+
publicKey)
141+
};
142+
},
143+
144+
importKey(keyFormat, keyData, params, extractable, keyUsages) {
145+
const { namedCurve } = params;
146+
147+
const opts = {
148+
key: toBuffer(keyData),
149+
format: 'der',
150+
type: keyFormat
151+
};
152+
153+
let key;
154+
if (keyFormat === 'spki') {
155+
limitUsages(keyUsages, ['verify']);
156+
key = crypto.createPublicKey(opts);
157+
} else if (keyFormat === 'pkcs8') {
158+
limitUsages(keyUsages, ['sign']);
159+
key = crypto.createPrivateKey(opts);
160+
} else {
161+
throw new NotSupportedError();
162+
}
163+
164+
if (key.asymmetricKeyType !== 'ec')
165+
throw new DataError();
166+
167+
return new CryptoKey(key.type, { name: this.name, namedCurve },
168+
extractable, keyUsages, key);
169+
},
170+
171+
exportKey(format, key) {
172+
if (format !== 'spki' && format !== 'pkcs8')
173+
throw new NotSupportedError();
174+
175+
if (format === 'spki' && key.type !== 'public' ||
176+
format === 'pkcs8' && key.type !== 'private')
177+
throw new InvalidAccessError();
178+
179+
return key[kKeyMaterial].export({
180+
format: 'der',
181+
type: format
182+
});
183+
},
184+
185+
sign(algorithm, key, data) {
186+
if (key.type !== 'private')
187+
throw new InvalidAccessError();
188+
189+
const { hash } = algorithm;
190+
const hashFn = opensslHashFunctionName(hash);
191+
192+
const asn1Sig = crypto.sign(hashFn, toBuffer(data), key[kKeyMaterial]);
193+
const n = curveBasePointOrderSizes[key.algorithm.namedCurve];
194+
return convertSignatureFromASN1(asn1Sig, n);
195+
},
196+
197+
verify(algorithm, key, signature, data) {
198+
if (key.type !== 'public')
199+
throw new InvalidAccessError();
200+
201+
const n = curveBasePointOrderSizes[key.algorithm.namedCurve];
202+
signature = convertSignatureToASN1(toBuffer(signature), n);
203+
204+
const { hash } = algorithm;
205+
const hashFn = opensslHashFunctionName(hash);
206+
return crypto.verify(hashFn, data, key[kKeyMaterial], signature);
207+
}
208+
};

test/algorithms/ecdsa.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
const { randomBytes } = require('crypto');
5+
6+
const { subtle } = require('../../');
7+
8+
// Disables timeouts for tests that involve key pair generation.
9+
const NO_TIMEOUT = 0;
10+
11+
describe('ECDSA', () => {
12+
it('should generate, import and export keys', async () => {
13+
const { publicKey, privateKey } = await subtle.generateKey({
14+
name: 'ECDSA',
15+
namedCurve: 'P-256'
16+
}, true, ['sign', 'verify']);
17+
18+
assert.strictEqual(publicKey.type, 'public');
19+
assert.strictEqual(privateKey.type, 'private');
20+
for (const key of [publicKey, privateKey]) {
21+
assert.strictEqual(key.algorithm.name, 'ECDSA');
22+
assert.strictEqual(key.algorithm.namedCurve, 'P-256');
23+
}
24+
25+
const expPublicKey = await subtle.exportKey('spki', publicKey);
26+
assert(Buffer.isBuffer(expPublicKey));
27+
const expPrivateKey = await subtle.exportKey('pkcs8', privateKey);
28+
assert(Buffer.isBuffer(expPrivateKey));
29+
30+
const impPublicKey = await subtle.importKey('spki', expPublicKey, {
31+
name: 'ECDSA',
32+
hash: 'SHA-384'
33+
}, true, ['verify']);
34+
const impPrivateKey = await subtle.importKey('pkcs8', expPrivateKey, {
35+
name: 'ECDSA',
36+
hash: 'SHA-384'
37+
}, true, ['sign']);
38+
39+
assert.deepStrictEqual(await subtle.exportKey('spki', impPublicKey),
40+
expPublicKey);
41+
assert.deepStrictEqual(await subtle.exportKey('pkcs8', impPrivateKey),
42+
expPrivateKey);
43+
})
44+
.timeout(NO_TIMEOUT);
45+
46+
it('should sign and verify data', async () => {
47+
async function test(namedCurve, signatureLength) {
48+
const { privateKey, publicKey } = await subtle.generateKey({
49+
name: 'ECDSA',
50+
namedCurve
51+
}, false, ['sign', 'verify']);
52+
53+
const data = randomBytes(200);
54+
for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) {
55+
const signature = await subtle.sign({
56+
name: 'ECDSA',
57+
hash
58+
}, privateKey, data);
59+
assert.strictEqual(signature.length, signatureLength);
60+
61+
let ok = await subtle.verify({
62+
name: 'ECDSA',
63+
hash
64+
}, publicKey, signature, data);
65+
assert.strictEqual(ok, true);
66+
67+
signature[Math.floor(signature.length * Math.random())] ^= 1;
68+
69+
ok = await subtle.verify({
70+
name: 'ECDSA',
71+
hash
72+
}, publicKey, signature, data);
73+
assert.strictEqual(ok, false);
74+
}
75+
}
76+
77+
return Promise.all([
78+
test('P-256', 2 * 32),
79+
test('P-384', 2 * 48),
80+
test('P-521', 2 * 66)
81+
]);
82+
})
83+
.timeout(NO_TIMEOUT);
84+
85+
it('should verify externally signed data', async () => {
86+
const publicKeyData = '3076301006072a8648ce3d020106052b810400220362000476' +
87+
'ece47b2ab001a109f741f9fcd7fbe9cbfd3b6abbac626bd1fb' +
88+
'eca18fc700adc612339a732ee4621a129dfdc22940011d17ff' +
89+
'94a06e8aa55b6a62c3014032aeefc099d455921a0072d26a45' +
90+
'b787bd327beb2846f70657268d2485423720be4b';
91+
const publicKeyBuffer = Buffer.from(publicKeyData, 'hex');
92+
const publicKey = await subtle.importKey('spki', publicKeyBuffer, {
93+
name: 'ECDSA',
94+
namedCurve: 'P-384'
95+
}, false, ['verify']);
96+
97+
const data = Buffer.from('0a0b0c0d0e0f', 'hex');
98+
const signatureData = '5ec17d2611a28d72e448826ba3b3fb7ef041275c5727b05d38' +
99+
'8fb435b2897a9047d9f02ade37908e6f81e1419fd671978881' +
100+
'9887f0fd830dd02ecc66051e14512fdba0f51fb3e58629210d' +
101+
'136a48944f411649874cfb29498161c6327a7d4c3d';
102+
const signature = Buffer.from(signatureData, 'hex');
103+
104+
const ok = await subtle.verify({ name: 'ECDSA', hash: 'SHA-512' },
105+
publicKey, signature, data);
106+
assert.strictEqual(ok, true);
107+
});
108+
});

0 commit comments

Comments
 (0)