Bugfix in Native ECC in Node

This commit is contained in:
Mahrud Sayrafi 2018-03-01 01:28:03 -08:00
parent 5fac00eddb
commit 5e857e131e
No known key found for this signature in database
GPG Key ID: C24071B956C3245F
9 changed files with 181 additions and 133 deletions

View File

@ -49,11 +49,12 @@ module.exports = function(grunt) {
standalone: 'openpgp'
},
// Don't bundle these packages with openpgp.js
external: ['crypto', 'zlib', 'node-localstorage', 'node-fetch', 'asn1.js', 'jwk-to-pem'],
external: ['crypto', 'zlib', 'node-localstorage', 'node-fetch', 'asn1.js'],
transform: [
["babelify", {
global: true,
only: /^(?:.*\/node_modules\/asmcrypto\.js\/|(?!.*\/node_modules\/)).*$/, // Only babelify asmcrypto in node_modules
// Only babelify asmcrypto in node_modules
only: /^(?:.*\/node_modules\/asmcrypto\.js\/|(?!.*\/node_modules\/)).*$/,
plugins: ["transform-async-to-generator",
"syntax-async-functions",
"transform-regenerator",
@ -75,11 +76,12 @@ module.exports = function(grunt) {
debug: true,
standalone: 'openpgp'
},
external: ['crypto', 'zlib', 'node-localstorage', 'node-fetch', 'asn1.js', 'jwk-to-pem'],
external: ['crypto', 'zlib', 'node-localstorage', 'node-fetch', 'asn1.js'],
transform: [
["babelify", {
global: true,
only: /^(?:.*\/node_modules\/asmcrypto\.js\/|(?!.*\/node_modules\/)).*$/, // Only babelify asmcrypto in node_modules
// Only babelify asmcrypto in node_modules
only: /^(?:.*\/node_modules\/asmcrypto\.js\/|(?!.*\/node_modules\/)).*$/,
plugins: ["transform-async-to-generator",
"syntax-async-functions",
"transform-regenerator",

View File

@ -79,7 +79,6 @@
"compressjs": "github:openpgpjs/compressjs",
"elliptic": "github:openpgpjs/elliptic",
"hash.js": "^1.1.3",
"jwk-to-pem": "^1.2.6",
"node-fetch": "^1.7.3",
"node-localstorage": "~1.3.0",
"pako": "^1.0.6",

View File

@ -56,7 +56,7 @@ if (nodeCrypto) {
const curves = {
p256: {
oid: util.Uint8Array_to_str([0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]),
oid: [0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07],
keyType: enums.publicKey.ecdsa,
hash: enums.hash.sha256,
cipher: enums.symmetric.aes128,
@ -65,7 +65,7 @@ const curves = {
payloadSize: 32
},
p384: {
oid: util.Uint8Array_to_str([0x2B, 0x81, 0x04, 0x00, 0x22]),
oid: [0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22],
keyType: enums.publicKey.ecdsa,
hash: enums.hash.sha384,
cipher: enums.symmetric.aes192,
@ -74,7 +74,7 @@ const curves = {
payloadSize: 48
},
p521: {
oid: util.Uint8Array_to_str([0x2B, 0x81, 0x04, 0x00, 0x23]),
oid: [0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x23],
keyType: enums.publicKey.ecdsa,
hash: enums.hash.sha512,
cipher: enums.symmetric.aes256,
@ -83,80 +83,83 @@ const curves = {
payloadSize: 66
},
secp256k1: {
oid: util.Uint8Array_to_str([0x2B, 0x81, 0x04, 0x00, 0x0A]),
oid: [0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x0A],
keyType: enums.publicKey.ecdsa,
hash: enums.hash.sha256,
cipher: enums.symmetric.aes128,
node: false // FIXME when we replace jwk-to-pem or it supports this curve
node: nodeCurves.secp256k1
},
ed25519: {
oid: util.Uint8Array_to_str([0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01]),
oid: [0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01],
keyType: enums.publicKey.eddsa,
hash: enums.hash.sha512,
payloadSize: 32
},
curve25519: {
oid: util.Uint8Array_to_str([0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01]),
oid: [0x06, 0x08, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01],
keyType: enums.publicKey.ecdsa,
hash: enums.hash.sha256,
cipher: enums.symmetric.aes128
},
brainpoolP256r1: { // TODO 1.3.36.3.3.2.8.1.1.7
oid: util.Uint8Array_to_str([0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07])
oid: [0x06, 0x07, 0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x07]
},
brainpoolP384r1: { // TODO 1.3.36.3.3.2.8.1.1.11
oid: util.Uint8Array_to_str([0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0B])
oid: [0x06, 0x07, 0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0B]
},
brainpoolP512r1: { // TODO 1.3.36.3.3.2.8.1.1.13
oid: util.Uint8Array_to_str([0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0D])
oid: [0x06, 0x07, 0x2B, 0x24, 0x03, 0x03, 0x02, 0x08, 0x01, 0x01, 0x0D]
}
};
export default function Curve(oid_or_name, params) {
if (OID.prototype.isPrototypeOf(oid_or_name) &&
enums.curve[oid_or_name.toHex()]) {
this.name = oid_or_name.toHex(); // by curve OID
} else if (enums.curve[oid_or_name]) {
this.name = oid_or_name; // by curve name
} else if (enums.curve[util.str_to_hex(oid_or_name)]) {
this.name = util.str_to_hex(oid_or_name); // by oid string
} else {
try {
if (util.isArray(oid_or_name) ||
util.isUint8Array(oid_or_name)) {
// by oid byte array
oid_or_name = new OID(oid_or_name);
}
if (oid_or_name instanceof OID) {
// by curve OID
oid_or_name = oid_or_name.getName();
}
// by curve name or oid string
this.name = enums.write(enums.curve, oid_or_name);
} catch (err) {
throw new Error('Not valid curve');
}
this.name = enums.write(enums.curve, this.name);
this.oid = new OID(curves[this.name].oid);
params = params || curves[this.name];
this.keyType = params.keyType;
switch (this.keyType) {
case enums.publicKey.eddsa:
this.curve = new EdDSA(this.name);
break;
case enums.publicKey.ecdsa:
this.curve = new EC(this.name);
break;
case enums.publicKey.eddsa:
this.curve = new EdDSA(this.name);
break;
default:
throw new Error('Unknown elliptic key type;');
}
this.oid = params.oid;
this.hash = params.hash;
this.cipher = params.cipher;
this.node = params.node && curves[this.name].node;
this.web = params.web && curves[this.name].web;
this.payloadSize = curves[this.name].payloadSize;
this.node = params.node && curves[this.name];
this.web = params.web && curves[this.name];
this.payloadSize = params.payloadSize;
}
Curve.prototype.keyFromPrivate = function (priv) { // Not for ed25519
return new KeyPair(this.curve, { priv: priv });
return new KeyPair(this, { priv: priv });
};
Curve.prototype.keyFromSecret = function (secret) { // Only for ed25519
return new KeyPair(this.curve, { secret: secret });
return new KeyPair(this, { secret: secret });
};
Curve.prototype.keyFromPublic = function (pub) {
return new KeyPair(this.curve, { pub: pub });
return new KeyPair(this, { pub: pub });
};
Curve.prototype.genKeyPair = async function () {
@ -174,7 +177,9 @@ Curve.prototype.genKeyPair = async function () {
if (!keyPair || !keyPair.priv) {
// elliptic fallback
const r = await this.curve.genKeyPair({ entropy: util.Uint8Array_to_str(random.getRandomBytes(32)) });
const r = await this.curve.genKeyPair({
entropy: util.Uint8Array_to_str(random.getRandomBytes(32))
});
const compact = this.curve.curve.type === 'edwards' || this.curve.curve.type === 'mont';
if (this.keyType === enums.publicKey.eddsa) {
keyPair = { secret: r.getSecret() };
@ -182,14 +187,14 @@ Curve.prototype.genKeyPair = async function () {
keyPair = { pub: r.getPublic('array', compact), priv: r.getPrivate().toArray() };
}
}
return new KeyPair(this.curve, keyPair);
return new KeyPair(this, keyPair);
};
async function generate(curve) {
curve = new Curve(curve);
const keyPair = await curve.genKeyPair();
return {
oid: curve.oid,
oid: new OID(curve.oid.slice(2)),
Q: new BN(keyPair.getPublic()),
d: new BN(keyPair.getPrivate()),
hash: curve.hash,
@ -230,6 +235,7 @@ async function webGenKeyPair(name) {
}
async function nodeGenKeyPair(name) {
// Note: ECDSA and ECDH key generation is structurally equivalent
const ecdh = nodeCrypto.createECDH(nodeCurves[name]);
await ecdh.generateKeys();

View File

@ -29,7 +29,7 @@
*/
import BN from 'bn.js';
import { webCurves, nodeCurves } from './curves';
import { curves, webCurves, nodeCurves } from './curves';
import hash from '../../hash';
import util from '../../../util';
import enums from '../../../enums';
@ -37,19 +37,10 @@ import enums from '../../../enums';
const webCrypto = util.getWebCrypto();
const nodeCrypto = util.getNodeCrypto();
const jwkToPem = nodeCrypto ? require('jwk-to-pem') : undefined;
const ECDSASignature = nodeCrypto ?
require('asn1.js').define('ECDSASignature', function() {
this.seq().obj(
this.key('r').int(),
this.key('s').int()
);
}) : undefined;
export default function KeyPair(curve, options) {
this.curve = curve;
this.keyType = curve.curve.type === 'edwards' ? enums.publicKey.eddsa : enums.publicKey.ecdsa;
this.keyPair = this.curve.keyPair(options);
this.keyPair = this.curve.curve.keyPair(options);
}
KeyPair.prototype.sign = async function (message, hash_algo) {
@ -90,12 +81,13 @@ KeyPair.prototype.derive = function (pub) {
};
KeyPair.prototype.getPublic = function () {
const compact = (this.curve.curve.type === 'edwards' || this.curve.curve.type === 'mont');
const compact = this.curve.curve.curve.type === 'edwards' ||
this.curve.curve.curve.type === 'mont';
return this.keyPair.getPublic('array', compact);
};
KeyPair.prototype.getPrivate = function () {
if (this.keyType === enums.publicKey.eddsa) {
if (this.curve.keyType === enums.publicKey.eddsa) {
return this.keyPair.getSecret();
}
return this.keyPair.getPrivate().toArray();
@ -110,15 +102,15 @@ KeyPair.prototype.getPrivate = function () {
async function webSign(curve, hash_algo, message, keyPair) {
const l = curve.payloadSize;
const len = curve.payloadSize;
const key = await webCrypto.importKey(
"jwk",
{
"kty": "EC",
"crv": webCurves[curve.name],
"x": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPublic().getX().toArray('be', l)), true),
"y": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPublic().getY().toArray('be', l)), true),
"d": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPrivate().toArray('be', l)), true),
"x": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPublic().getX().toArray('be', len)), true),
"y": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPublic().getY().toArray('be', len)), true),
"d": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPrivate().toArray('be', len)), true),
"use": "sig",
"kid": "ECDSA Private Key"
},
@ -141,23 +133,20 @@ async function webSign(curve, hash_algo, message, keyPair) {
message
));
return {
r: signature.slice(0, l),
s: signature.slice(l, 2 * l)
r: signature.slice(0, len),
s: signature.slice(len, len << 1)
};
}
async function webVerify(curve, hash_algo, { r, s }, message, publicKey) {
const l = curve.payloadSize;
r = Array(l - r.length).fill(0).concat(r);
s = Array(l - s.length).fill(0).concat(s);
const signature = new Uint8Array(r.concat(s)).buffer;
const len = curve.payloadSize;
const key = await webCrypto.importKey(
"jwk",
{
"kty": "EC",
"crv": webCurves[curve.name],
"x": util.Uint8Array_to_b64(new Uint8Array(publicKey.getX().toArray('be', l)), true),
"y": util.Uint8Array_to_b64(new Uint8Array(publicKey.getY().toArray('be', l)), true),
"x": util.Uint8Array_to_b64(new Uint8Array(publicKey.getX().toArray('be', len)), true),
"y": util.Uint8Array_to_b64(new Uint8Array(publicKey.getY().toArray('be', len)), true),
"use": "sig",
"kid": "ECDSA Public Key"
},
@ -170,6 +159,10 @@ async function webVerify(curve, hash_algo, { r, s }, message, publicKey) {
["verify"]
);
r = [].concat(Array(len - r.length).fill(0), r);
s = [].concat(Array(len - s.length).fill(0), s);
const signature = new Uint8Array([].concat(r, s)).buffer;
return webCrypto.verify(
{
"name": 'ECDSA',
@ -182,58 +175,88 @@ async function webVerify(curve, hash_algo, { r, s }, message, publicKey) {
);
}
async function nodeSign(curve, hash_algo, message, keyPair) {
console.log({
"kty": "EC",
"crv": webCurves[curve.name],
"x": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPublic().getX().toArray())),
"y": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPublic().getY().toArray())),
"d": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPrivate().toArray())),
"use": "sig",
"kid": "ECDSA Private Key"
});
const key = jwkToPem(
{
"kty": "EC",
"crv": webCurves[curve.name],
"x": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPublic().getX().toArray())),
"y": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPublic().getY().toArray())),
"d": util.Uint8Array_to_b64(new Uint8Array(keyPair.getPrivate().toArray())),
"use": "sig",
"kid": "ECDSA Private Key"
},
{ private: true }
);
const sign = nodeCrypto.createSign(enums.read(enums.hash, hash_algo));
sign.write(message);
sign.end();
const signature = await ECDSASignature.decode(sign.sign(key), 'der');
return {
r: signature.r.toArray(),
s: signature.s.toArray()
};
const key = ECPrivateKey.encode({
version: 1,
parameters: curve.oid,
privateKey: keyPair.getPrivate().toArray(),
publicKey: { unused: 0, data: keyPair.getPublic().encode() }
}, 'pem', {
label: 'EC PRIVATE KEY'
});
return ECDSASignature.decode(sign.sign(key), 'der');
}
async function nodeVerify(curve, hash_algo, { r, s }, message, publicKey) {
const signature = ECDSASignature.encode({ r: new BN(r), s: new BN(s) }, 'der');
const key = jwkToPem(
{
"kty": "EC",
"crv": webCurves[curve.name],
"x": util.Uint8Array_to_b64(new Uint8Array(publicKey.getX().toArray())),
"y": util.Uint8Array_to_b64(new Uint8Array(publicKey.getY().toArray())),
"use": "sig",
"kid": "ECDSA Public Key"
},
{ private: false }
);
const verify = nodeCrypto.createVerify(enums.read(enums.hash, hash_algo));
verify.write(message);
verify.end();
const result = await verify.verify(key, signature);
return result;
const key = SubjectPublicKeyInfo.encode({
algorithm: {
algorithm: [1, 2, 840, 10045, 2, 1],
parameters: curve.oid
},
subjectPublicKey: { unused: 0, data: publicKey.encode() }
}, 'pem', {
label: 'PUBLIC KEY'
});
const signature = ECDSASignature.encode({
r: new BN(r), s: new BN(s)
}, 'der');
try {
return verify.verify(key, signature);
} catch (err) {
return false;
}
}
const asn1 = nodeCrypto ? require('asn1.js') : undefined;
const ECDSASignature = nodeCrypto ?
asn1.define('ECDSASignature', function() {
this.seq().obj(
this.key('r').int(),
this.key('s').int()
);
}) : undefined;
const ECParameters = nodeCrypto ?
asn1.define('ECParameters', function() {
this.choice({
namedCurve: this.objid()
});
}) : undefined;
const ECPrivateKey = nodeCrypto ?
asn1.define('ECPrivateKey', function() {
this.seq().obj(
this.key('version').int(),
this.key('privateKey').octstr(),
this.key('parameters').explicit(0).optional().any(),
this.key('publicKey').explicit(1).optional().bitstr()
);
}) : undefined;
const AlgorithmIdentifier = nodeCrypto ?
asn1.define('AlgorithmIdentifier', function() {
this.seq().obj(
this.key('algorithm').objid(),
this.key('parameters').optional().any()
);
}) : undefined;
const SubjectPublicKeyInfo = nodeCrypto ?
asn1.define('SubjectPublicKeyInfo', function() {
this.seq().obj(
this.key('algorithm').use(AlgorithmIdentifier),
this.key('subjectPublicKey').bitstr()
);
}) : undefined;

View File

@ -41,7 +41,9 @@ import util from '../util';
*/
export default function MPI(data) {
/** An implementation dependent integer */
if (BN.isBN(data)) {
if (data instanceof MPI) {
this.data = data.data;
} else if (BN.isBN(data)) {
this.fromBN(data);
} else if (util.isUint8Array(data)) {
this.fromUint8Array(data);

View File

@ -18,8 +18,17 @@
/**
* Wrapper to an OID value
*
* An object identifier type from
* {@link https://tools.ietf.org/html/rfc6637#section-11|RFC6637, section 11}.
* {@link https://tools.ietf.org/html/rfc6637#section-11|RFC6637, section 11}:
* The sequence of octets in the third column is the result of applying
* the Distinguished Encoding Rules (DER) to the ASN.1 Object Identifier
* with subsequent truncation. The truncation removes the two fields of
* encoded Object Identifier. The first omitted field is one octet
* representing the Object Identifier tag, and the second omitted field
* is the length of the Object Identifier body. For example, the
* complete ASN.1 DER encoding for the NIST P-256 curve OID is "06 08 2A
* 86 48 CE 3D 03 01 07", from which the first entry in the table above
* is constructed by omitting the first two octets. Only the truncated
* sequence of octets is the valid representation of a curve OID.
* @requires util
* @requires enums
* @module type/oid
@ -33,15 +42,16 @@ import enums from '../enums';
*/
function OID(oid) {
if (oid instanceof OID) {
oid = oid.oid;
} else if (typeof oid === 'undefined') {
oid = '';
} else if (util.isArray(oid)) {
oid = util.Uint8Array_to_str(oid);
} else if (util.isUint8Array(oid)) {
oid = util.Uint8Array_to_str(oid);
this.oid = oid.oid;
} else if (util.isArray(oid) ||
util.isUint8Array(oid)) {
if (oid[0] === 0x06) { // DER encoded oid byte array
oid = oid.slice(2);
}
this.oid = util.Uint8Array_to_str(oid);
} else {
this.oid = '';
}
this.oid = oid;
}
/**

View File

@ -18,10 +18,12 @@
/**
* This object contains utility functions
* @requires config
* @requires encoding/base64
* @module util
*/
import config from './config';
import b64 from './encoding/base64';
export default {
@ -157,8 +159,8 @@ export default {
* @return {Uint8Array} An array of 8-bit integers
*/
b64_to_Uint8Array: function (base64) {
const str = atob(base64.replace(/\-/g, '+').replace(/_/g, '/'));
return this.str_to_Uint8Array(str);
// atob(base64.replace(/\-/g, '+').replace(/_/g, '/'));
return b64.decode(base64.replace(/\-/g, '+').replace(/_/g, '/'));
},
/**
@ -168,8 +170,8 @@ export default {
* @return {String} Base-64 encoded string
*/
Uint8Array_to_b64: function (bytes, url) {
const base64 = btoa(this.Uint8Array_to_str(bytes));
return url ? base64.replace(/\+/g, '-').replace(/\//g, '_') : base64;
// btoa(this.Uint8Array_to_str(bytes)).replace(/\+/g, '-').replace(/\//g, '_');
return b64.encode(bytes, url).replace('\n', '');
},
/**

View File

@ -144,7 +144,7 @@ describe('Elliptic Curve Cryptography', function () {
it('Creating curve from oid', function (done) {
const oids = ['2A8648CE3D030107', '2B81040022', '2B81040023', '2B8104000A'];
oids.forEach(function (oid) {
expect(new elliptic_curves.Curve(openpgp.util.hex_to_str(oid))).to.exist;
expect(new elliptic_curves.Curve(oid)).to.exist;
});
done();
});
@ -173,21 +173,25 @@ describe('Elliptic Curve Cryptography', function () {
it('Signature verification', function (done) {
const curve = new elliptic_curves.Curve('p256');
const key = curve.keyFromPublic(signature_data.pub);
expect(key.verify(signature_data.message, signature_data.signature, 8)).to.eventually.be.true;
done();
expect(
key.verify(signature_data.message, signature_data.signature, 8)
).to.eventually.be.true.notify(done);
});
it('Invalid signature', function (done) {
const curve = new elliptic_curves.Curve('p256');
const key = curve.keyFromPublic(key_data.p256.pub);
expect(key.verify(signature_data.message, signature_data.signature, 8)).to.eventually.be.false;
done();
expect(
key.verify(signature_data.message, signature_data.signature, 8)
).to.eventually.be.false.notify(done);
});
it('Signature generation', function () {
const curve = new elliptic_curves.Curve('p256');
let key = curve.keyFromPrivate(key_data.p256.priv);
return key.sign(signature_data.message, 8).then(signature => {
key = curve.keyFromPublic(key_data.p256.pub);
expect(key.verify(signature_data.message, signature, 8)).to.eventually.be.true;
expect(
key.verify(signature_data.message, signature, 8)
).to.eventually.be.true;
});
});
it('Shared secret generation', function (done) {
@ -312,7 +316,7 @@ describe('Elliptic Curve Cryptography', function () {
return Promise.resolve().then(() => {
const curve = new elliptic_curves.Curve(oid);
return elliptic_curves.ecdh.decrypt(
curve.oid,
new openpgp.OID(curve.oid),
cipher,
hash,
new Uint8Array(ephemeral),

View File

@ -524,7 +524,7 @@ describe('X25519 Cryptography', function () {
/* TODO how does GPG2 accept this?
it('Should handle little-endian parameters in EdDSA', function () {
var pubKey = [
const pubKey = [
'-----BEGIN PGP PUBLIC KEY BLOCK-----',
'Version: OpenPGP.js v3.0.0',
'Comment: https://openpgpjs.org',
@ -539,12 +539,12 @@ describe('X25519 Cryptography', function () {
'Gbm1oe83ZB+0aSp5m34YkpHQNb80y8PGFy7nIexiAA==',
'=xeG/',
'-----END PGP PUBLIC KEY BLOCK-----'].join('\n');
var hi = openpgp.key.readArmored(pubKey).keys[0];
const hi = openpgp.key.readArmored(pubKey).keys[0];
return hi.verifyPrimaryUser().then(() => {
var results = hi.getPrimaryUser();
const results = hi.getPrimaryUser();
expect(results).to.exist;
expect(results.user).to.exist;
var user = results.user;
const user = results.user;
expect(user.selfCertifications[0].verify(
hi.primaryKey, {userid: user.userId, key: hi.primaryKey}
)).to.eventually.be.true;