diff --git a/package-lock.json b/package-lock.json index a1264117..0c0cc051 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c8b4f351..90cd6346 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 37001916..ed370578 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -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 * diff --git a/src/crypto/public_key/elliptic/ecdsa.js b/src/crypto/public_key/elliptic/ecdsa.js index 19412aa1..d000bf01 100644 --- a/src/crypto/public_key/elliptic/ecdsa.js +++ b/src/crypto/public_key/elliptic/ecdsa.js @@ -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); diff --git a/src/crypto/public_key/elliptic/indutnyKey.js b/src/crypto/public_key/elliptic/indutnyKey.js deleted file mode 100644 index ddfc6aa7..00000000 --- a/src/crypto/public_key/elliptic/indutnyKey.js +++ /dev/null @@ -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); -} diff --git a/src/crypto/public_key/elliptic/oid_curves.js b/src/crypto/public_key/elliptic/oid_curves.js index 4d40e25b..d6561d5a 100644 --- a/src/crypto/public_key/elliptic/oid_curves.js +++ b/src/crypto/public_key/elliptic/oid_curves.js @@ -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 diff --git a/test/crypto/ecdh.js b/test/crypto/ecdh.js index 85eba9fa..c3b4536f 100644 --- a/test/crypto/ecdh.js +++ b/test/crypto/ecdh.js @@ -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; } }); diff --git a/test/crypto/elliptic.js b/test/crypto/elliptic.js index 048413fd..a434c543 100644 --- a/test/crypto/elliptic.js +++ b/test/crypto/elliptic.js @@ -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; }); - }); + })); }); }); diff --git a/test/crypto/elliptic_data.js b/test/crypto/elliptic_data.js index 61eba7d9..1fbc64d0 100644 --- a/test/crypto/elliptic_data.js +++ b/test/crypto/elliptic_data.js @@ -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') } } }; diff --git a/test/general/brainpool.js b/test/general/brainpool.js index e22dae58..903e4a66 100644 --- a/test/general/brainpool.js +++ b/test/general/brainpool.js @@ -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 });