From f443c6183e11f5f6879c41d33820a80a07a15ce0 Mon Sep 17 00:00:00 2001 From: larabr Date: Wed, 26 Jun 2024 14:04:10 +0200 Subject: [PATCH] Use noble-ed25519 over tweetnacl for signature verification (#16) Much faster than tweetnacl, and no constant-timeness required. We are not using v2 for now, despite being smaller, because it relies on bigint literals, and it requires polyfilling the WebCrypto lib manually in Node < 19. --- package-lock.json | 13 ++++++++++++ package.json | 1 + rollup.config.js | 21 +++++++++++++++++++ src/crypto/public_key/elliptic/eddsa.js | 12 ++++++----- .../public_key/elliptic/eddsa_legacy.js | 4 ++-- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index d40157c80..62bb16323 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@noble/ciphers": "^0.6.0", "@noble/curves": "^1.6.0", + "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", @@ -856,6 +857,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/ed25519": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", + "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@noble/hashes": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", diff --git a/package.json b/package.json index 7330dbb4f..f3a7411ff 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "devDependencies": { "@noble/ciphers": "^0.6.0", "@noble/curves": "^1.6.0", + "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", diff --git a/rollup.config.js b/rollup.config.js index e08b16297..1f0e895a6 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -70,6 +70,13 @@ export default Object.assign([ commonjs({ ignore: nodeBuiltinModules.concat(nodeDependencies) }), + replace({ + include: 'node_modules/@noble/ed25519/**', + // Rollup ignores the `browser: { crypto: false }` directive in package.json, since `exports` are present, + // hence we need to manually drop it. + "import * as nodeCrypto from 'crypto'": 'const nodeCrypto = null', + delimiters: ['', ''] + }), replace({ 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`, "import { createRequire } from 'module';": 'const createRequire = () => () => {}', @@ -119,6 +126,13 @@ export default Object.assign([ commonjs({ ignore: nodeBuiltinModules.concat(nodeDependencies) }), + replace({ + include: 'node_modules/@noble/ed25519/**', + // Rollup ignores the `browser: { crypto: false }` directive in package.json, since `exports` are present, + // hence we need to manually drop it. + "import * as nodeCrypto from 'crypto'": 'const nodeCrypto = null', + delimiters: ['', ''] + }), replace({ 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`, "import { createRequire } from 'module';": 'const createRequire = () => () => {}', @@ -149,6 +163,13 @@ export default Object.assign([ ignore: nodeBuiltinModules.concat(nodeDependencies), requireReturnsDefault: 'preferred' }), + replace({ + include: 'node_modules/@noble/ed25519/**', + // Rollup ignores the `browser: { crypto: false }` directive in package.json, since `exports` are present, + // hence we need to manually drop it. + "import * as nodeCrypto from 'crypto'": 'const nodeCrypto = null', + delimiters: ['', ''] + }), replace({ "import { createRequire } from 'module';": 'const createRequire = () => () => {}', delimiters: ['', ''] diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index d22cd8bd2..cc992257a 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -20,7 +20,9 @@ * @module crypto/public_key/elliptic/eddsa */ -import ed25519 from '@openpgp/tweetnacl'; +import naclEd25519 from '@openpgp/tweetnacl'; // better constant-timeness as it uses Uint8Arrays over BigInts +import { verify as nobleEd25519Verify } from '@noble/ed25519'; + import util from '../../../util'; import enums from '../../../enums'; import hash from '../../hash'; @@ -52,7 +54,7 @@ export async function generate(algo) { throw err; } const seed = getRandomBytes(getPayloadSize(algo)); - const { publicKey: A } = ed25519.sign.keyPair.fromSeed(seed); + const { publicKey: A } = naclEd25519.sign.keyPair.fromSeed(seed); return { A, seed }; } @@ -101,7 +103,7 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe throw err; } const secretKey = util.concatUint8Array([privateKey, publicKey]); - const signature = ed25519.sign.detached(hashed, secretKey); + const signature = naclEd25519.sign.detached(hashed, secretKey); return { RS: signature }; } @@ -143,7 +145,7 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) { if (err.name !== 'NotSupportedError') { throw err; } - return ed25519.sign.detached.verify(hashed, RS, publicKey); + return nobleEd25519Verify(RS, hashed, publicKey); } case enums.publicKey.ed448: { @@ -171,7 +173,7 @@ export async function validateParams(algo, A, seed) { * and expect A == A' * TODO: move to sign-verify using WebCrypto (same as ECDSA) when curve is more widely implemented */ - const { publicKey } = ed25519.sign.keyPair.fromSeed(seed); + const { publicKey } = naclEd25519.sign.keyPair.fromSeed(seed); return util.equalsUint8Array(A, publicKey); } diff --git a/src/crypto/public_key/elliptic/eddsa_legacy.js b/src/crypto/public_key/elliptic/eddsa_legacy.js index 28c72d4ec..9f942bb87 100644 --- a/src/crypto/public_key/elliptic/eddsa_legacy.js +++ b/src/crypto/public_key/elliptic/eddsa_legacy.js @@ -21,7 +21,7 @@ * @module crypto/public_key/elliptic/eddsa_legacy */ -import nacl from '@openpgp/tweetnacl'; +import naclEd25519 from '@openpgp/tweetnacl'; // better constant-timeness as it uses Uint8Arrays over BigInts import util from '../../../util'; import enums from '../../../enums'; import hash from '../../hash'; @@ -96,7 +96,7 @@ export async function validateParams(oid, Q, k) { * Derive public point Q' = dG from private key * and expect Q == Q' */ - const { publicKey } = nacl.sign.keyPair.fromSeed(k); + const { publicKey } = naclEd25519.sign.keyPair.fromSeed(k); const dG = new Uint8Array([0x40, ...publicKey]); // Add public key prefix return util.equalsUint8Array(Q, dG);