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

Commit

Permalink
Add ECDSA prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
tniessen committed Sep 2, 2019
1 parent f86642e commit 7d87f8c
Show file tree
Hide file tree
Showing 3 changed files with 319 additions and 0 deletions.
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);
});
});

0 comments on commit 7d87f8c

Please sign in to comment.