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.
This commit is contained in:
larabr 2024-06-26 14:04:10 +02:00 committed by larabr
parent 9fe278241a
commit 0ebf5797a5
5 changed files with 44 additions and 7 deletions

13
package-lock.json generated
View File

@ -11,6 +11,7 @@
"devDependencies": { "devDependencies": {
"@noble/ciphers": "^1.0.0", "@noble/ciphers": "^1.0.0",
"@noble/curves": "^1.6.0", "@noble/curves": "^1.6.0",
"@noble/ed25519": "^1.7.3",
"@noble/hashes": "^1.5.0", "@noble/hashes": "^1.5.0",
"@openpgp/jsdoc": "^3.6.11", "@openpgp/jsdoc": "^3.6.11",
"@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/seek-bzip": "^1.0.5-git",
@ -956,6 +957,18 @@
"url": "https://paulmillr.com/funding/" "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": { "node_modules/@noble/hashes": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz",

View File

@ -64,6 +64,7 @@
"devDependencies": { "devDependencies": {
"@noble/ciphers": "^1.0.0", "@noble/ciphers": "^1.0.0",
"@noble/curves": "^1.6.0", "@noble/curves": "^1.6.0",
"@noble/ed25519": "^1.7.3",
"@noble/hashes": "^1.5.0", "@noble/hashes": "^1.5.0",
"@openpgp/jsdoc": "^3.6.11", "@openpgp/jsdoc": "^3.6.11",
"@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/seek-bzip": "^1.0.5-git",

View File

@ -100,6 +100,13 @@ const fullBrowserBuild = {
commonjs({ commonjs({
ignore: nodeBuiltinModules.concat(nodeDependencies) 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({ replace({
'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`, 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`,
"import { createRequire } from 'module';": 'const createRequire = () => () => {}', "import { createRequire } from 'module';": 'const createRequire = () => () => {}',
@ -127,6 +134,13 @@ const lightweightBrowserBuild = {
commonjs({ commonjs({
ignore: nodeBuiltinModules.concat(nodeDependencies) 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({ replace({
'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`, 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`,
"import { createRequire } from 'module';": 'const createRequire = () => () => {}', "import { createRequire } from 'module';": 'const createRequire = () => () => {}',
@ -158,6 +172,13 @@ const testBuild = {
ignore: nodeBuiltinModules.concat(nodeDependencies), ignore: nodeBuiltinModules.concat(nodeDependencies),
requireReturnsDefault: 'preferred' 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({ replace({
"import { createRequire } from 'module';": 'const createRequire = () => () => {}', "import { createRequire } from 'module';": 'const createRequire = () => () => {}',
delimiters: ['', ''] delimiters: ['', '']

View File

@ -20,7 +20,9 @@
* @module crypto/public_key/elliptic/eddsa * @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 util from '../../../util';
import enums from '../../../enums'; import enums from '../../../enums';
import { getHashByteLength } from '../../hash'; import { getHashByteLength } from '../../hash';
@ -52,7 +54,7 @@ export async function generate(algo) {
throw err; throw err;
} }
const seed = getRandomBytes(getPayloadSize(algo)); const seed = getRandomBytes(getPayloadSize(algo));
const { publicKey: A } = ed25519.sign.keyPair.fromSeed(seed); const { publicKey: A } = naclEd25519.sign.keyPair.fromSeed(seed);
return { A, seed }; return { A, seed };
} }
@ -104,7 +106,7 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe
throw err; throw err;
} }
const secretKey = util.concatUint8Array([privateKey, publicKey]); const secretKey = util.concatUint8Array([privateKey, publicKey]);
const signature = ed25519.sign.detached(hashed, secretKey); const signature = naclEd25519.sign.detached(hashed, secretKey);
return { RS: signature }; return { RS: signature };
} }
@ -149,7 +151,7 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) {
if (err.name !== 'NotSupportedError') { if (err.name !== 'NotSupportedError') {
throw err; throw err;
} }
return ed25519.sign.detached.verify(hashed, RS, publicKey); return nobleEd25519Verify(RS, hashed, publicKey);
} }
case enums.publicKey.ed448: { case enums.publicKey.ed448: {
@ -177,7 +179,7 @@ export async function validateParams(algo, A, seed) {
* and expect A == A' * and expect A == A'
* TODO: move to sign-verify using WebCrypto (same as ECDSA) when curve is more widely implemented * 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); return util.equalsUint8Array(A, publicKey);
} }

View File

@ -21,7 +21,7 @@
* @module crypto/public_key/elliptic/eddsa_legacy * @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 util from '../../../util';
import enums from '../../../enums'; import enums from '../../../enums';
import { getHashByteLength } from '../../hash'; import { getHashByteLength } from '../../hash';
@ -101,7 +101,7 @@ export async function validateParams(oid, Q, k) {
* Derive public point Q' = dG from private key * Derive public point Q' = dG from private key
* and expect Q == Q' * 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 const dG = new Uint8Array([0x40, ...publicKey]); // Add public key prefix
return util.equalsUint8Array(Q, dG); return util.equalsUint8Array(Q, dG);