This repository has been archived by the owner on Oct 23, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
319 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |