Replace indutny-elliptic lib with noble-curves

Unlike elliptic, noble-curves targets algorithmic constant time, and
it relies on the native BigInts when available, resulting in a smaller bundle
and improved performance.

Also, expand testing of fallback elliptic implementation.
This commit is contained in:
larabr 2023-10-06 16:14:59 +02:00
parent 01df8ca889
commit a9fae5ff12
10 changed files with 204 additions and 345 deletions

97
package-lock.json generated
View File

@ -13,7 +13,6 @@
},
"devDependencies": {
"@openpgp/asmcrypto.js": "^3.0.0",
"@openpgp/elliptic": "^6.5.1",
"@openpgp/jsdoc": "^3.6.11",
"@openpgp/noble-curves": "^1.2.1-0",
"@openpgp/noble-hashes": "^1.3.3-0",
@ -572,21 +571,6 @@
"integrity": "sha512-X/DPYy7uHe+dlY2Botb99uXwb2kXR6HTv0hQOnnI0TVEqOIMQyzCDWAzlX00AacsYryDAphuOndg6mk6wtJCNg==",
"dev": true
},
"node_modules/@openpgp/elliptic": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@openpgp/elliptic/-/elliptic-6.5.1.tgz",
"integrity": "sha512-VR20QWndMXoZTAzCUqauDT4dLrHO4RTnyVV3szuRHllQSU/JZToLvWtFxpEQth4XWyqlxHPwq7tljE5V97+n1g==",
"dev": true,
"dependencies": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
}
},
"node_modules/@openpgp/jsdoc": {
"version": "3.6.11",
"resolved": "https://registry.npmjs.org/@openpgp/jsdoc/-/jsdoc-3.6.11.tgz",
@ -1489,12 +1473,6 @@
"node": ">=8"
}
},
"node_modules/brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
"dev": true
},
"node_modules/browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
@ -3465,16 +3443,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash.js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz",
"integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.0"
}
},
"node_modules/hasha": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz",
@ -3496,17 +3464,6 @@
"he": "bin/he"
}
},
"node_modules/hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"dev": true,
"dependencies": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@ -4975,12 +4932,6 @@
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz",
"integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M="
},
"node_modules/minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
"dev": true
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -7993,21 +7944,6 @@
"integrity": "sha512-X/DPYy7uHe+dlY2Botb99uXwb2kXR6HTv0hQOnnI0TVEqOIMQyzCDWAzlX00AacsYryDAphuOndg6mk6wtJCNg==",
"dev": true
},
"@openpgp/elliptic": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/@openpgp/elliptic/-/elliptic-6.5.1.tgz",
"integrity": "sha512-VR20QWndMXoZTAzCUqauDT4dLrHO4RTnyVV3szuRHllQSU/JZToLvWtFxpEQth4XWyqlxHPwq7tljE5V97+n1g==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
}
},
"@openpgp/jsdoc": {
"version": "3.6.11",
"resolved": "https://registry.npmjs.org/@openpgp/jsdoc/-/jsdoc-3.6.11.tgz",
@ -8699,12 +8635,6 @@
"fill-range": "^7.0.1"
}
},
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
"dev": true
},
"browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
@ -10238,16 +10168,6 @@
"has-symbols": "^1.0.2"
}
},
"hash.js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz",
"integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.0"
}
},
"hasha": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz",
@ -10263,17 +10183,6 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"dev": true,
"requires": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@ -11425,12 +11334,6 @@
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz",
"integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M="
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
"dev": true
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",

View File

@ -64,7 +64,6 @@
"devDependencies": {
"@openpgp/asmcrypto.js": "^3.0.0",
"@openpgp/noble-curves": "^1.2.1-0",
"@openpgp/elliptic": "^6.5.1",
"@openpgp/jsdoc": "^3.6.11",
"@openpgp/noble-hashes": "^1.3.3-0",
"@openpgp/seek-bzip": "^1.0.5-git",

View File

@ -21,7 +21,7 @@
*/
import nacl from '@openpgp/tweetnacl/nacl-fast-light';
import { CurveWithOID, jwkToRawPublic, rawPublicToJWK, privateToJWK, validateStandardParams } from './oid_curves';
import { CurveWithOID, jwkToRawPublic, rawPublicToJWK, privateToJWK, validateStandardParams, getNobleCurve } from './oid_curves';
import * as aesKW from '../../aes_kw';
import { getRandomBytes } from '../../random';
import hash from '../../hash';
@ -29,7 +29,6 @@ import enums from '../../../enums';
import util from '../../../util';
import { b64ToUint8Array } from '../../../encoding/base64';
import * as pkcs5 from '../../pkcs5';
import { keyFromPublic, keyFromPrivate, getIndutnyCurve } from './indutnyKey';
import getCipher from '../../cipher/getCipher';
const webCrypto = util.getWebCrypto();
@ -105,13 +104,16 @@ async function genPublicEphemeralKey(curve, Q) {
return await webPublicEphemeralKey(curve, Q);
} catch (err) {
util.printDebugError(err);
return jsPublicEphemeralKey(curve, Q);
}
}
break;
case 'node':
return nodePublicEphemeralKey(curve, Q);
default: {
return jsPublicEphemeralKey(curve, Q);
}
}
return ellipticPublicEphemeralKey(curve, Q);
}
/**
@ -165,13 +167,16 @@ async function genPrivateEphemeralKey(curve, V, Q, d) {
return await webPrivateEphemeralKey(curve, V, Q, d);
} catch (err) {
util.printDebugError(err);
return jsPrivateEphemeralKey(curve, V, d);
}
}
break;
case 'node':
return nodePrivateEphemeralKey(curve, V, d);
default: {
return jsPrivateEphemeralKey(curve, V, d);
}
}
return ellipticPrivateEphemeralKey(curve, V, d);
}
/**
@ -205,6 +210,24 @@ export async function decrypt(oid, kdfParams, V, C, Q, d, fingerprint) {
throw err;
}
function jsPrivateEphemeralKey(curve, V, d) {
const nobleCurve = getNobleCurve(curve.name);
// The output includes parity byte
const sharedSecretWithParity = nobleCurve.getSharedSecret(d, V);
const sharedKey = sharedSecretWithParity.subarray(1);
return { secretKey: d, sharedKey };
}
async function jsPublicEphemeralKey(curve, Q) {
const nobleCurve = getNobleCurve(curve.name);
const { publicKey: V, privateKey: v } = await curve.genKeyPair();
// The output includes parity byte
const sharedSecretWithParity = nobleCurve.getSharedSecret(v, Q);
const sharedKey = sharedSecretWithParity.subarray(1);
return { publicKey: V, sharedKey };
}
/**
* Generate ECDHE secret from private key and public part of ephemeral key using webCrypto
*
@ -306,46 +329,6 @@ async function webPublicEphemeralKey(curve, Q) {
return { publicKey, sharedKey };
}
/**
* Generate ECDHE secret from private key and public part of ephemeral key using indutny/elliptic
*
* @param {CurveWithOID} curve - Elliptic curve object
* @param {Uint8Array} V - Public part of ephemeral key
* @param {Uint8Array} d - Recipient private key
* @returns {Promise<{secretKey: Uint8Array, sharedKey: Uint8Array}>}
* @async
*/
async function ellipticPrivateEphemeralKey(curve, V, d) {
const indutnyCurve = await getIndutnyCurve(curve.name);
V = keyFromPublic(indutnyCurve, V);
d = keyFromPrivate(indutnyCurve, d);
const secretKey = new Uint8Array(d.getPrivate());
const S = d.derive(V.getPublic());
const len = indutnyCurve.curve.p.byteLength();
const sharedKey = S.toArrayLike(Uint8Array, 'be', len);
return { secretKey, sharedKey };
}
/**
* Generate ECDHE ephemeral key and secret from public key using indutny/elliptic
*
* @param {CurveWithOID} curve - Elliptic curve object
* @param {Uint8Array} Q - Recipient public key
* @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>}
* @async
*/
async function ellipticPublicEphemeralKey(curve, Q) {
const indutnyCurve = await getIndutnyCurve(curve.name);
const v = await curve.genKeyPair();
Q = keyFromPublic(indutnyCurve, Q);
const V = keyFromPrivate(indutnyCurve, v.privateKey);
const publicKey = v.publicKey;
const S = V.derive(Q.getPublic());
const len = indutnyCurve.curve.p.byteLength();
const sharedKey = S.toArrayLike(Uint8Array, 'be', len);
return { publicKey, sharedKey };
}
/**
* Generate ECDHE secret from private key and public part of ephemeral key using nodeCrypto
*

View File

@ -24,8 +24,7 @@ import enums from '../../../enums';
import util from '../../../util';
import { getRandomBytes } from '../../random';
import hash from '../../hash';
import { CurveWithOID, webCurves, privateToJWK, rawPublicToJWK, validateStandardParams } from './oid_curves';
import { getIndutnyCurve, keyFromPrivate, keyFromPublic } from './indutnyKey';
import { CurveWithOID, webCurves, privateToJWK, rawPublicToJWK, validateStandardParams, getNobleCurve } from './oid_curves';
const webCrypto = util.getWebCrypto();
const nodeCrypto = util.getNodeCrypto();
@ -74,7 +73,14 @@ export async function sign(oid, hashAlgo, message, publicKey, privateKey, hashed
}
}
}
return ellipticSign(curve, hashed, privateKey);
const nobleCurve = getNobleCurve(curve.name);
// lowS: non-canonical sig: https://stackoverflow.com/questions/74338846/ecdsa-signature-verification-mismatch
const signature = nobleCurve.sign(hashed, privateKey, { lowS: false });
return {
r: signature.r.toUint8Array('be', curve.payloadSize),
s: signature.s.toUint8Array('be', curve.payloadSize)
};
}
/**
@ -111,8 +117,10 @@ export async function verify(oid, hashAlgo, signature, message, publicKey, hashe
return nodeVerify(curve, hashAlgo, signature, message, publicKey);
}
}
const digest = (typeof hashAlgo === 'undefined') ? message : hashed;
return ellipticVerify(curve, signature, digest, publicKey);
const nobleCurve = getNobleCurve(curve.name);
// lowS: non-canonical sig: https://stackoverflow.com/questions/74338846/ecdsa-signature-verification-mismatch
return nobleCurve.verify(util.concatUint8Array([signature.r, signature.s]), hashed, publicKey, { lowS: false });
}
/**
@ -156,23 +164,6 @@ export async function validateParams(oid, Q, d) {
// Helper functions //
// //
//////////////////////////
async function ellipticSign(curve, hashed, privateKey) {
const indutnyCurve = await getIndutnyCurve(curve.name);
const key = keyFromPrivate(indutnyCurve, privateKey);
const signature = key.sign(hashed);
return {
r: signature.r.toArrayLike(Uint8Array),
s: signature.s.toArrayLike(Uint8Array)
};
}
async function ellipticVerify(curve, signature, digest, publicKey) {
const indutnyCurve = await getIndutnyCurve(curve.name);
const key = keyFromPublic(indutnyCurve, publicKey);
return key.verify(digest, signature);
}
async function webSign(curve, hashAlgo, message, keyPair) {
const len = curve.payloadSize;
const jwk = privateToJWK(curve.payloadSize, webCurves[curve.name], keyPair.publicKey, keyPair.privateKey);

View File

@ -1,44 +0,0 @@
// OpenPGP.js - An OpenPGP implementation in javascript
// Copyright (C) 2015-2016 Decentral
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3.0 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
/**
* @fileoverview Wrapper for a KeyPair of an curve from indutny/elliptic library
* @module crypto/public_key/elliptic/indutnyKey
*/
import config from '../../../config';
export function keyFromPrivate(indutnyCurve, priv) {
const keyPair = indutnyCurve.keyPair({ priv: priv });
return keyPair;
}
export function keyFromPublic(indutnyCurve, pub) {
const keyPair = indutnyCurve.keyPair({ pub: pub });
if (keyPair.validate().result !== true) {
throw new Error('Invalid elliptic public key');
}
return keyPair;
}
export async function getIndutnyCurve(name) {
if (!config.useIndutnyElliptic) {
throw new Error('This curve is only supported in the full build of OpenPGP.js');
}
const { default: elliptic } = await import('@openpgp/elliptic');
return new elliptic.ec(name);
}

View File

@ -19,37 +19,58 @@
* @fileoverview Wrapper of an instance of an Elliptic Curve
* @module crypto/public_key/elliptic/curve
*/
import { BigInteger } from '@openpgp/noble-hashes/biginteger';
import nacl from '@openpgp/tweetnacl/nacl-fast-light';
import { p256 } from '@openpgp/noble-curves/p256';
import { p384 } from '@openpgp/noble-curves/p384';
import { p521 } from '@openpgp/noble-curves/p521';
import { brainpoolP256r1 } from '@openpgp/noble-curves/brainpoolP256r1';
import { brainpoolP384r1 } from '@openpgp/noble-curves/brainpoolP384r1';
import { brainpoolP512r1 } from '@openpgp/noble-curves/brainpoolP512r1';
import { secp256k1 } from '@openpgp/noble-curves/secp256k1';
import { getRandomBytes } from '../../random';
import enums from '../../../enums';
import util from '../../../util';
import { uint8ArrayToB64, b64ToUint8Array } from '../../../encoding/base64';
import OID from '../../../type/oid';
import { keyFromPublic, keyFromPrivate, getIndutnyCurve } from './indutnyKey';
import { UnsupportedError } from '../../../packet/packet';
const webCrypto = util.getWebCrypto();
const nodeCrypto = util.getNodeCrypto();
const webCurves = {
'p256': 'P-256',
'p384': 'P-384',
'p521': 'P-521'
[enums.curve.p256]: 'P-256',
[enums.curve.p384]: 'P-384',
[enums.curve.p521]: 'P-521'
};
const knownCurves = nodeCrypto ? nodeCrypto.getCurves() : [];
const nodeCurves = nodeCrypto ? {
secp256k1: knownCurves.includes('secp256k1') ? 'secp256k1' : undefined,
p256: knownCurves.includes('prime256v1') ? 'prime256v1' : undefined,
p384: knownCurves.includes('secp384r1') ? 'secp384r1' : undefined,
p521: knownCurves.includes('secp521r1') ? 'secp521r1' : undefined,
ed25519: knownCurves.includes('ED25519') ? 'ED25519' : undefined,
curve25519: knownCurves.includes('X25519') ? 'X25519' : undefined,
brainpoolP256r1: knownCurves.includes('brainpoolP256r1') ? 'brainpoolP256r1' : undefined,
brainpoolP384r1: knownCurves.includes('brainpoolP384r1') ? 'brainpoolP384r1' : undefined,
brainpoolP512r1: knownCurves.includes('brainpoolP512r1') ? 'brainpoolP512r1' : undefined
[enums.curve.secp256k1]: knownCurves.includes('secp256k1') ? 'secp256k1' : undefined,
[enums.curve.p256]: knownCurves.includes('prime256v1') ? 'prime256v1' : undefined,
[enums.curve.p384]: knownCurves.includes('secp384r1') ? 'secp384r1' : undefined,
[enums.curve.p521]: knownCurves.includes('secp521r1') ? 'secp521r1' : undefined,
[enums.curve.ed25519Legacy]: knownCurves.includes('ED25519') ? 'ED25519' : undefined,
[enums.curve.curve25519Legacy]: knownCurves.includes('X25519') ? 'X25519' : undefined,
[enums.curve.brainpoolP256r1]: knownCurves.includes('brainpoolP256r1') ? 'brainpoolP256r1' : undefined,
[enums.curve.brainpoolP384r1]: knownCurves.includes('brainpoolP384r1') ? 'brainpoolP384r1' : undefined,
[enums.curve.brainpoolP512r1]: knownCurves.includes('brainpoolP512r1') ? 'brainpoolP512r1' : undefined
} : {};
const nobleCurvess = {
[enums.curve.p256]: p256,
[enums.curve.p384]: p384,
[enums.curve.p521]: p521,
[enums.curve.secp256k1]: secp256k1,
[enums.curve.brainpoolP256r1]: brainpoolP256r1,
[enums.curve.brainpoolP384r1]: brainpoolP384r1,
[enums.curve.brainpoolP512r1]: brainpoolP512r1
};
export const getNobleCurve = curveName => {
const curve = nobleCurvess[curveName];
if (!curve) throw new Error('Unsupported curve');
return curve;
};
const curves = {
p256: {
oid: [0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07],
@ -169,14 +190,13 @@ class CurveWithOID {
}
async genKeyPair() {
let keyPair;
switch (this.type) {
case 'web':
try {
return await webGenKeyPair(this.name);
} catch (err) {
util.printDebugError('Browser did not support generating ec key ' + err.message);
break;
return jsGenKeyPair(this.name);
}
case 'node':
return nodeGenKeyPair(this.name);
@ -185,8 +205,8 @@ class CurveWithOID {
privateKey[0] = (privateKey[0] & 127) | 64;
privateKey[31] &= 248;
const secretKey = privateKey.slice().reverse();
keyPair = nacl.box.keyPair.fromSecretKey(secretKey);
const publicKey = util.concatUint8Array([new Uint8Array([0x40]), keyPair.publicKey]);
const { publicKey: rawPublicKey } = nacl.box.keyPair.fromSecretKey(secretKey);
const publicKey = util.concatUint8Array([new Uint8Array([0x40]), rawPublicKey]);
return { publicKey, privateKey };
}
case 'ed25519Legacy': {
@ -195,26 +215,23 @@ class CurveWithOID {
const publicKey = util.concatUint8Array([new Uint8Array([0x40]), keyPair.publicKey]);
return { publicKey, privateKey };
}
default: {
return jsGenKeyPair(this.name);
}
}
const indutnyCurve = await getIndutnyCurve(this.name);
keyPair = await indutnyCurve.genKeyPair({
entropy: util.uint8ArrayToString(getRandomBytes(32))
});
return { publicKey: new Uint8Array(keyPair.getPublic('array', false)), privateKey: keyPair.getPrivate().toArrayLike(Uint8Array) };
}
}
async function generate(curve) {
curve = new CurveWithOID(curve);
async function generate(curveName) {
const curve = new CurveWithOID(curveName);
const { oid, hash, cipher } = curve;
const keyPair = await curve.genKeyPair();
const Q = BigInteger.new(keyPair.publicKey).toUint8Array();
const secret = BigInteger.new(keyPair.privateKey).toUint8Array('be', curve.payloadSize);
return {
oid: curve.oid,
Q,
secret,
hash: curve.hash,
cipher: curve.cipher
oid,
Q: keyPair.publicKey,
secret: util.leftPad(keyPair.privateKey, curve.payloadSize),
hash,
cipher
};
}
@ -269,20 +286,13 @@ async function validateStandardParams(algo, oid, Q, d) {
return true;
}
const curve = await getIndutnyCurve(curveName);
try {
// Parse Q and check that it is on the curve but not at infinity
Q = keyFromPublic(curve, Q).getPublic();
} catch (validationErrors) {
return false;
}
/**
const nobleCurve = getNobleCurve(enums.write(enums.curve, oid.toHex()));
/*
* Re-derive public point Q' = dG from private key
* Expect Q == Q'
*/
const dG = keyFromPrivate(curve, d).getPublic();
if (!dG.eq(Q)) {
const dG = nobleCurve.getPublicKey(d, false);
if (!util.equalsUint8Array(dG, Q)) {
return false;
}
@ -298,7 +308,12 @@ export {
// Helper functions //
// //
//////////////////////////
function jsGenKeyPair(name) {
const nobleCurve = getNobleCurve(name);
const privateKey = nobleCurve.utils.randomPrivateKey();
const publicKey = nobleCurve.getPublicKey(privateKey, false);
return { publicKey, privateKey };
}
async function webGenKeyPair(name) {
// Note: keys generated with ECDSA and ECDH are structurally equivalent

View File

@ -77,7 +77,7 @@ export default () => describe('ECDH key exchange @lightweight', function () {
}
expect(decrypt_message(
'secp256k1', 2, 7, [], [], [], [], []
)).to.be.rejectedWith(Error, /Private key is not valid for specified curve|Unknown point format/).notify(done);
)).to.be.rejectedWith(Error, /Private key is not valid for specified curve|second arg must be public key/).notify(done);
});
it('Invalid elliptic public key', function (done) {
if (!openpgp.config.useIndutnyElliptic && !util.getNodeCrypto()) {
@ -85,7 +85,7 @@ export default () => describe('ECDH key exchange @lightweight', function () {
}
expect(decrypt_message(
'secp256k1', 2, 7, secp256k1_value, secp256k1_point, secp256k1_invalid_point, secp256k1_data, []
)).to.be.rejectedWith(Error, /Public key is not valid for specified curve|Failed to translate Buffer to a EC_POINT|Invalid elliptic public key/).notify(done);
)).to.be.rejectedWith(/Public key is not valid for specified curve|Failed to translate Buffer to a EC_POINT|bad point/).notify(done);
});
it('Invalid key data integrity', function (done) {
if (!openpgp.config.useIndutnyElliptic && !util.getNodeCrypto()) {
@ -93,7 +93,7 @@ export default () => describe('ECDH key exchange @lightweight', function () {
}
expect(decrypt_message(
'secp256k1', 2, 7, secp256k1_value, secp256k1_point, secp256k1_point, secp256k1_data, []
)).to.be.rejectedWith(Error, /Key Data Integrity failed/).notify(done);
)).to.be.rejectedWith(/Key Data Integrity failed/).notify(done);
});
const Q1 = new Uint8Array([
@ -143,9 +143,9 @@ export default () => describe('ECDH key exchange @lightweight', function () {
const oid = new OID(curve.oid);
const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher });
const data = util.stringToUint8Array('test');
expect(
ecdh.encrypt(oid, kdfParams, data, Q1, fingerprint1)
).to.be.rejectedWith(Error, /Public key is not valid for specified curve|Failed to translate Buffer to a EC_POINT|Unknown point format/);
await expect(
ecdh.encrypt(oid, kdfParams, data, Q1.subarray(1), fingerprint1)
).to.be.rejectedWith(/Public key is not valid for specified curve|Failed to translate Buffer to a EC_POINT|second arg must be public key/);
});
it('Different keys', async function () {
@ -199,8 +199,9 @@ export default () => describe('ECDH key exchange @lightweight', function () {
expect(await ecdhX.decrypt(openpgp.enums.publicKey.x448, ephemeralPublicKey, wrappedKey, K_B, b)).to.deep.equal(data);
});
['p256', 'p384', 'p521'].forEach(curveName => {
it(`NIST ${curveName} - Successful exchange`, async function () {
const allCurves = ['secp256k1', 'p256', 'p384', 'p521', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1'];
allCurves.forEach(curveName => {
it(`${curveName} - Successful exchange`, async function () {
const curve = new elliptic_curves.CurveWithOID(curveName);
const oid = new OID(curve.oid);
const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher });
@ -236,14 +237,16 @@ export default () => describe('ECDH key exchange @lightweight', function () {
getNodeCryptoStub && getNodeCryptoStub.restore();
};
['p256', 'p384', 'p521'].forEach(curveName => {
it(`NIST ${curveName}`, async function () {
allCurves.forEach(curveName => {
it(`${curveName}`, async function () {
const nodeCrypto = util.getNodeCrypto();
const webCrypto = util.getWebCrypto();
if (!nodeCrypto && !webCrypto) {
this.skip();
}
const expectNativeWeb = new Set(['p256', 'p384']); // older versions of safari do not implement p521
const curve = new elliptic_curves.CurveWithOID(curveName);
const oid = new OID(curve.oid);
const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher });
@ -254,9 +257,11 @@ export default () => describe('ECDH key exchange @lightweight', function () {
const nativeDecryptSpy = webCrypto ? sinonSandbox.spy(webCrypto, 'deriveBits') : sinonSandbox.spy(nodeCrypto, 'createECDH');
expect(await ecdh.decrypt(oid, kdfParams, V, C, Q, d, fingerprint1)).to.deep.equal(data);
const expectedNativeCallCount = nativeDecryptSpy.callCount;
disableNative();
expect(await ecdh.decrypt(oid, kdfParams, V, C, Q, d, fingerprint1)).to.deep.equal(data);
if (curveName !== 'p521') { // safari does not implement p521 in webcrypto
expect(nativeDecryptSpy.callCount).to.equal(expectedNativeCallCount); // assert that fallback implementation was called
if (expectNativeWeb.has(curveName)) {
expect(nativeDecryptSpy.calledOnce).to.be.true;
}
});

View File

@ -122,6 +122,21 @@ export default () => describe('Elliptic Curve Cryptography @lightweight', functi
getNodeCryptoStub && getNodeCryptoStub.restore();
};
const testNativeAndFallback = async fn => {
const webCrypto = util.getWebCrypto();
const nodeCrypto = util.getNodeCrypto();
const nativeSpy = webCrypto ? sinonSandbox.spy(webCrypto, 'importKey') : sinonSandbox.spy(nodeCrypto, 'createVerify'); // spy on function used on verification, since that's used by all tests calling `testNativeAndFallback`
// if native not available, fallback will be tested twice (not possible to automatically check native algo availability)
enableNative();
await fn();
const expectedNativeCallCount = nativeSpy.callCount;
disableNative();
await fn();
expect(nativeSpy.callCount).to.equal(expectedNativeCallCount);
enableNative();
};
const verify_signature = async function (oid, hash, r, s, message, pub) {
if (util.isString(message)) {
message = util.stringToUint8Array(message);
@ -162,99 +177,81 @@ export default () => describe('Elliptic Curve Cryptography @lightweight', functi
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]);
it('Invalid curve oid', function () {
return Promise.all([
expect(verify_signature(
'invalid oid', 8, [], [], [], []
)).to.be.rejectedWith(Error, /Unknown curve/),
expect(verify_signature(
'\x00', 8, [], [], [], []
)).to.be.rejectedWith(Error, /Unknown curve/)
]);
it('Invalid curve oid', async function () {
await expect(verify_signature(
'invalid oid', 8, [], [], [], []
)).to.be.rejectedWith(Error, /Unknown curve/);
await expect(verify_signature(
'\x00', 8, [], [], [], []
)).to.be.rejectedWith(Error, /Unknown curve/);
});
it('Invalid public key', async function () {
it('secp256k1 - Invalid public key', async function () {
if (!config.useIndutnyElliptic && !util.getNodeCrypto()) {
this.skip(); // webcrypto does not implement secp256k1
}
if (util.getNodeCrypto()) {
await expect(verify_signature(
'secp256k1', 8, [], [], [], []
)).to.eventually.be.false;
await expect(verify_signature(
'secp256k1', 8, [], [], [], secp256k1_invalid_point_format
)).to.eventually.be.false;
}
if (config.useIndutnyElliptic) {
disableNative();
await expect(verify_signature(
'secp256k1', 8, [], [], [], []
)).to.be.rejectedWith(Error, /Unknown point format/);
await expect(verify_signature(
'secp256k1', 8, [], [], [], secp256k1_invalid_point_format
)).to.be.rejectedWith(Error, /Unknown point format/);
this.skip(); // webcrypto does not implement secp256k1: JS fallback tested instead
}
await expect(verify_signature(
'secp256k1', 8, [], [], [], []
)).to.eventually.be.false;
await expect(verify_signature(
'secp256k1', 8, [], [], [], secp256k1_invalid_point_format
)).to.eventually.be.false;
});
it('Invalid point', async function () {
it('secp256k1 - Invalid point', async function () {
if (!config.useIndutnyElliptic && !util.getNodeCrypto()) {
this.skip();
}
if (util.getNodeCrypto()) {
await expect(verify_signature(
'secp256k1', 8, [], [], [], secp256k1_invalid_point
)).to.eventually.be.false;
}
if (config.useIndutnyElliptic) {
disableNative();
await expect(verify_signature(
'secp256k1', 8, [], [], [], secp256k1_invalid_point
)).to.be.rejectedWith(Error, /Invalid elliptic public key/);
this.skip(); // webcrypto does not implement secp256k1: JS fallback tested instead
}
await expect(verify_signature(
'secp256k1', 8, [], [], [], secp256k1_invalid_point
)).to.eventually.be.false;
});
it('Invalid signature', function (done) {
it('secp256k1 - Invalid signature', function (done) {
if (!config.useIndutnyElliptic && !util.getNodeCrypto()) {
this.skip();
this.skip(); // webcrypto does not implement secp256k1: JS fallback tested instead
}
expect(verify_signature(
'secp256k1', 8, [], [], [], secp256k1_point
)).to.eventually.be.false.notify(done);
});
const p384_message = new Uint8Array([
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF
]);
const p384_r = new Uint8Array([
0x9D, 0x07, 0xCA, 0xA5, 0x9F, 0xBE, 0xB8, 0x76,
0xA9, 0xB9, 0x66, 0x0F, 0xA0, 0x64, 0x70, 0x5D,
0xE6, 0x37, 0x40, 0x43, 0xD0, 0x8E, 0x40, 0xA8,
0x8B, 0x37, 0x83, 0xE7, 0xBC, 0x1C, 0x4C, 0x86,
0xCB, 0x3C, 0xD5, 0x9B, 0x68, 0xF0, 0x65, 0xEB,
0x3A, 0xB6, 0xD6, 0xA6, 0xCF, 0x85, 0x3D, 0xA9
]);
const p384_s = new Uint8Array([
0x32, 0x85, 0x78, 0xCC, 0xEA, 0xC5, 0x22, 0x83,
0x10, 0x73, 0x1C, 0xCF, 0x10, 0x8A, 0x52, 0x11,
0x8E, 0x49, 0x9E, 0xCF, 0x7E, 0x17, 0x18, 0xC3,
0x11, 0x11, 0xBC, 0x0F, 0x6D, 0x98, 0xE2, 0x16,
0x68, 0x58, 0x23, 0x1D, 0x11, 0xEF, 0x3D, 0x21,
0x30, 0x75, 0x24, 0x39, 0x48, 0x89, 0x03, 0xDC
]);
it('Valid signature', function (done) {
expect(verify_signature('p384', 8, p384_r, p384_s, p384_message, key_data.p384.pub))
.to.eventually.be.true.notify(done);
it('P-384 - Valid signature', async function () {
const p384_r = new Uint8Array([
0x9D, 0x07, 0xCA, 0xA5, 0x9F, 0xBE, 0xB8, 0x76,
0xA9, 0xB9, 0x66, 0x0F, 0xA0, 0x64, 0x70, 0x5D,
0xE6, 0x37, 0x40, 0x43, 0xD0, 0x8E, 0x40, 0xA8,
0x8B, 0x37, 0x83, 0xE7, 0xBC, 0x1C, 0x4C, 0x86,
0xCB, 0x3C, 0xD5, 0x9B, 0x68, 0xF0, 0x65, 0xEB,
0x3A, 0xB6, 0xD6, 0xA6, 0xCF, 0x85, 0x3D, 0xA9
]);
const p384_s = new Uint8Array([
0x32, 0x85, 0x78, 0xCC, 0xEA, 0xC5, 0x22, 0x83,
0x10, 0x73, 0x1C, 0xCF, 0x10, 0x8A, 0x52, 0x11,
0x8E, 0x49, 0x9E, 0xCF, 0x7E, 0x17, 0x18, 0xC3,
0x11, 0x11, 0xBC, 0x0F, 0x6D, 0x98, 0xE2, 0x16,
0x68, 0x58, 0x23, 0x1D, 0x11, 0xEF, 0x3D, 0x21,
0x30, 0x75, 0x24, 0x39, 0x48, 0x89, 0x03, 0xDC
]);
const p384_message = new Uint8Array([
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF
]);
await testNativeAndFallback(
() => expect(verify_signature('p384', 8, p384_r, p384_s, p384_message, key_data.p384.pub)).to.eventually.be.true
);
});
it('Sign and verify message', function () {
const curve = new elliptic_curves.CurveWithOID('p521');
return curve.genKeyPair().then(async keyPair => {
const keyPublic = new Uint8Array(keyPair.publicKey);
const keyPrivate = new Uint8Array(keyPair.privateKey);
const oid = curve.oid;
const message = p384_message;
return elliptic_curves.ecdsa.sign(oid, 10, message, keyPublic, keyPrivate, await hashMod.digest(10, message)).then(async signature => {
await expect(elliptic_curves.ecdsa.verify(oid, 10, signature, message, keyPublic, await hashMod.digest(10, message)))
.to.eventually.be.true;
});
const curves = ['secp256k1' , 'p256', 'p384', 'p521', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1'];
curves.forEach(curveName => it(`${curveName} - Sign and verify message`, async function () {
const curve = new elliptic_curves.CurveWithOID(curveName);
const { publicKey: keyPublic, privateKey: keyPrivate } = await curve.genKeyPair();
const message = new Uint8Array([
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF
]);
const messageDigest = await hashMod.digest(openpgp.enums.hash.sha512, message);
await testNativeAndFallback(async () => {
const signature = await elliptic_curves.ecdsa.sign(curve.oid, openpgp.enums.hash.sha512, message, keyPublic, keyPrivate, messageDigest);
await expect(elliptic_curves.ecdsa.verify(curve.oid, openpgp.enums.hash.sha512, signature, message, keyPublic, messageDigest)).to.eventually.be.true;
});
});
}));
});
});

View File

@ -1,3 +1,5 @@
import util from '../../src/util.js';
const elliptic_data = {
key_data: {
p256: {
@ -95,6 +97,18 @@ const elliptic_data = {
0xB8, 0xFD, 0x0B, 0xDF, 0x76, 0xCE, 0xBC, 0x95,
0x4B, 0x92, 0x26, 0xFC, 0xAA, 0x7A, 0x7C, 0x3F
])
},
brainpoolP256r1: {
priv: util.hexToUint8Array('8b426897130e1e5e70a4d6320c4002bb1642a5e57ade066e060464137dfd5e05'),
pub: util.hexToUint8Array('042a43d8cc20e5a3fbd75d3a5a9b17d867bba80f11334d0665f0c641d13460a52aa3373a4ccfaa7d76765a689bd9fe15a4fd107ef1ec9ac980234c31647170c81a')
},
brainpoolP384r1: {
priv: util.hexToUint8Array('7ccc97acdf4b775606c5c994a37a8b28086167046ac0d55664ede4097d8de79dec56e69dfff5776d53fcbd2147bbae9f'),
pub: util.hexToUint8Array('043809fa0c74ec9817cb73eba67db71e01663528fb9fbe6a123f8339346c37efc9ff7cd116074a80684448e44ee9204c795c88ad634ad272585c0b4e3093b11e6c99a6c0ca9c278f83ef57e2ed802502aee76f4529bcb873eef754bec894a5032f')
},
brainpoolP512r1: {
priv: util.hexToUint8Array('0a32459d1ecf8815397a66f6cdb18692c6f79a3c6059b4c344d0162416c7603a82a9a938568edafb132c7433ffeeab4cf201d9542209eb28070bea56ab6b8938'),
pub: util.hexToUint8Array('040f64473d9b3597752e3a87095c0b219dd85f56a79c3b2dc8fb2b0c95b60f4be45c41a8a7ea31d60e15fea6275eb7db93856bc2eb30cc8876513335d43812bd2c4e195e05679ac667a2f7fb05c5842779d18fa411500e43e2f291ea8348f061db15382d4db1cfcf106a29f46e1c00e7d63e635c51293f69c0dd4f6a61da589b2a')
}
}
};

View File

@ -256,10 +256,6 @@ EJ4QcD/oQ6x1M/8X/iKQCtxZP8RnlrbH7ExkNON5s5g=
expect(await result.signatures[0].verified).to.be.true;
});
it('Decrypt and verify message with leading zero in hash signed with old elliptic algorithm', async function () {
//this test would not work with nodeCrypto, since message is signed with leading zero stripped from the hash
if (util.getNodeCrypto()) {
this.skip(); // eslint-disable-line no-invalid-this
}
const juliet = await load_priv_key('juliet');
const romeo = await load_pub_key('romeo');
const msg = await openpgp.readMessage({ armoredMessage: data.romeo.message_encrypted_with_leading_zero_in_hash_signed_by_elliptic_with_old_implementation });