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

Add ECDSA prototype #18

Merged
merged 1 commit into from
Sep 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/algorithms.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const { AES_CTR, AES_CBC, AES_GCM, AES_KW } = require('./algorithms/aes');
const { ECDSA } = require('./algorithms/ecdsa');
const { HKDF } = require('./algorithms/hkdf');
const { HMAC } = require('./algorithms/hmac');
const { PBKDF2 } = require('./algorithms/pbkdf2');
Expand All @@ -14,6 +15,8 @@ const algorithms = [
AES_GCM,
AES_KW,

ECDSA,

HKDF,

HMAC,
Expand Down
208 changes: 208 additions & 0 deletions lib/algorithms/ecdsa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
'use strict';

const crypto = require('crypto');
const { promisify } = require('util');

const {
DataError,
InvalidAccessError,
NotSupportedError,
OperationError
} = require('../errors');
const { kKeyMaterial, CryptoKey } = require('../key');
const { limitUsages, opensslHashFunctionName, toBuffer } = require('../util');

const generateKeyPair = promisify(crypto.generateKeyPair);

const curveBasePointOrderSizes = {
'P-256': 32,
'P-384': 48,
'P-521': 66
};

const byte = (b) => Buffer.from([b]);

function convertSignatureToASN1(signature, n) {
if (signature.length !== 2 * n)
throw new OperationError();

const r = signature.slice(0, n);
const s = signature.slice(n);

function encodeLength(len) {
// Short form.
if (len < 128)
return byte(len);

// Long form.
const buffer = Buffer.alloc(5);
buffer.writeUInt32BE(len, 1);
let offset = 1;
while (buffer[offset] === 0)
offset++;
buffer[offset - 1] = 0x80 | (5 - offset);
return buffer.slice(offset - 1);
}

function encodeUnsignedInteger(integer) {
// ASN.1 integers are signed, so in order to encode unsigned integers, we
// need to make sure that the MSB is not set.
if (integer[0] & 0x80) {
return Buffer.concat([
byte(0x02),
encodeLength(integer.length + 1),
byte(0x00), integer
]);
} else {
// If the MSB is not set, enforce a minimal representation of the integer.
let i = 0;
while (integer[i] === 0 && (integer[i + 1] & 0x80) === 0)
i++;
return Buffer.concat([
byte(0x02),
encodeLength(integer.length - i),
integer.slice(i)
]);
}
}

const seq = Buffer.concat([
encodeUnsignedInteger(r),
encodeUnsignedInteger(s)
]);

return Buffer.concat([byte(0x30), encodeLength(seq.length), seq]);
}

function convertSignatureFromASN1(signature, n) {
let offset = 2;
if (signature[1] & 0x80)
offset += signature[1] & ~0x80;

function decodeUnsignedInteger() {
let length = signature[offset + 1];
offset += 2;
if (length & 0x80) {
// Long form.
const nBytes = length & ~0x80;
length = 0;
for (let i = 0; i < nBytes; i++)
length = (length << 8) | signature[offset + 2 + i];
offset += nBytes;
}

// There may be exactly one leading zero (if the next byte's MSB is set).
if (signature[offset] === 0) {
offset++;
length--;
}

const result = signature.slice(offset, offset + length);
offset += length;
return result;
}

const r = decodeUnsignedInteger();
const s = decodeUnsignedInteger();

const result = Buffer.alloc(2 * n, 0);
r.copy(result, n - r.length);
s.copy(result, 2 * n - s.length);
return result;
}

// Spec: https://www.w3.org/TR/WebCryptoAPI/#ecdsa
module.exports.ECDSA = {
name: 'ECDSA',

async generateKey(algorithm, extractable, usages) {
limitUsages(usages, ['sign', 'verify']);
const privateUsages = usages.includes('sign') ? ['sign'] : [];
const publicUsages = usages.includes('verify') ? ['verify'] : [];

const { namedCurve } = algorithm;
if (!curveBasePointOrderSizes[namedCurve])
throw new NotSupportedError();

const { privateKey, publicKey } = await generateKeyPair('ec', {
namedCurve
});

const alg = {
name: this.name,
namedCurve
};

return {
privateKey: new CryptoKey('private', alg, extractable, privateUsages,
privateKey),
publicKey: new CryptoKey('public', alg, extractable, publicUsages,
publicKey)
};
},

importKey(keyFormat, keyData, params, extractable, keyUsages) {
const { namedCurve } = params;

const opts = {
key: toBuffer(keyData),
format: 'der',
type: keyFormat
};

let key;
if (keyFormat === 'spki') {
limitUsages(keyUsages, ['verify']);
key = crypto.createPublicKey(opts);
} else if (keyFormat === 'pkcs8') {
limitUsages(keyUsages, ['sign']);
key = crypto.createPrivateKey(opts);
} else {
throw new NotSupportedError();
}

if (key.asymmetricKeyType !== 'ec')
throw new DataError();

return new CryptoKey(key.type, { name: this.name, namedCurve },
extractable, keyUsages, key);
},

exportKey(format, key) {
if (format !== 'spki' && format !== 'pkcs8')
throw new NotSupportedError();

if (format === 'spki' && key.type !== 'public' ||
format === 'pkcs8' && key.type !== 'private')
throw new InvalidAccessError();

return key[kKeyMaterial].export({
format: 'der',
type: format
});
},

sign(algorithm, key, data) {
if (key.type !== 'private')
throw new InvalidAccessError();

const { hash } = algorithm;
const hashFn = opensslHashFunctionName(hash);

const asn1Sig = crypto.sign(hashFn, toBuffer(data), key[kKeyMaterial]);
const n = curveBasePointOrderSizes[key.algorithm.namedCurve];
return convertSignatureFromASN1(asn1Sig, n);
},

verify(algorithm, key, signature, data) {
if (key.type !== 'public')
throw new InvalidAccessError();

const n = curveBasePointOrderSizes[key.algorithm.namedCurve];
signature = convertSignatureToASN1(toBuffer(signature), n);

const { hash } = algorithm;
const hashFn = opensslHashFunctionName(hash);
return crypto.verify(hashFn, data, key[kKeyMaterial], signature);
}
};
108 changes: 108 additions & 0 deletions test/algorithms/ecdsa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'use strict';

const assert = require('assert');
const { randomBytes } = require('crypto');

const { subtle } = require('../../');

// Disables timeouts for tests that involve key pair generation.
const NO_TIMEOUT = 0;

describe('ECDSA', () => {
it('should generate, import and export keys', async () => {
const { publicKey, privateKey } = await subtle.generateKey({
name: 'ECDSA',
namedCurve: 'P-256'
}, true, ['sign', 'verify']);

assert.strictEqual(publicKey.type, 'public');
assert.strictEqual(privateKey.type, 'private');
for (const key of [publicKey, privateKey]) {
assert.strictEqual(key.algorithm.name, 'ECDSA');
assert.strictEqual(key.algorithm.namedCurve, 'P-256');
}

const expPublicKey = await subtle.exportKey('spki', publicKey);
assert(Buffer.isBuffer(expPublicKey));
const expPrivateKey = await subtle.exportKey('pkcs8', privateKey);
assert(Buffer.isBuffer(expPrivateKey));

const impPublicKey = await subtle.importKey('spki', expPublicKey, {
name: 'ECDSA',
hash: 'SHA-384'
}, true, ['verify']);
const impPrivateKey = await subtle.importKey('pkcs8', expPrivateKey, {
name: 'ECDSA',
hash: 'SHA-384'
}, true, ['sign']);

assert.deepStrictEqual(await subtle.exportKey('spki', impPublicKey),
expPublicKey);
assert.deepStrictEqual(await subtle.exportKey('pkcs8', impPrivateKey),
expPrivateKey);
})
.timeout(NO_TIMEOUT);

it('should sign and verify data', async () => {
async function test(namedCurve, signatureLength) {
const { privateKey, publicKey } = await subtle.generateKey({
name: 'ECDSA',
namedCurve
}, false, ['sign', 'verify']);

const data = randomBytes(200);
for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) {
const signature = await subtle.sign({
name: 'ECDSA',
hash
}, privateKey, data);
assert.strictEqual(signature.length, signatureLength);

let ok = await subtle.verify({
name: 'ECDSA',
hash
}, publicKey, signature, data);
assert.strictEqual(ok, true);

signature[Math.floor(signature.length * Math.random())] ^= 1;

ok = await subtle.verify({
name: 'ECDSA',
hash
}, publicKey, signature, data);
assert.strictEqual(ok, false);
}
}

return Promise.all([
test('P-256', 2 * 32),
test('P-384', 2 * 48),
test('P-521', 2 * 66)
]);
})
.timeout(NO_TIMEOUT);

it('should verify externally signed data', async () => {
const publicKeyData = '3076301006072a8648ce3d020106052b810400220362000476' +
'ece47b2ab001a109f741f9fcd7fbe9cbfd3b6abbac626bd1fb' +
'eca18fc700adc612339a732ee4621a129dfdc22940011d17ff' +
'94a06e8aa55b6a62c3014032aeefc099d455921a0072d26a45' +
'b787bd327beb2846f70657268d2485423720be4b';
const publicKeyBuffer = Buffer.from(publicKeyData, 'hex');
const publicKey = await subtle.importKey('spki', publicKeyBuffer, {
name: 'ECDSA',
namedCurve: 'P-384'
}, false, ['verify']);

const data = Buffer.from('0a0b0c0d0e0f', 'hex');
const signatureData = '5ec17d2611a28d72e448826ba3b3fb7ef041275c5727b05d38' +
'8fb435b2897a9047d9f02ade37908e6f81e1419fd671978881' +
'9887f0fd830dd02ecc66051e14512fdba0f51fb3e58629210d' +
'136a48944f411649874cfb29498161c6327a7d4c3d';
const signature = Buffer.from(signatureData, 'hex');

const ok = await subtle.verify({ name: 'ECDSA', hash: 'SHA-512' },
publicKey, signature, data);
assert.strictEqual(ok, true);
});
});