147 Commits
0.5.2 ... 0.9.1

Author SHA1 Message Date
Paul Miller
19f04a4c1c Release 0.9.1. 2023-03-31 10:02:05 +02:00
Paul Miller
d0c3bee4de weierstrass, edwards: make points expose typescript x, y 2023-03-30 09:20:35 +02:00
Paul Miller
4244f97d38 bls: get rid of bigint literals. gh-22 2023-03-28 19:01:42 +02:00
Paul Miller
618508d32c weierstrass, edwards: get rid of bigint literals. Closes gh-22 2023-03-28 19:01:00 +02:00
Paul Miller
3936449e7b edwards: add toRawBytes to ts type 2023-03-26 15:54:04 +02:00
Paul Miller
0ffa38db6b Release 0.9.0. 2023-03-24 11:12:02 +01:00
Paul Miller
c4c580edc0 Bump devdeps 2023-03-24 11:06:48 +01:00
Paul Miller
abe8adac7b README 2023-03-24 10:25:03 +01:00
Paul Miller
4fd2ae82b6 readme 2023-03-21 07:27:45 +01:00
Paul Miller
e2411f7dfd modular: add comment 2023-03-21 07:25:09 +01:00
Paul Miller
cb61e4f292 readme 2023-03-21 07:25:01 +01:00
Paul Miller
bb875791bd docs 2023-03-21 07:11:17 +01:00
Paul Miller
3df2553ced Docs 2023-03-21 07:02:07 +01:00
Paul Miller
8fabc7ff06 All files: rename Fp to Field 2023-03-21 06:51:18 +01:00
Paul Miller
f3c21eb347 weierstrass: make weierstrassPoints fromBytes / toBytes optional 2023-03-21 05:51:10 +01:00
Paul Miller
a8b8192714 Add CURVE.p param 2023-03-21 03:06:06 +01:00
Paul Miller
1c6aa07ff7 Release 0.8.3. 2023-03-16 19:41:20 +01:00
Paul Miller
e110237298 readme 2023-03-16 19:17:34 +01:00
Paul Miller
45393db807 Bump docs 2023-03-16 19:05:33 +01:00
Paul Miller
acc3a9dc4d Bump devdep types/node 2023-03-16 18:52:03 +01:00
Paul Miller
9295b0dbae Upgrade to Typescript 5 2023-03-16 18:49:48 +01:00
Paul Miller
5784ef23f6 Release 0.8.2. 2023-03-14 00:44:02 +01:00
Paul Miller
ef55efe842 Fix common.js build 2023-03-14 00:42:40 +01:00
Paul Miller
1cfd6a76ca Release 0.8.1. 2023-03-14 00:40:05 +01:00
Paul Miller
89f81b2204 pkg.json: improve bench, clean scripts 2023-03-14 00:39:21 +01:00
Paul Miller
d77ac16f51 Bring back common.js for now. Need more thorough work with consumers 2023-03-14 00:32:09 +01:00
Paul Miller
fe68da61f6 Move stark curve to micro-starknet 2023-03-10 20:18:05 +01:00
Paul Miller
32c0841bed Add Trail of Bits audit 2023-03-10 01:09:49 +01:00
Paul Miller
49a659b248 Release 0.8.0. 2023-03-03 05:12:36 +04:00
Paul Miller
9d0a2e25dc readme: esm-only 2023-03-03 05:11:21 +04:00
Paul Miller
7c461af2b2 test: remove common.js support 2023-03-03 05:09:50 +04:00
Paul Miller
4a8f447c8d package.json, tsconfig: remove common.js support. Pure ESM now 2023-03-03 05:09:36 +04:00
Paul Miller
4b2d31ce7f stark: more methods 2023-02-28 23:18:06 +04:00
Paul Miller
16115f27a6 readme update 2023-02-28 14:04:15 +04:00
Paul Miller
0e0d0f530d benchmark: add tonneli-shanks sqrt 2023-02-28 02:59:28 +04:00
Paul Miller
fa5105aef2 ecdsa: remove scalar blinding. CSPRNG dep not good: cryptofuzz, other envs will fail 2023-02-28 01:48:06 +04:00
Paul Miller
11f1626ecc modular: Add comment. Add benchmark 2023-02-27 22:41:24 +04:00
Paul Miller
53ff287bf7 Schnorr: remove getExtendedPublicKey 2023-02-27 20:29:47 +04:00
Paul Miller
214c9aa553 secp256k1: Fix schnorrGetExtPubKey y coordinate 2023-02-27 20:20:13 +04:00
Paul Miller
ec2c3e1248 Add test for ristretto equality testing 2023-02-27 19:33:41 +04:00
Paul Miller
e64a9d654c Fix ristretto255 equals 2023-02-27 19:07:45 +04:00
Paul Miller
088edd0fbb h2c: move params validation. add experimental hash_to_ristretto255 2023-02-27 15:07:24 +01:00
Paul Miller
3e90930e9d Fix types 2023-02-26 19:10:50 +01:00
Paul Miller
b8b2e91f74 Release 0.7.3. 2023-02-26 19:05:53 +01:00
Paul Miller
9ee694ae23 docs updates 2023-02-26 19:05:40 +01:00
Paul Miller
6bc4b35cf4 hash-to-curve: speed-up os2ip, change code a bit 2023-02-26 18:55:30 +01:00
Paul Miller
0163b63532 Release 0.7.2. 2023-02-25 10:13:45 +01:00
Paul Miller
7e825520f1 README 2023-02-25 10:05:48 +01:00
Paul Miller
d739297b2c Move p192, p224 from main pkg to tests for now. Reason: not popular 2023-02-25 10:00:24 +01:00
Paul Miller
285aa6375d stark: refactor 2023-02-20 16:50:29 +01:00
Paul Miller
8c77331ef2 add hash-to-curve benchmark 2023-02-20 16:33:05 +01:00
Paul Miller
669641e0a3 README wording 2023-02-16 17:54:17 +01:00
Paul Miller
68dd57ed31 Cryptofuzz 2023-02-16 17:49:48 +01:00
Paul Miller
a9fdd6df9f readme: typo 2023-02-16 12:33:32 +01:00
Paul Miller
d485d8b0e6 Fix prettier 2023-02-16 12:32:32 +01:00
Paul Miller
0fdd763dc7 montgomery: add randomPrivateKey. Add ecdh benchmark. 2023-02-16 12:32:18 +01:00
Paul Miller
586e2ad5fb Release 0.7.1. 2023-02-16 00:20:37 +01:00
Paul Miller
ed81707bdc readme 2023-02-16 00:12:23 +01:00
Paul Miller
6d56b2d78e readme 2023-02-16 00:08:18 +01:00
Paul Miller
8397241a8f bls, stark: adjust methods 2023-02-16 00:03:20 +01:00
Paul Miller
001d0cc24a weierstrass: rename method, adjust comments 2023-02-16 00:03:10 +01:00
Paul Miller
ce9d165657 readme hash-to-scalar 2023-02-15 23:46:43 +01:00
Paul Miller
2902b0299a readme 2023-02-15 23:38:26 +01:00
Paul Miller
e1cb8549e8 weierstrass, montgomery, secp: add comments 2023-02-15 23:26:56 +01:00
Paul Miller
26ebb5dcce x25519, x448: change param from a24 to a. Change Gu to bigint 2023-02-15 23:07:52 +01:00
Paul Miller
8b2863aeac Fix benchmark 2023-02-15 22:50:32 +01:00
Paul Miller
b1f50d9364 hash-to-curve: bls examples 2023-02-15 00:08:38 +01:00
Paul Miller
b81d74d3cb readme 2023-02-15 00:06:39 +01:00
Paul Miller
d5fe537159 hash-to-curve readme 2023-02-15 00:03:18 +01:00
Paul Miller
cde1d5c488 Fix tests 2023-02-14 23:51:11 +01:00
Paul Miller
3486bbf6b8 Release 0.7.0. 2023-02-14 23:45:53 +01:00
Paul Miller
0d7a8296c5 gitignore update 2023-02-14 23:45:39 +01:00
Paul Miller
0f1e7a5a43 Move output from lib to root. React Native does not support pkg.json#exports 2023-02-14 23:43:28 +01:00
Paul Miller
3da48cf899 bump bmark 2023-02-14 23:24:11 +01:00
Paul Miller
4ec46dd65d Remove scure-base from top-level dep 2023-02-14 18:00:11 +01:00
Paul Miller
7073f63c6b drbg: move from weierstrass to utils 2023-02-14 17:54:57 +01:00
Paul Miller
80966cbd03 hash-to-curve: more type checks. Rename method to createHasher 2023-02-14 17:39:56 +01:00
Paul Miller
98ea15dca4 edwards: improve hex errors 2023-02-14 17:35:19 +01:00
Paul Miller
e1910e85ea mod, utils, weierstrass, secp: improve hex errors. secp: improve verify() logic and schnorr 2023-02-14 17:34:31 +01:00
Paul Miller
4d311d7294 Emit source maps 2023-02-14 17:23:51 +01:00
Paul Miller
c36d90cae6 bump lockfile, add comment to shortw 2023-02-13 23:55:58 +01:00
Paul Miller
af5aa8424f readme: supply chain attacks 2023-02-13 23:32:49 +01:00
Paul Miller
67b99652fc BLS: add docs 2023-02-12 22:25:36 +01:00
Paul Miller
c8d292976b README 2023-02-12 22:25:22 +01:00
Paul Miller
daffaa2339 README: more docs 2023-02-12 21:37:27 +01:00
Paul Miller
a462fc5779 readme updates 2023-02-12 11:30:55 +01:00
Paul Miller
fe3491c5aa Release 0.6.4. 2023-02-09 23:19:15 +01:00
Paul Miller
c0877ba69a Fix weierstrass type 2023-02-09 23:18:32 +01:00
Paul Miller
8e449cc78c ed25519 tests: unify with noble-ed25519 2023-02-09 21:26:24 +01:00
Paul Miller
1b6071cabd weierstrass: rename normPrivKey util. tests: prepare for unification w old noble pkg 2023-02-09 20:26:20 +01:00
Paul Miller
debb9d9709 Release 0.6.3. 2023-02-09 16:19:08 +01:00
Paul Miller
d2c6459756 Update wnaf comments 2023-02-09 15:45:21 +01:00
Paul Miller
47533b6336 Add more tests for weierstrass, etc 2023-02-09 13:29:19 +01:00
Paul Miller
00b73b68d3 hash-to-curve small refactor 2023-02-06 20:50:52 +01:00
Paul Miller
cef4b52d12 Update hashes to 1.2, scure devdeps, add lockfile 2023-02-06 20:50:41 +01:00
Paul Miller
47ce547dcf README update 2023-02-06 20:50:23 +01:00
Paul Miller
e2a7594eae Release 0.6.2. 2023-01-30 08:18:07 +01:00
Paul Miller
823149ecd9 Clarify comment 2023-01-30 08:17:08 +01:00
Paul Miller
e57aec63d8 Fix edwards assertValidity 2023-01-30 08:04:36 +01:00
Paul Miller
837aca98c9 Fix bugs 2023-01-30 06:10:56 +01:00
Paul Miller
dbb16b0e5e edwards: add assertValidity 2023-01-30 06:10:08 +01:00
Paul Miller
e14af67254 utils: fix hexToNumber, improve validateObject 2023-01-30 06:07:53 +01:00
Paul Miller
4780850748 montgomery: fix fieldLen 2023-01-30 05:56:07 +01:00
Paul Miller
3374a70f47 README update 2023-01-30 05:55:36 +01:00
Paul Miller
131f88b504 Release 0.6.1. 2023-01-29 05:14:10 +01:00
Paul Miller
4333e9a686 README 2023-01-29 05:12:58 +01:00
Paul Miller
a60d15ff05 Upgrading guide from other noble libraries 2023-01-29 05:10:58 +01:00
Paul Miller
ceffbc69da More Schnorr utils 2023-01-29 04:46:38 +01:00
Paul Miller
c75129e629 Use declarative curve field validation 2023-01-28 03:19:46 +01:00
Paul Miller
f39fb80c52 weierstrass: rename normalizePrivateKey to allowedPrivateKeyLengths 2023-01-27 23:45:55 +01:00
Paul Miller
fcd422d246 README updates 2023-01-27 03:48:53 +01:00
Paul Miller
ed9bf89038 stark: isCompressed=false. Update benchmarks 2023-01-27 03:43:18 +01:00
Paul Miller
7262b4219f Bump micro-should 2023-01-26 08:26:07 +01:00
Paul Miller
02b0b25147 New schnorr exports. Simplify RFC6979 k gen, privkey checks 2023-01-26 08:16:00 +01:00
Paul Miller
79100c2d47 Release 0.6.0. 2023-01-26 06:31:16 +01:00
Paul Miller
4ef2cad685 hash-to-curve: assertValidity 2023-01-26 06:14:12 +01:00
Paul Miller
69b3ab5a57 Shuffle code 2023-01-26 05:46:14 +01:00
Paul Miller
9465e60d30 More refactoring 2023-01-26 05:24:41 +01:00
Paul Miller
0fb78b7097 Rename group to curve. More refactoring 2023-01-26 04:14:21 +01:00
Paul Miller
be0b2a32a5 Fp rename. Edwards refactor. Weierstrass Fn instead of mod 2023-01-26 03:07:45 +01:00
Paul Miller
3d77422731 Restructure tests 2023-01-26 03:06:28 +01:00
Paul Miller
c46914f1bc weierstrass: remove most private utils 2023-01-25 08:21:48 +01:00
Paul Miller
f250f355e8 Schnorr: remove all private methods 2023-01-25 08:14:53 +01:00
Paul Miller
c095d74673 More schnorr updates 2023-01-25 08:10:05 +01:00
Paul Miller
ac52fea952 Another schnorr adjustment 2023-01-25 07:55:21 +01:00
Paul Miller
f2ee24bee4 schnorr: remove packSig 2023-01-25 07:54:00 +01:00
Paul Miller
cffea91061 Schnorr, weierstrass: refactor 2023-01-25 07:48:53 +01:00
Paul Miller
5fc38fc0e7 weierstrass: prehash option in sign/verify. Remove _normalizePublicKey 2023-01-25 05:45:49 +01:00
Paul Miller
849dc38f3c Change TypeError to Error 2023-01-25 05:24:22 +01:00
Paul Miller
0422e6ef38 p.x, p.y are now getters executing toAffine() 2023-01-25 04:51:08 +01:00
Paul Miller
21d2438a33 BLS: fix tests. Poseidon: more tests 2023-01-25 00:30:53 +01:00
Paul Miller
cea4696599 BLS tests: remove async 2023-01-25 00:13:39 +01:00
Paul Miller
f14b8d2be5 More AffinePoint fixes 2023-01-25 00:07:25 +01:00
Paul Miller
2ed27da8eb weierstrass: remove affine Point 2023-01-24 06:42:44 +01:00
Paul Miller
17e5be5f1b edwards: affine Point removal tests 2023-01-24 05:37:53 +01:00
Paul Miller
a49f0d266e edwards: remove affine Point, Signature. Stricter types 2023-01-24 05:34:56 +01:00
Paul Miller
bfbcf733e6 Update tests 2023-01-24 04:02:45 +01:00
Paul Miller
7fda6de619 weierstrass: make points compressed by def. Rewrite drbg, k generation. 2023-01-24 04:02:38 +01:00
Paul Miller
2b908ad602 edwards: simplify bounds check 2023-01-24 04:01:28 +01:00
Paul Miller
ceb3f67faa stark: switch to new weierstrass methods 2023-01-23 23:07:21 +01:00
Paul Miller
a2c87f9c2f weierstrass: simplify bits2int, remove truncateHash 2023-01-23 23:06:43 +01:00
Paul Miller
e1fd346279 utils: small improvements 2023-01-23 23:06:24 +01:00
Paul Miller
11e78aadbf Edwards: prohibit number scalars, only allow bigints 2023-01-23 20:28:01 +01:00
Paul Miller
055147f1be Add poseidon252 snark-friendly hash 2023-01-23 19:41:19 +01:00
Paul Miller
6f99f6042e weierstrass: bits2int, int2octets, truncateHash now comply with standard 2023-01-21 19:03:39 +01:00
Paul Miller
1e47bf2372 Bump prettier to 2.8.3 because it fails to parse bls 2023-01-21 19:02:58 +01:00
Paul Miller
40530eae0c hash-to-curve: decrease coupling, improve tree shaking support 2023-01-21 19:02:46 +01:00
69 changed files with 6823 additions and 7673 deletions

14
.gitignore vendored
View File

@@ -1,7 +1,13 @@
build/ build/
node_modules/ node_modules/
coverage/ coverage/
/lib/**/*.js /*.js
/lib/**/*.ts /*.ts
/lib/**/*.d.ts.map /*.js.map
/curve-definitions/lib /*.d.ts.map
/esm/*.js
/esm/*.ts
/esm/*.js.map
/esm/*.d.ts.map
/esm/abstract
/abstract/

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"files.exclude": {
"*.{js,d.ts,js.map,d.ts.map}": true,
"esm/*.{js,d.ts,js.map,d.ts.map}": true
}
}

919
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,8 @@
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| >=0.5.0 | :white_check_mark: | | >=1.0.0 | :white_check_mark: |
| <0.5.0 | :x: | | <1.0.0 | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability

Binary file not shown.

11
audit/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Audit
The library has been audited during Jan-Feb 2023 by an independent security firm [Trail of Bits](https://www.trailofbits.com):
[PDF](https://github.com/trailofbits/publications/blob/master/reviews/2023-01-ryanshea-noblecurveslibrary-securityreview.pdf).
The audit has been funded by Ryan Shea. Audit scope was abstract modules `curve`, `hash-to-curve`, `modular`, `poseidon`, `utils`, `weierstrass`, and top-level modules `_shortw_utils` and `secp256k1`. See [changes since audit](https://github.com/paulmillr/noble-curves/compare/0.7.3..main).
File in the directory was saved from
[github.com/trailofbits/publications](https://github.com/trailofbits/publications).
Check out their repo and verify checksums to ensure the PDF in this directory has not been altered.
See information about fuzzing in root [README](../README.md).

7
benchmark/_shared.js Normal file
View File

@@ -0,0 +1,7 @@
export function generateData(curve) {
const priv = curve.utils.randomPrivateKey();
const pub = curve.getPublicKey(priv);
const msg = curve.utils.randomPrivateKey();
const sig = curve.sign(msg, priv);
return { priv, pub, msg, sig };
}

52
benchmark/bls.js Normal file
View File

@@ -0,0 +1,52 @@
import { readFileSync } from 'fs';
import { mark, run } from 'micro-bmark';
import { bls12_381 as bls } from '../bls12-381.js';
const G2_VECTORS = readFileSync('../test/bls12-381/bls12-381-g2-test-vectors.txt', 'utf-8')
.trim()
.split('\n')
.map((l) => l.split(':'));
run(async () => {
console.log(`\x1b[36mbls12-381\x1b[0m`);
let p1, p2, sig;
await mark('init', 1, () => {
p1 =
bls.G1.ProjectivePoint.BASE.multiply(
0x28b90deaf189015d3a325908c5e0e4bf00f84f7e639b056ff82d7e70b6eede4cn
);
p2 =
bls.G2.ProjectivePoint.BASE.multiply(
0x28b90deaf189015d3a325908c5e0e4bf00f84f7e639b056ff82d7e70b6eede4dn
);
bls.pairing(p1, p2);
});
const priv = '28b90deaf189015d3a325908c5e0e4bf00f84f7e639b056ff82d7e70b6eede4c';
sig = bls.sign('09', priv);
const pubs = G2_VECTORS.map((v) => bls.getPublicKey(v[0]));
const sigs = G2_VECTORS.map((v) => v[2]);
const pub = bls.getPublicKey(priv);
const pub512 = pubs.slice(0, 512); // .map(bls.PointG1.fromHex)
const pub32 = pub512.slice(0, 32);
const pub128 = pub512.slice(0, 128);
const pub2048 = pub512.concat(pub512, pub512, pub512);
const sig512 = sigs.slice(0, 512); // .map(bls.PointG2.fromSignature);
const sig32 = sig512.slice(0, 32);
const sig128 = sig512.slice(0, 128);
const sig2048 = sig512.concat(sig512, sig512, sig512);
await mark('getPublicKey 1-bit', 1000, () => bls.getPublicKey('2'.padStart(64, '0')));
await mark('getPublicKey', 1000, () => bls.getPublicKey(priv));
await mark('sign', 50, () => bls.sign('09', priv));
await mark('verify', 50, () => bls.verify(sig, '09', pub));
await mark('pairing', 100, () => bls.pairing(p1, p2));
await mark('aggregatePublicKeys/8', 100, () => bls.aggregatePublicKeys(pubs.slice(0, 8)));
await mark('aggregatePublicKeys/32', 50, () => bls.aggregatePublicKeys(pub32));
await mark('aggregatePublicKeys/128', 20, () => bls.aggregatePublicKeys(pub128));
await mark('aggregatePublicKeys/512', 10, () => bls.aggregatePublicKeys(pub512));
await mark('aggregatePublicKeys/2048', 5, () => bls.aggregatePublicKeys(pub2048));
await mark('aggregateSignatures/8', 100, () => bls.aggregateSignatures(sigs.slice(0, 8)));
await mark('aggregateSignatures/32', 50, () => bls.aggregateSignatures(sig32));
await mark('aggregateSignatures/128', 20, () => bls.aggregateSignatures(sig128));
await mark('aggregateSignatures/512', 10, () => bls.aggregateSignatures(sig512));
await mark('aggregateSignatures/2048', 5, () => bls.aggregateSignatures(sig2048));
});

23
benchmark/curves.js Normal file
View File

@@ -0,0 +1,23 @@
import { run, mark, utils } from 'micro-bmark';
import { generateData } from './_shared.js';
import { P256 } from '../p256.js';
import { P384 } from '../p384.js';
import { P521 } from '../p521.js';
import { ed25519 } from '../ed25519.js';
import { ed448 } from '../ed448.js';
run(async () => {
const RAM = false
for (let kv of Object.entries({ P256, P384, P521, ed25519, ed448 })) {
const [name, curve] = kv;
console.log();
console.log(`\x1b[36m${name}\x1b[0m`);
if (RAM) utils.logMem();
await mark('init', 1, () => curve.utils.precompute(8));
const d = generateData(curve);
await mark('getPublicKey', 5000, () => curve.getPublicKey(d.priv));
await mark('sign', 5000, () => curve.sign(d.msg, d.priv));
await mark('verify', 500, () => curve.verify(d.sig, d.msg, d.pub));
if (RAM) utils.logMem();
}
});

19
benchmark/ecdh.js Normal file
View File

@@ -0,0 +1,19 @@
import { run, mark, compare, utils } from 'micro-bmark';
import { generateData } from './_shared.js';
import { secp256k1 } from '../secp256k1.js';
import { P256 } from '../p256.js';
import { P384 } from '../p384.js';
import { P521 } from '../p521.js';
import { x25519 } from '../ed25519.js';
import { x448 } from '../ed448.js';
run(async () => {
const curves = { x25519, secp256k1, P256, P384, P521, x448 };
const fns = {};
for (let [k, c] of Object.entries(curves)) {
const pubB = c.getPublicKey(c.utils.randomPrivateKey());
const privA = c.utils.randomPrivateKey();
fns[k] = () => c.getSharedSecret(privA, pubB);
}
await compare('ecdh', 1000, fns);
});

View File

@@ -0,0 +1,29 @@
import { run, mark, utils } from 'micro-bmark';
import { hash_to_field } from '../abstract/hash-to-curve.js';
import { hashToPrivateScalar } from '../abstract/modular.js';
import { randomBytes } from '@noble/hashes/utils';
import { sha256 } from '@noble/hashes/sha256';
// import { generateData } from './_shared.js';
import { hashToCurve as secp256k1 } from '../secp256k1.js';
import { hashToCurve as P256 } from '../p256.js';
import { hashToCurve as P384 } from '../p384.js';
import { hashToCurve as P521 } from '../p521.js';
import { hashToCurve as ed25519 } from '../ed25519.js';
import { hashToCurve as ed448 } from '../ed448.js';
import { utf8ToBytes } from '../abstract/utils.js';
const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
run(async () => {
const rand = randomBytes(40);
await mark('hashToPrivateScalar', 1000000, () => hashToPrivateScalar(rand, N));
// - p, the characteristic of F
// - m, the extension degree of F, m >= 1
// - L = ceil((ceil(log2(p)) + k) / 8), where k is the security of suite (e.g. 128)
await mark('hash_to_field', 1000000, () =>
hash_to_field(rand, 1, { DST: 'secp256k1', hash: sha256, p: N, m: 1, k: 128 })
);
const msg = utf8ToBytes('message');
for (let [title, fn] of Object.entries({ secp256k1, P256, P384, P521, ed25519, ed448 })) {
await mark(`hashToCurve ${title}`, 1000, () => fn(msg));
}
});

View File

@@ -1,416 +0,0 @@
import * as bench from 'micro-bmark';
const { run, mark } = bench; // or bench.mark
import { readFileSync } from 'fs';
// Curves
import { secp256k1 } from '../lib/secp256k1.js';
import { P256 } from '../lib/p256.js';
import { P384 } from '../lib/p384.js';
import { P521 } from '../lib/p521.js';
import { ed25519 } from '../lib/ed25519.js';
import { ed448 } from '../lib/ed448.js';
import { bls12_381 as bls } from '../lib/bls12-381.js';
// Others
import { hmac } from '@noble/hashes/hmac';
import { sha256 } from '@noble/hashes/sha256';
import { sha512 } from '@noble/hashes/sha512';
import * as old_secp from '@noble/secp256k1';
import * as old_bls from '@noble/bls12-381';
import { concatBytes, hexToBytes } from '@noble/hashes/utils';
import * as starkwareCrypto from '@starkware-industries/starkware-crypto-utils';
import * as stark from '../lib/stark.js';
old_secp.utils.sha256Sync = (...msgs) =>
sha256
.create()
.update(concatBytes(...msgs))
.digest();
old_secp.utils.hmacSha256Sync = (key, ...msgs) =>
hmac
.create(sha256, key)
.update(concatBytes(...msgs))
.digest();
import * as noble_ed25519 from '@noble/ed25519';
noble_ed25519.utils.sha512Sync = (...m) => sha512(concatBytes(...m));
// BLS
const G2_VECTORS = readFileSync('../test/bls12-381/bls12-381-g2-test-vectors.txt', 'utf-8')
.trim()
.split('\n')
.map((l) => l.split(':'));
let p1, p2, oldp1, oldp2;
// /BLS
for (let item of [secp256k1, ed25519, ed448, P256, P384, P521, old_secp, noble_ed25519]) {
item.utils.precompute(8);
}
const ONLY_NOBLE = process.argv[2] === 'noble';
function generateData(namespace) {
const priv = namespace.utils.randomPrivateKey();
const pub = namespace.getPublicKey(priv);
const msg = namespace.utils.randomPrivateKey();
const sig = namespace.sign(msg, priv);
return { priv, pub, msg, sig };
}
export const CURVES = {
secp256k1: {
data: () => {
return generateData(secp256k1);
},
getPublicKey1: {
samples: 10000,
secp256k1_old: () => old_secp.getPublicKey(3n),
secp256k1: () => secp256k1.getPublicKey(3n),
},
getPublicKey255: {
samples: 10000,
secp256k1_old: () => old_secp.getPublicKey(2n ** 255n - 1n),
secp256k1: () => secp256k1.getPublicKey(2n ** 255n - 1n),
},
sign: {
samples: 5000,
secp256k1_old: ({ msg, priv }) => old_secp.signSync(msg, priv),
secp256k1: ({ msg, priv }) => secp256k1.sign(msg, priv),
},
verify: {
samples: 1000,
secp256k1_old: ({ sig, msg, pub }) => {
return old_secp.verify(new old_secp.Signature(sig.r, sig.s), msg, pub);
},
secp256k1: ({ sig, msg, pub }) => secp256k1.verify(sig, msg, pub),
},
getSharedSecret: {
samples: 1000,
secp256k1_old: ({ pub, priv }) => old_secp.getSharedSecret(priv, pub),
secp256k1: ({ pub, priv }) => secp256k1.getSharedSecret(priv, pub),
},
recoverPublicKey: {
samples: 1000,
secp256k1_old: ({ sig, msg }) =>
old_secp.recoverPublicKey(msg, new old_secp.Signature(sig.r, sig.s), sig.recovery),
secp256k1: ({ sig, msg }) => sig.recoverPublicKey(msg),
},
hashToCurve: {
samples: 500,
noble: () => secp256k1.Point.hashToCurve('abcd'),
},
},
ed25519: {
data: () => {
function to32Bytes(numOrStr) {
const hex = typeof numOrStr === 'string' ? numOrStr : numOrStr.toString(16);
return hexToBytes(hex.padStart(64, '0'));
}
const priv = to32Bytes(0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60n);
const pub = noble_ed25519.sync.getPublicKey(priv);
const msg = to32Bytes('deadbeefdeadbeefdeadbeefdeadbeefdeadbeef');
const sig = noble_ed25519.sync.sign(msg, priv);
return { pub, priv, msg, sig };
},
getPublicKey: {
samples: 10000,
old: () => noble_ed25519.sync.getPublicKey(noble_ed25519.utils.randomPrivateKey()),
noble: () => ed25519.getPublicKey(ed25519.utils.randomPrivateKey()),
},
sign: {
samples: 5000,
old: ({ msg, priv }) => noble_ed25519.sync.sign(msg, priv),
noble: ({ msg, priv }) => ed25519.sign(msg, priv),
},
verify: {
samples: 1000,
old: ({ sig, msg, pub }) => noble_ed25519.sync.verify(sig, msg, pub),
noble: ({ sig, msg, pub }) => ed25519.verify(sig, msg, pub),
},
hashToCurve: {
samples: 500,
noble: () => ed25519.Point.hashToCurve('abcd'),
},
},
ed448: {
data: () => {
const priv = ed448.utils.randomPrivateKey();
const pub = ed448.getPublicKey(priv);
const msg = ed448.utils.randomPrivateKey();
const sig = ed448.sign(msg, priv);
return { priv, pub, msg, sig };
},
getPublicKey: {
samples: 5000,
noble: () => ed448.getPublicKey(ed448.utils.randomPrivateKey()),
},
sign: {
samples: 2500,
noble: ({ msg, priv }) => ed448.sign(msg, priv),
},
verify: {
samples: 500,
noble: ({ sig, msg, pub }) => ed448.verify(sig, msg, pub),
},
hashToCurve: {
samples: 500,
noble: () => ed448.Point.hashToCurve('abcd'),
},
},
nist: {
data: () => {
return { p256: generateData(P256), p384: generateData(P384), p521: generateData(P521) };
},
getPublicKey: {
samples: 2500,
P256: () => P256.getPublicKey(P256.utils.randomPrivateKey()),
P384: () => P384.getPublicKey(P384.utils.randomPrivateKey()),
P521: () => P521.getPublicKey(P521.utils.randomPrivateKey()),
},
sign: {
samples: 1000,
P256: ({ p256: { msg, priv } }) => P256.sign(msg, priv),
P384: ({ p384: { msg, priv } }) => P384.sign(msg, priv),
P521: ({ p521: { msg, priv } }) => P521.sign(msg, priv),
},
verify: {
samples: 250,
P256: ({ p256: { sig, msg, pub } }) => P256.verify(sig, msg, pub),
P384: ({ p384: { sig, msg, pub } }) => P384.verify(sig, msg, pub),
P521: ({ p521: { sig, msg, pub } }) => P521.verify(sig, msg, pub),
},
hashToCurve: {
samples: 500,
P256: () => P256.Point.hashToCurve('abcd'),
P384: () => P384.Point.hashToCurve('abcd'),
P521: () => P521.Point.hashToCurve('abcd'),
},
},
stark: {
data: () => {
const priv = '2dccce1da22003777062ee0870e9881b460a8b7eca276870f57c601f182136c';
const msg = 'c465dd6b1bbffdb05442eb17f5ca38ad1aa78a6f56bf4415bdee219114a47';
const pub = stark.getPublicKey(priv);
const sig = stark.sign(msg, priv);
const privateKey = '2dccce1da22003777062ee0870e9881b460a8b7eca276870f57c601f182136c';
const msgHash = 'c465dd6b1bbffdb05442eb17f5ca38ad1aa78a6f56bf4415bdee219114a47';
const keyPair = starkwareCrypto.default.ec.keyFromPrivate(privateKey, 'hex');
const publicKeyStark = starkwareCrypto.default.ec.keyFromPublic(
keyPair.getPublic(true, 'hex'),
'hex'
);
return { priv, sig, msg, pub, publicKeyStark, msgHash, keyPair };
},
pedersen: {
samples: 500,
old: () => {
return starkwareCrypto.default.pedersen([
'3d937c035c878245caf64531a5756109c53068da139362728feb561405371cb',
'208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a',
]);
},
noble: () => {
return stark.pedersen(
'3d937c035c878245caf64531a5756109c53068da139362728feb561405371cb',
'208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a'
);
},
},
verify: {
samples: 500,
old: ({ publicKeyStark, msgHash, keyPair }) => {
return starkwareCrypto.default.verify(
publicKeyStark,
msgHash,
starkwareCrypto.default.sign(keyPair, msgHash)
);
},
noble: ({ priv, msg, pub }) => {
return stark.verify(stark.sign(msg, priv), msg, pub);
},
},
},
'bls12-381': {
data: async () => {
const priv = '28b90deaf189015d3a325908c5e0e4bf00f84f7e639b056ff82d7e70b6eede4c';
const pubs = G2_VECTORS.map((v) => bls.getPublicKey(v[0]));
const sigs = G2_VECTORS.map((v) => v[2]);
const pub = bls.getPublicKey(priv);
const pub512 = pubs.slice(0, 512); // .map(bls.PointG1.fromHex)
const pub32 = pub512.slice(0, 32);
const pub128 = pub512.slice(0, 128);
const pub2048 = pub512.concat(pub512, pub512, pub512);
const sig512 = sigs.slice(0, 512); // .map(bls.PointG2.fromSignature);
const sig32 = sig512.slice(0, 32);
const sig128 = sig512.slice(0, 128);
const sig2048 = sig512.concat(sig512, sig512, sig512);
return {
priv,
pubs,
sigs,
pub,
pub512,
pub32,
pub128,
pub2048,
sig32,
sig128,
sig512,
sig2048,
};
},
init: {
samples: 1,
old: () => {
oldp1 =
old_bls.PointG1.BASE.multiply(
0x28b90deaf189015d3a325908c5e0e4bf00f84f7e639b056ff82d7e70b6eede4cn
);
oldp2 =
old_bls.PointG2.BASE.multiply(
0x28b90deaf189015d3a325908c5e0e4bf00f84f7e639b056ff82d7e70b6eede4dn
);
old_bls.pairing(oldp1, oldp2);
},
noble: () => {
p1 =
bls.G1.Point.BASE.multiply(
0x28b90deaf189015d3a325908c5e0e4bf00f84f7e639b056ff82d7e70b6eede4cn
);
p2 =
bls.G2.Point.BASE.multiply(
0x28b90deaf189015d3a325908c5e0e4bf00f84f7e639b056ff82d7e70b6eede4dn
);
bls.pairing(p1, p2);
},
},
'getPublicKey (1-bit)': {
samples: 1000,
old: () => old_bls.getPublicKey('2'.padStart(64, '0')),
noble: () => bls.getPublicKey('2'.padStart(64, '0')),
},
getPublicKey: {
samples: 1000,
old: ({ priv }) => old_bls.getPublicKey(priv),
noble: ({ priv }) => bls.getPublicKey(priv),
},
sign: {
samples: 50,
old: ({ priv }) => old_bls.sign('09', priv),
noble: ({ priv }) => bls.sign('09', priv),
},
verify: {
samples: 50,
old: ({ pub }) =>
old_bls.verify(
'8647aa9680cd0cdf065b94e818ff2bb948cc97838bcee987b9bc1b76d0a0a6e0d85db4e9d75aaedfc79d4ea2733a21ae0579014de7636dd2943d45b87c82b1c66a289006b0b9767921bb8edd3f6c5c5dec0d54cd65f61513113c50cc977849e5',
'09',
pub
),
noble: ({ pub }) =>
bls.verify(
'8647aa9680cd0cdf065b94e818ff2bb948cc97838bcee987b9bc1b76d0a0a6e0d85db4e9d75aaedfc79d4ea2733a21ae0579014de7636dd2943d45b87c82b1c66a289006b0b9767921bb8edd3f6c5c5dec0d54cd65f61513113c50cc977849e5',
'09',
pub
),
},
pairing: {
samples: 100,
old: () => old_bls.pairing(oldp1, oldp2),
noble: () => bls.pairing(p1, p2),
},
'hashToCurve/G1': {
samples: 500,
old: () => old_bls.PointG1.hashToCurve('abcd'),
noble: () => bls.G1.Point.hashToCurve('abcd'),
},
'hashToCurve/G2': {
samples: 200,
old: () => old_bls.PointG2.hashToCurve('abcd'),
noble: () => bls.G2.Point.hashToCurve('abcd'),
},
// SLOW PART
// Requires points which we cannot init before (data fn same for all)
// await mark('sign/nc', 30, () => bls.sign(msgp, priv));
// await mark('verify/nc', 30, () => bls.verify(sigp, msgp, pubp));
'aggregatePublicKeys/8': {
samples: 100,
old: ({ pubs }) => old_bls.aggregatePublicKeys(pubs.slice(0, 8)),
noble: ({ pubs }) => bls.aggregatePublicKeys(pubs.slice(0, 8)),
},
'aggregatePublicKeys/32': {
samples: 50,
old: ({ pub32 }) => old_bls.aggregatePublicKeys(pub32.map(old_bls.PointG1.fromHex)),
noble: ({ pub32 }) => bls.aggregatePublicKeys(pub32.map(bls.G1.Point.fromHex)),
},
'aggregatePublicKeys/128': {
samples: 20,
old: ({ pub128 }) => old_bls.aggregatePublicKeys(pub128.map(old_bls.PointG1.fromHex)),
noble: ({ pub128 }) => bls.aggregatePublicKeys(pub128.map(bls.G1.Point.fromHex)),
},
'aggregatePublicKeys/512': {
samples: 10,
old: ({ pub512 }) => old_bls.aggregatePublicKeys(pub512.map(old_bls.PointG1.fromHex)),
noble: ({ pub512 }) => bls.aggregatePublicKeys(pub512.map(bls.G1.Point.fromHex)),
},
'aggregatePublicKeys/2048': {
samples: 5,
old: ({ pub2048 }) => old_bls.aggregatePublicKeys(pub2048.map(old_bls.PointG1.fromHex)),
noble: ({ pub2048 }) => bls.aggregatePublicKeys(pub2048.map(bls.G1.Point.fromHex)),
},
'aggregateSignatures/8': {
samples: 50,
old: ({ sigs }) => old_bls.aggregateSignatures(sigs.slice(0, 8)),
noble: ({ sigs }) => bls.aggregateSignatures(sigs.slice(0, 8)),
},
'aggregateSignatures/32': {
samples: 10,
old: ({ sig32 }) => old_bls.aggregateSignatures(sig32.map(old_bls.PointG2.fromSignature)),
noble: ({ sig32 }) => bls.aggregateSignatures(sig32.map(bls.Signature.decode)),
},
'aggregateSignatures/128': {
samples: 5,
old: ({ sig128 }) => old_bls.aggregateSignatures(sig128.map(old_bls.PointG2.fromSignature)),
noble: ({ sig128 }) => bls.aggregateSignatures(sig128.map(bls.Signature.decode)),
},
'aggregateSignatures/512': {
samples: 3,
old: ({ sig512 }) => old_bls.aggregateSignatures(sig512.map(old_bls.PointG2.fromSignature)),
noble: ({ sig512 }) => bls.aggregateSignatures(sig512.map(bls.Signature.decode)),
},
'aggregateSignatures/2048': {
samples: 2,
old: ({ sig2048 }) => old_bls.aggregateSignatures(sig2048.map(old_bls.PointG2.fromSignature)),
noble: ({ sig2048 }) => bls.aggregateSignatures(sig2048.map(bls.Signature.decode)),
},
},
};
const main = () =>
run(async () => {
for (const [name, curve] of Object.entries(CURVES)) {
console.log(`==== ${name} ====`);
const data = await curve.data();
for (const [fnName, libs] of Object.entries(curve)) {
if (fnName === 'data') continue;
const samples = libs.samples;
console.log(` - ${fnName} (samples: ${samples})`);
for (const [lib, fn] of Object.entries(libs)) {
if (lib === 'samples') continue;
if (ONLY_NOBLE && lib !== 'noble') continue;
await mark(` ${lib}`, samples, () => fn(data));
}
}
}
// Log current RAM
bench.logMem();
});
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
main();
}

13
benchmark/modular.js Normal file
View File

@@ -0,0 +1,13 @@
import { run, mark } from 'micro-bmark';
import { secp256k1 } from '../secp256k1.js';
import { Field as Fp } from '../abstract/modular.js';
run(async () => {
console.log(`\x1b[36mmodular, secp256k1 field\x1b[0m`);
const { Fp: secpFp } = secp256k1.CURVE;
await mark('invert a', 300000, () => secpFp.inv(2n ** 232n - 5910n));
await mark('invert b', 300000, () => secpFp.inv(2n ** 231n - 5910n));
await mark('sqrt p = 3 mod 4', 15000, () => secpFp.sqrt(2n ** 231n - 5910n));
const FpStark = Fp(BigInt('0x800000000000011000000000000000000000000000000000000000000000001'));
await mark('sqrt tonneli-shanks', 500, () => FpStark.sqrt(2n ** 231n - 5909n))
});

View File

@@ -12,15 +12,10 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"micro-bmark": "0.2.1" "micro-bmark": "0.3.0"
}, },
"dependencies": { "dependencies": {
"@noble/bls12-381": "^1.4.0",
"@noble/ed25519": "^1.7.1",
"@noble/hashes": "^1.1.5", "@noble/hashes": "^1.1.5",
"@noble/secp256k1": "^1.7.0",
"@starkware-industries/starkware-crypto-utils": "^0.0.2",
"calculate-correlation": "^1.2.3",
"elliptic": "^6.5.4" "elliptic": "^6.5.4"
} }
} }

22
benchmark/secp256k1.js Normal file
View File

@@ -0,0 +1,22 @@
import { run, mark, utils } from 'micro-bmark';
import { secp256k1, schnorr } from '../secp256k1.js';
import { generateData } from './_shared.js';
run(async () => {
const RAM = false;
if (RAM) utils.logMem();
console.log(`\x1b[36msecp256k1\x1b[0m`);
await mark('init', 1, () => secp256k1.utils.precompute(8));
const d = generateData(secp256k1);
await mark('getPublicKey', 10000, () => secp256k1.getPublicKey(d.priv));
await mark('sign', 10000, () => secp256k1.sign(d.msg, d.priv));
await mark('verify', 1000, () => secp256k1.verify(d.sig, d.msg, d.pub));
const pub2 = secp256k1.getPublicKey(secp256k1.utils.randomPrivateKey());
await mark('getSharedSecret', 1000, () => secp256k1.getSharedSecret(d.priv, pub2));
await mark('recoverPublicKey', 1000, () => d.sig.recoverPublicKey(d.msg));
const s = schnorr.sign(d.msg, d.priv);
const spub = schnorr.getPublicKey(d.priv);
await mark('schnorr.sign', 1000, () => schnorr.sign(d.msg, d.priv));
await mark('schnorr.verify', 1000, () => schnorr.verify(s, d.msg, spub));
if (RAM) utils.logMem();
});

View File

@@ -2,6 +2,6 @@
"type": "module", "type": "module",
"browser": { "browser": {
"crypto": false, "crypto": false,
"./crypto": "./esm/cryptoBrowser.js" "./crypto": "./esm/crypto.js"
} }
} }

181
package-lock.json generated Normal file
View File

@@ -0,0 +1,181 @@
{
"name": "@noble/curves",
"version": "0.9.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@noble/curves",
"version": "0.9.1",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.0"
},
"devDependencies": {
"@scure/bip32": "~1.2.0",
"@scure/bip39": "~1.2.0",
"@types/node": "18.11.18",
"fast-check": "3.0.0",
"micro-bmark": "0.3.1",
"micro-should": "0.4.0",
"prettier": "2.8.4",
"typescript": "5.0.2"
}
},
"node_modules/@noble/curves": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-0.8.3.tgz",
"integrity": "sha512-OqaOf4RWDaCRuBKJLDURrgVxjLmneGsiCXGuzYB5y95YithZMA6w4uk34DHSm0rKMrrYiaeZj48/81EvaAScLQ==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"@noble/hashes": "1.3.0"
}
},
"node_modules/@noble/hashes": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz",
"integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
]
},
"node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
]
},
"node_modules/@scure/bip32": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.2.0.tgz",
"integrity": "sha512-O+vT/hBVk+ag2i6j2CDemwd1E1MtGt+7O1KzrPNsaNvSsiEK55MyPIxJIMI2PS8Ijj464B2VbQlpRoQXxw1uHg==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"@noble/curves": "~0.8.3",
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
}
},
"node_modules/@scure/bip39": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.0.tgz",
"integrity": "sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
}
},
"node_modules/@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
},
"node_modules/fast-check": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.0.0.tgz",
"integrity": "sha512-uujtrFJEQQqnIMO52ARwzPcuV4omiL1OJBUBLE9WnNFeu0A97sREXDOmCIHY+Z6KLVcemUf09rWr0q0Xy/Y/Ew==",
"dev": true,
"dependencies": {
"pure-rand": "^5.0.1"
},
"engines": {
"node": ">=8.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
},
"node_modules/micro-bmark": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/micro-bmark/-/micro-bmark-0.3.1.tgz",
"integrity": "sha512-bNaKObD4yPAAPrpEqp5jO6LJ2sEFgLoFSmRjEY809mJ62+2AehI/K3+RlVpN3Oo92RHpgC2RQhj6b1Tb4dmo+w==",
"dev": true
},
"node_modules/micro-should": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/micro-should/-/micro-should-0.4.0.tgz",
"integrity": "sha512-Vclj8yrngSYc9Y3dL2C+AdUlTkyx/syWc4R7LYfk4h7+icfF0DoUBGjjUIaEDzZA19RzoI+Hg8rW9IRoNGP0tQ==",
"dev": true
},
"node_modules/prettier": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz",
"integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pure-rand": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-5.0.5.tgz",
"integrity": "sha512-BwQpbqxSCBJVpamI6ydzcKqyFmnd5msMWUGvzXLm1aXvusbbgkbOto/EUPM00hjveJEaJtdbhUjKSzWRhQVkaw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
]
},
"node_modules/typescript": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz",
"integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=12.20"
}
}
}
}

View File

@@ -1,14 +1,21 @@
{ {
"name": "@noble/curves", "name": "@noble/curves",
"version": "0.5.2", "version": "0.9.1",
"description": "Minimal, auditable JS implementation of elliptic curve cryptography", "description": "Audited & minimal JS implementation of elliptic curve cryptography",
"files": [ "files": [
"lib" "abstract",
"esm",
"src",
"*.js",
"*.js.map",
"*.d.ts",
"*.d.ts.map"
], ],
"scripts": { "scripts": {
"bench": "node benchmark/index.js", "bench": "cd benchmark; node secp256k1.js; node curves.js; node ecdh.js; node hash-to-curve.js; node modular.js; node bls.js",
"build": "tsc && tsc -p tsconfig.esm.json", "build": "tsc && tsc -p tsconfig.esm.json",
"build:release": "rollup -c rollup.config.js", "build:release": "rollup -c rollup.config.js",
"build:clean": "rm *.{js,d.ts,d.ts.map,js.map} esm/*.{js,d.ts,d.ts.map,js.map} 2> /dev/null",
"lint": "prettier --check 'src/**/*.{js,ts}' 'test/*.js'", "lint": "prettier --check 'src/**/*.{js,ts}' 'test/*.js'",
"format": "prettier --write 'src/**/*.{js,ts}' 'test/*.js'", "format": "prettier --write 'src/**/*.{js,ts}' 'test/*.js'",
"test": "node test/index.test.js" "test": "node test/index.test.js"
@@ -21,142 +28,129 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@noble/hashes": "1.1.5" "@noble/hashes": "1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-node-resolve": "13.3.0", "@scure/bip32": "~1.2.0",
"@scure/base": "~1.1.1", "@scure/bip39": "~1.2.0",
"@scure/bip32": "~1.1.1", "@types/node": "18.11.18",
"@scure/bip39": "~1.1.0",
"@types/node": "18.11.3",
"fast-check": "3.0.0", "fast-check": "3.0.0",
"micro-bmark": "0.2.0", "micro-bmark": "0.3.1",
"micro-should": "0.3.0", "micro-should": "0.4.0",
"prettier": "2.6.2", "prettier": "2.8.4",
"rollup": "2.75.5", "typescript": "5.0.2"
"typescript": "4.7.3"
}, },
"main": "index.js", "main": "index.js",
"exports": { "exports": {
".": { ".": {
"types": "./lib/index.d.ts", "types": "./index.d.ts",
"import": "./lib/esm/index.js", "import": "./esm/index.js",
"default": "./lib/index.js" "default": "./index.js"
}, },
"./abstract/edwards": { "./abstract/edwards": {
"types": "./lib/abstract/edwards.d.ts", "types": "./abstract/edwards.d.ts",
"import": "./lib/esm/abstract/edwards.js", "import": "./esm/abstract/edwards.js",
"default": "./lib/abstract/edwards.js" "default": "./abstract/edwards.js"
}, },
"./abstract/modular": { "./abstract/modular": {
"types": "./lib/abstract/modular.d.ts", "types": "./abstract/modular.d.ts",
"import": "./lib/esm/abstract/modular.js", "import": "./esm/abstract/modular.js",
"default": "./lib/abstract/modular.js" "default": "./abstract/modular.js"
}, },
"./abstract/montgomery": { "./abstract/montgomery": {
"types": "./lib/abstract/montgomery.d.ts", "types": "./abstract/montgomery.d.ts",
"import": "./lib/esm/abstract/montgomery.js", "import": "./esm/abstract/montgomery.js",
"default": "./lib/abstract/montgomery.js" "default": "./abstract/montgomery.js"
}, },
"./abstract/weierstrass": { "./abstract/weierstrass": {
"types": "./lib/abstract/weierstrass.d.ts", "types": "./abstract/weierstrass.d.ts",
"import": "./lib/esm/abstract/weierstrass.js", "import": "./esm/abstract/weierstrass.js",
"default": "./lib/abstract/weierstrass.js" "default": "./abstract/weierstrass.js"
}, },
"./abstract/bls": { "./abstract/bls": {
"types": "./lib/abstract/bls.d.ts", "types": "./abstract/bls.d.ts",
"import": "./lib/esm/abstract/bls.js", "import": "./esm/abstract/bls.js",
"default": "./lib/abstract/bls.js" "default": "./abstract/bls.js"
}, },
"./abstract/hash-to-curve": { "./abstract/hash-to-curve": {
"types": "./lib/abstract/hash-to-curve.d.ts", "types": "./abstract/hash-to-curve.d.ts",
"import": "./lib/esm/abstract/hash-to-curve.js", "import": "./esm/abstract/hash-to-curve.js",
"default": "./lib/abstract/hash-to-curve.js" "default": "./abstract/hash-to-curve.js"
}, },
"./abstract/group": { "./abstract/curve": {
"types": "./lib/abstract/group.d.ts", "types": "./abstract/curve.d.ts",
"import": "./lib/esm/abstract/group.js", "import": "./esm/abstract/curve.js",
"default": "./lib/abstract/group.js" "default": "./abstract/curve.js"
}, },
"./abstract/utils": { "./abstract/utils": {
"types": "./lib/abstract/utils.d.ts", "types": "./abstract/utils.d.ts",
"import": "./lib/esm/abstract/utils.js", "import": "./esm/abstract/utils.js",
"default": "./lib/abstract/utils.js" "default": "./abstract/utils.js"
},
"./abstract/poseidon": {
"types": "./abstract/poseidon.d.ts",
"import": "./esm/abstract/poseidon.js",
"default": "./abstract/poseidon.js"
}, },
"./_shortw_utils": { "./_shortw_utils": {
"types": "./lib/_shortw_utils.d.ts", "types": "./_shortw_utils.d.ts",
"import": "./lib/esm/_shortw_utils.js", "import": "./esm/_shortw_utils.js",
"default": "./lib/_shortw_utils.js" "default": "./_shortw_utils.js"
}, },
"./bls12-381": { "./bls12-381": {
"types": "./lib/bls12-381.d.ts", "types": "./bls12-381.d.ts",
"import": "./lib/esm/bls12-381.js", "import": "./esm/bls12-381.js",
"default": "./lib/bls12-381.js" "default": "./bls12-381.js"
}, },
"./bn": { "./bn": {
"types": "./lib/bn.d.ts", "types": "./bn.d.ts",
"import": "./lib/esm/bn.js", "import": "./esm/bn.js",
"default": "./lib/bn.js" "default": "./bn.js"
}, },
"./ed25519": { "./ed25519": {
"types": "./lib/ed25519.d.ts", "types": "./ed25519.d.ts",
"import": "./lib/esm/ed25519.js", "import": "./esm/ed25519.js",
"default": "./lib/ed25519.js" "default": "./ed25519.js"
}, },
"./ed448": { "./ed448": {
"types": "./lib/ed448.d.ts", "types": "./ed448.d.ts",
"import": "./lib/esm/ed448.js", "import": "./esm/ed448.js",
"default": "./lib/ed448.js" "default": "./ed448.js"
}, },
"./index": { "./index": {
"types": "./lib/index.d.ts", "types": "./index.d.ts",
"import": "./lib/esm/index.js", "import": "./esm/index.js",
"default": "./lib/index.js" "default": "./index.js"
}, },
"./jubjub": { "./jubjub": {
"types": "./lib/jubjub.d.ts", "types": "./jubjub.d.ts",
"import": "./lib/esm/jubjub.js", "import": "./esm/jubjub.js",
"default": "./lib/jubjub.js" "default": "./jubjub.js"
},
"./p192": {
"types": "./lib/p192.d.ts",
"import": "./lib/esm/p192.js",
"default": "./lib/p192.js"
},
"./p224": {
"types": "./lib/p224.d.ts",
"import": "./lib/esm/p224.js",
"default": "./lib/p224.js"
}, },
"./p256": { "./p256": {
"types": "./lib/p256.d.ts", "types": "./p256.d.ts",
"import": "./lib/esm/p256.js", "import": "./esm/p256.js",
"default": "./lib/p256.js" "default": "./p256.js"
}, },
"./p384": { "./p384": {
"types": "./lib/p384.d.ts", "types": "./p384.d.ts",
"import": "./lib/esm/p384.js", "import": "./esm/p384.js",
"default": "./lib/p384.js" "default": "./p384.js"
}, },
"./p521": { "./p521": {
"types": "./lib/p521.d.ts", "types": "./p521.d.ts",
"import": "./lib/esm/p521.js", "import": "./esm/p521.js",
"default": "./lib/p521.js" "default": "./p521.js"
}, },
"./pasta": { "./pasta": {
"types": "./lib/pasta.d.ts", "types": "./pasta.d.ts",
"import": "./lib/esm/pasta.js", "import": "./esm/pasta.js",
"default": "./lib/pasta.js" "default": "./pasta.js"
}, },
"./secp256k1": { "./secp256k1": {
"types": "./lib/secp256k1.d.ts", "types": "./secp256k1.d.ts",
"import": "./lib/esm/secp256k1.js", "import": "./esm/secp256k1.js",
"default": "./lib/secp256k1.js" "default": "./secp256k1.js"
},
"./stark": {
"types": "./lib/stark.d.ts",
"import": "./lib/esm/stark.js",
"default": "./lib/stark.js"
} }
}, },
"keywords": [ "keywords": [
@@ -177,7 +171,7 @@
"bn254", "bn254",
"pasta", "pasta",
"bls", "bls",
"nist", "noble",
"ecc", "ecc",
"ecdsa", "ecdsa",
"eddsa", "eddsa",

View File

@@ -4,6 +4,7 @@ import { concatBytes, randomBytes } from '@noble/hashes/utils';
import { weierstrass, CurveType } from './abstract/weierstrass.js'; import { weierstrass, CurveType } from './abstract/weierstrass.js';
import { CHash } from './abstract/utils.js'; import { CHash } from './abstract/utils.js';
// connects noble-curves to noble-hashes
export function getHash(hash: CHash) { export function getHash(hash: CHash) {
return { return {
hash, hash,

View File

@@ -11,107 +11,108 @@
* We are using Fp for private keys (shorter) and Fp₂ for signatures (longer). * We are using Fp for private keys (shorter) and Fp₂ for signatures (longer).
* Some projects may prefer to swap this relation, it is not supported for now. * Some projects may prefer to swap this relation, it is not supported for now.
*/ */
import * as mod from './modular.js'; import { AffinePoint } from './curve.js';
import * as ut from './utils.js'; import { IField, hashToPrivateScalar } from './modular.js';
// Types require separate import import { Hex, PrivKey, CHash, bitLen, bitGet, ensureBytes } from './utils.js';
import { Hex, PrivKey } from './utils.js'; import * as htf from './hash-to-curve.js';
import { import {
htfOpts, CurvePointsType,
stringToBytes, ProjPointType as ProjPointType,
hash_to_field as hashToField, CurvePointsRes,
expand_message_xmd as expandMessageXMD, weierstrassPoints,
} from './hash-to-curve.js'; } from './weierstrass.js';
import { CurvePointsType, PointType, CurvePointsRes, weierstrassPoints } from './weierstrass.js';
type Fp = bigint; // Can be different field? type Fp = bigint; // Can be different field?
export type SignatureCoder<Fp2> = { export type SignatureCoder<Fp2> = {
decode(hex: Hex): PointType<Fp2>; decode(hex: Hex): ProjPointType<Fp2>;
encode(point: PointType<Fp2>): Uint8Array; encode(point: ProjPointType<Fp2>): Uint8Array;
}; };
export type CurveType<Fp, Fp2, Fp6, Fp12> = { export type CurveType<Fp, Fp2, Fp6, Fp12> = {
r: bigint; r: bigint;
G1: Omit<CurvePointsType<Fp>, 'n'>; G1: Omit<CurvePointsType<Fp>, 'n'> & {
mapToCurve: htf.MapToCurve<Fp>;
htfDefaults: htf.Opts;
};
G2: Omit<CurvePointsType<Fp2>, 'n'> & { G2: Omit<CurvePointsType<Fp2>, 'n'> & {
Signature: SignatureCoder<Fp2>; Signature: SignatureCoder<Fp2>;
mapToCurve: htf.MapToCurve<Fp2>;
htfDefaults: htf.Opts;
}; };
x: bigint; x: bigint;
Fp: mod.Field<Fp>; Fp: IField<Fp>;
Fr: mod.Field<bigint>; Fr: IField<bigint>;
Fp2: mod.Field<Fp2> & { Fp2: IField<Fp2> & {
reim: (num: Fp2) => { re: bigint; im: bigint }; reim: (num: Fp2) => { re: bigint; im: bigint };
multiplyByB: (num: Fp2) => Fp2; multiplyByB: (num: Fp2) => Fp2;
frobeniusMap(num: Fp2, power: number): Fp2; frobeniusMap(num: Fp2, power: number): Fp2;
}; };
Fp6: mod.Field<Fp6>; Fp6: IField<Fp6>;
Fp12: mod.Field<Fp12> & { Fp12: IField<Fp12> & {
frobeniusMap(num: Fp12, power: number): Fp12; frobeniusMap(num: Fp12, power: number): Fp12;
multiplyBy014(num: Fp12, o0: Fp2, o1: Fp2, o4: Fp2): Fp12; multiplyBy014(num: Fp12, o0: Fp2, o1: Fp2, o4: Fp2): Fp12;
conjugate(num: Fp12): Fp12; conjugate(num: Fp12): Fp12;
finalExponentiate(num: Fp12): Fp12; finalExponentiate(num: Fp12): Fp12;
}; };
htfDefaults: htfOpts; htfDefaults: htf.Opts;
hash: ut.CHash; // Because we need outputLen for DRBG hash: CHash; // Because we need outputLen for DRBG
randomBytes: (bytesLength?: number) => Uint8Array; randomBytes: (bytesLength?: number) => Uint8Array;
}; };
export type CurveFn<Fp, Fp2, Fp6, Fp12> = { export type CurveFn<Fp, Fp2, Fp6, Fp12> = {
CURVE: CurveType<Fp, Fp2, Fp6, Fp12>; CURVE: CurveType<Fp, Fp2, Fp6, Fp12>;
Fr: mod.Field<bigint>; Fr: IField<bigint>;
Fp: mod.Field<Fp>; Fp: IField<Fp>;
Fp2: mod.Field<Fp2>; Fp2: IField<Fp2>;
Fp6: mod.Field<Fp6>; Fp6: IField<Fp6>;
Fp12: mod.Field<Fp12>; Fp12: IField<Fp12>;
G1: CurvePointsRes<Fp>; G1: CurvePointsRes<Fp> & ReturnType<typeof htf.createHasher<Fp>>;
G2: CurvePointsRes<Fp2>; G2: CurvePointsRes<Fp2> & ReturnType<typeof htf.createHasher<Fp2>>;
Signature: SignatureCoder<Fp2>; Signature: SignatureCoder<Fp2>;
millerLoop: (ell: [Fp2, Fp2, Fp2][], g1: [Fp, Fp]) => Fp12; millerLoop: (ell: [Fp2, Fp2, Fp2][], g1: [Fp, Fp]) => Fp12;
calcPairingPrecomputes: (x: Fp2, y: Fp2) => [Fp2, Fp2, Fp2][]; calcPairingPrecomputes: (p: AffinePoint<Fp2>) => [Fp2, Fp2, Fp2][];
pairing: (P: PointType<Fp>, Q: PointType<Fp2>, withFinalExponent?: boolean) => Fp12; pairing: (P: ProjPointType<Fp>, Q: ProjPointType<Fp2>, withFinalExponent?: boolean) => Fp12;
getPublicKey: (privateKey: PrivKey) => Uint8Array; getPublicKey: (privateKey: PrivKey) => Uint8Array;
sign: { sign: {
(message: Hex, privateKey: PrivKey): Uint8Array; (message: Hex, privateKey: PrivKey): Uint8Array;
(message: PointType<Fp2>, privateKey: PrivKey): PointType<Fp2>; (message: ProjPointType<Fp2>, privateKey: PrivKey): ProjPointType<Fp2>;
}; };
verify: ( verify: (
signature: Hex | PointType<Fp2>, signature: Hex | ProjPointType<Fp2>,
message: Hex | PointType<Fp2>, message: Hex | ProjPointType<Fp2>,
publicKey: Hex | PointType<Fp> publicKey: Hex | ProjPointType<Fp>
) => boolean; ) => boolean;
aggregatePublicKeys: { aggregatePublicKeys: {
(publicKeys: Hex[]): Uint8Array; (publicKeys: Hex[]): Uint8Array;
(publicKeys: PointType<Fp>[]): PointType<Fp>; (publicKeys: ProjPointType<Fp>[]): ProjPointType<Fp>;
}; };
aggregateSignatures: { aggregateSignatures: {
(signatures: Hex[]): Uint8Array; (signatures: Hex[]): Uint8Array;
(signatures: PointType<Fp2>[]): PointType<Fp2>; (signatures: ProjPointType<Fp2>[]): ProjPointType<Fp2>;
}; };
verifyBatch: ( verifyBatch: (
signature: Hex | PointType<Fp2>, signature: Hex | ProjPointType<Fp2>,
messages: (Hex | PointType<Fp2>)[], messages: (Hex | ProjPointType<Fp2>)[],
publicKeys: (Hex | PointType<Fp>)[] publicKeys: (Hex | ProjPointType<Fp>)[]
) => boolean; ) => boolean;
utils: { utils: {
stringToBytes: typeof stringToBytes; randomPrivateKey: () => Uint8Array;
hashToField: typeof hashToField;
expandMessageXMD: typeof expandMessageXMD;
getDSTLabel: () => string;
setDSTLabel(newLabel: string): void;
}; };
}; };
export function bls<Fp2, Fp6, Fp12>( export function bls<Fp2, Fp6, Fp12>(
CURVE: CurveType<Fp, Fp2, Fp6, Fp12> CURVE: CurveType<Fp, Fp2, Fp6, Fp12>
): CurveFn<Fp, Fp2, Fp6, Fp12> { ): CurveFn<Fp, Fp2, Fp6, Fp12> {
// Fields looks pretty specific for curve, so for now we need to pass them with options // Fields looks pretty specific for curve, so for now we need to pass them with opts
const { Fp, Fr, Fp2, Fp6, Fp12 } = CURVE; const { Fp, Fr, Fp2, Fp6, Fp12 } = CURVE;
const BLS_X_LEN = ut.bitLen(CURVE.x); const BLS_X_LEN = bitLen(CURVE.x);
const groupLen = 32; // TODO: calculate; hardcoded for now const groupLen = 32; // TODO: calculate; hardcoded for now
// Pre-compute coefficients for sparse multiplication // Pre-compute coefficients for sparse multiplication
// Point addition and point double calculations is reused for coefficients // Point addition and point double calculations is reused for coefficients
function calcPairingPrecomputes(x: Fp2, y: Fp2) { function calcPairingPrecomputes(p: AffinePoint<Fp2>) {
const { x, y } = p;
// prettier-ignore // prettier-ignore
const Qx = x, Qy = y, Qz = Fp2.ONE; const Qx = x, Qy = y, Qz = Fp2.ONE;
// prettier-ignore // prettier-ignore
@@ -119,32 +120,32 @@ export function bls<Fp2, Fp6, Fp12>(
let ell_coeff: [Fp2, Fp2, Fp2][] = []; let ell_coeff: [Fp2, Fp2, Fp2][] = [];
for (let i = BLS_X_LEN - 2; i >= 0; i--) { for (let i = BLS_X_LEN - 2; i >= 0; i--) {
// Double // Double
let t0 = Fp2.square(Ry); // Ry² let t0 = Fp2.sqr(Ry); // Ry²
let t1 = Fp2.square(Rz); // Rz² let t1 = Fp2.sqr(Rz); // Rz²
let t2 = Fp2.multiplyByB(Fp2.mul(t1, 3n)); // 3 * T1 * B let t2 = Fp2.multiplyByB(Fp2.mul(t1, 3n)); // 3 * T1 * B
let t3 = Fp2.mul(t2, 3n); // 3 * T2 let t3 = Fp2.mul(t2, 3n); // 3 * T2
let t4 = Fp2.sub(Fp2.sub(Fp2.square(Fp2.add(Ry, Rz)), t1), t0); // (Ry + Rz)² - T1 - T0 let t4 = Fp2.sub(Fp2.sub(Fp2.sqr(Fp2.add(Ry, Rz)), t1), t0); // (Ry + Rz)² - T1 - T0
ell_coeff.push([ ell_coeff.push([
Fp2.sub(t2, t0), // T2 - T0 Fp2.sub(t2, t0), // T2 - T0
Fp2.mul(Fp2.square(Rx), 3n), // 3 * Rx² Fp2.mul(Fp2.sqr(Rx), 3n), // 3 * Rx²
Fp2.negate(t4), // -T4 Fp2.neg(t4), // -T4
]); ]);
Rx = Fp2.div(Fp2.mul(Fp2.mul(Fp2.sub(t0, t3), Rx), Ry), 2n); // ((T0 - T3) * Rx * Ry) / 2 Rx = Fp2.div(Fp2.mul(Fp2.mul(Fp2.sub(t0, t3), Rx), Ry), 2n); // ((T0 - T3) * Rx * Ry) / 2
Ry = Fp2.sub(Fp2.square(Fp2.div(Fp2.add(t0, t3), 2n)), Fp2.mul(Fp2.square(t2), 3n)); // ((T0 + T3) / 2)² - 3 * T2² Ry = Fp2.sub(Fp2.sqr(Fp2.div(Fp2.add(t0, t3), 2n)), Fp2.mul(Fp2.sqr(t2), 3n)); // ((T0 + T3) / 2)² - 3 * T2²
Rz = Fp2.mul(t0, t4); // T0 * T4 Rz = Fp2.mul(t0, t4); // T0 * T4
if (ut.bitGet(CURVE.x, i)) { if (bitGet(CURVE.x, i)) {
// Addition // Addition
let t0 = Fp2.sub(Ry, Fp2.mul(Qy, Rz)); // Ry - Qy * Rz let t0 = Fp2.sub(Ry, Fp2.mul(Qy, Rz)); // Ry - Qy * Rz
let t1 = Fp2.sub(Rx, Fp2.mul(Qx, Rz)); // Rx - Qx * Rz let t1 = Fp2.sub(Rx, Fp2.mul(Qx, Rz)); // Rx - Qx * Rz
ell_coeff.push([ ell_coeff.push([
Fp2.sub(Fp2.mul(t0, Qx), Fp2.mul(t1, Qy)), // T0 * Qx - T1 * Qy Fp2.sub(Fp2.mul(t0, Qx), Fp2.mul(t1, Qy)), // T0 * Qx - T1 * Qy
Fp2.negate(t0), // -T0 Fp2.neg(t0), // -T0
t1, // T1 t1, // T1
]); ]);
let t2 = Fp2.square(t1); // T1² let t2 = Fp2.sqr(t1); // T1²
let t3 = Fp2.mul(t2, t1); // T2 * T1 let t3 = Fp2.mul(t2, t1); // T2 * T1
let t4 = Fp2.mul(t2, Rx); // T2 * Rx let t4 = Fp2.mul(t2, Rx); // T2 * Rx
let t5 = Fp2.add(Fp2.sub(t3, Fp2.mul(t4, 2n)), Fp2.mul(Fp2.square(t0), Rz)); // T3 - 2 * T4 + T0² * Rz let t5 = Fp2.add(Fp2.sub(t3, Fp2.mul(t4, 2n)), Fp2.mul(Fp2.sqr(t0), Rz)); // T3 - 2 * T4 + T0² * Rz
Rx = Fp2.mul(t1, t5); // T1 * T5 Rx = Fp2.mul(t1, t5); // T1 * T5
Ry = Fp2.sub(Fp2.mul(Fp2.sub(t4, t5), t0), Fp2.mul(t3, Ry)); // (T4 - T5) * T0 - T3 * Ry Ry = Fp2.sub(Fp2.mul(Fp2.sub(t4, t5), t0), Fp2.mul(t3, Ry)); // (T4 - T5) * T0 - T3 * Ry
Rz = Fp2.mul(Rz, t3); // Rz * T3 Rz = Fp2.mul(Rz, t3); // Rz * T3
@@ -161,46 +162,31 @@ export function bls<Fp2, Fp6, Fp12>(
for (let j = 0, i = BLS_X_LEN - 2; i >= 0; i--, j++) { for (let j = 0, i = BLS_X_LEN - 2; i >= 0; i--, j++) {
const E = ell[j]; const E = ell[j];
f12 = Fp12.multiplyBy014(f12, E[0], Fp2.mul(E[1], Px), Fp2.mul(E[2], Py)); f12 = Fp12.multiplyBy014(f12, E[0], Fp2.mul(E[1], Px), Fp2.mul(E[2], Py));
if (ut.bitGet(x, i)) { if (bitGet(x, i)) {
j += 1; j += 1;
const F = ell[j]; const F = ell[j];
f12 = Fp12.multiplyBy014(f12, F[0], Fp2.mul(F[1], Px), Fp2.mul(F[2], Py)); f12 = Fp12.multiplyBy014(f12, F[0], Fp2.mul(F[1], Px), Fp2.mul(F[2], Py));
} }
if (i !== 0) f12 = Fp12.square(f12); if (i !== 0) f12 = Fp12.sqr(f12);
} }
return Fp12.conjugate(f12); return Fp12.conjugate(f12);
} }
const utils = { const utils = {
hexToBytes: ut.hexToBytes, randomPrivateKey: (): Uint8Array => {
bytesToHex: ut.bytesToHex, return Fr.toBytes(hashToPrivateScalar(CURVE.randomBytes(groupLen + 8), CURVE.r));
stringToBytes: stringToBytes,
// TODO: do we need to export it here?
hashToField: (
msg: Uint8Array,
count: number,
options: Partial<typeof CURVE.htfDefaults> = {}
) => hashToField(msg, count, { ...CURVE.htfDefaults, ...options }),
expandMessageXMD: (msg: Uint8Array, DST: Uint8Array, lenInBytes: number, H = CURVE.hash) =>
expandMessageXMD(msg, DST, lenInBytes, H),
hashToPrivateKey: (hash: Hex): Uint8Array => Fr.toBytes(ut.hashToPrivateScalar(hash, CURVE.r)),
randomBytes: (bytesLength: number = groupLen): Uint8Array => CURVE.randomBytes(bytesLength),
randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(utils.randomBytes(groupLen + 8)),
getDSTLabel: () => CURVE.htfDefaults.DST,
setDSTLabel(newLabel: string) {
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
if (typeof newLabel !== 'string' || newLabel.length > 2048 || newLabel.length === 0) {
throw new TypeError('Invalid DST');
}
CURVE.htfDefaults.DST = newLabel;
}, },
}; };
// Point on G1 curve: (x, y) // Point on G1 curve: (x, y)
const G1 = weierstrassPoints({ const G1_ = weierstrassPoints({ n: Fr.ORDER, ...CURVE.G1 });
n: Fr.ORDER, const G1 = Object.assign(
...CURVE.G1, G1_,
}); htf.createHasher(G1_.ProjectivePoint, CURVE.G1.mapToCurve, {
...CURVE.htfDefaults,
...CURVE.G1.htfDefaults,
})
);
// Sparse multiplication against precomputed coefficients // Sparse multiplication against precomputed coefficients
// TODO: replace with weakmap? // TODO: replace with weakmap?
@@ -208,83 +194,92 @@ export function bls<Fp2, Fp6, Fp12>(
function pairingPrecomputes(point: G2): [Fp2, Fp2, Fp2][] { function pairingPrecomputes(point: G2): [Fp2, Fp2, Fp2][] {
const p = point as G2 & withPairingPrecomputes; const p = point as G2 & withPairingPrecomputes;
if (p._PPRECOMPUTES) return p._PPRECOMPUTES; if (p._PPRECOMPUTES) return p._PPRECOMPUTES;
p._PPRECOMPUTES = calcPairingPrecomputes(p.x, p.y); p._PPRECOMPUTES = calcPairingPrecomputes(point.toAffine());
return p._PPRECOMPUTES; return p._PPRECOMPUTES;
} }
function clearPairingPrecomputes(point: G2) { // TODO: export
const p = point as G2 & withPairingPrecomputes; // function clearPairingPrecomputes(point: G2) {
p._PPRECOMPUTES = undefined; // const p = point as G2 & withPairingPrecomputes;
} // p._PPRECOMPUTES = undefined;
clearPairingPrecomputes; // }
function millerLoopG1(Q: G1, P: G2): Fp12 {
return millerLoop(pairingPrecomputes(P), [Q.x, Q.y]);
}
// Point on G2 curve (complex numbers): (x₁, x₂+i), (y₁, y₂+i) // Point on G2 curve (complex numbers): (x₁, x₂+i), (y₁, y₂+i)
const G2 = weierstrassPoints({ const G2_ = weierstrassPoints({ n: Fr.ORDER, ...CURVE.G2 });
n: Fr.ORDER, const G2 = Object.assign(
...CURVE.G2, G2_,
}); htf.createHasher(G2_.ProjectivePoint as htf.H2CPointConstructor<Fp2>, CURVE.G2.mapToCurve, {
...CURVE.htfDefaults,
...CURVE.G2.htfDefaults,
})
);
const { Signature } = CURVE.G2; const { Signature } = CURVE.G2;
// Calculates bilinear pairing // Calculates bilinear pairing
function pairing(P: G1, Q: G2, withFinalExponent: boolean = true): Fp12 { function pairing(Q: G1, P: G2, withFinalExponent: boolean = true): Fp12 {
if (P.equals(G1.Point.ZERO) || Q.equals(G2.Point.ZERO)) if (Q.equals(G1.ProjectivePoint.ZERO) || P.equals(G2.ProjectivePoint.ZERO))
throw new Error('No pairings at point of Infinity'); throw new Error('pairing is not available for ZERO point');
P.assertValidity();
Q.assertValidity(); Q.assertValidity();
P.assertValidity();
// Performance: 9ms for millerLoop and ~14ms for exp. // Performance: 9ms for millerLoop and ~14ms for exp.
const looped = millerLoopG1(P, Q); const Qa = Q.toAffine();
const looped = millerLoop(pairingPrecomputes(P), [Qa.x, Qa.y]);
return withFinalExponent ? Fp12.finalExponentiate(looped) : looped; return withFinalExponent ? Fp12.finalExponentiate(looped) : looped;
} }
type G1 = typeof G1.Point.BASE; type G1 = typeof G1.ProjectivePoint.BASE;
type G2 = typeof G2.Point.BASE; type G2 = typeof G2.ProjectivePoint.BASE;
type G1Hex = Hex | G1; type G1Hex = Hex | G1;
type G2Hex = Hex | G2; type G2Hex = Hex | G2;
function normP1(point: G1Hex): G1 { function normP1(point: G1Hex): G1 {
return point instanceof G1.Point ? (point as G1) : G1.Point.fromHex(point); return point instanceof G1.ProjectivePoint ? (point as G1) : G1.ProjectivePoint.fromHex(point);
} }
function normP2(point: G2Hex): G2 { function normP2(point: G2Hex): G2 {
return point instanceof G2.Point ? point : Signature.decode(point); return point instanceof G2.ProjectivePoint ? point : Signature.decode(point);
} }
function normP2Hash(point: G2Hex): G2 { function normP2Hash(point: G2Hex, htfOpts?: htf.htfBasicOpts): G2 {
return point instanceof G2.Point ? point : G2.Point.hashToCurve(point); return point instanceof G2.ProjectivePoint
? point
: (G2.hashToCurve(ensureBytes('point', point), htfOpts) as G2);
} }
// Multiplies generator by private key. // Multiplies generator by private key.
// P = pk x G // P = pk x G
function getPublicKey(privateKey: PrivKey): Uint8Array { function getPublicKey(privateKey: PrivKey): Uint8Array {
return G1.Point.fromPrivateKey(privateKey).toRawBytes(true); return G1.ProjectivePoint.fromPrivateKey(privateKey).toRawBytes(true);
} }
// Executes `hashToCurve` on the message and then multiplies the result by private key. // Executes `hashToCurve` on the message and then multiplies the result by private key.
// S = pk x H(m) // S = pk x H(m)
function sign(message: Hex, privateKey: PrivKey): Uint8Array; function sign(message: Hex, privateKey: PrivKey, htfOpts?: htf.htfBasicOpts): Uint8Array;
function sign(message: G2, privateKey: PrivKey): G2; function sign(message: G2, privateKey: PrivKey, htfOpts?: htf.htfBasicOpts): G2;
function sign(message: G2Hex, privateKey: PrivKey): Uint8Array | G2 { function sign(message: G2Hex, privateKey: PrivKey, htfOpts?: htf.htfBasicOpts): Uint8Array | G2 {
const msgPoint = normP2Hash(message); const msgPoint = normP2Hash(message, htfOpts);
msgPoint.assertValidity(); msgPoint.assertValidity();
const sigPoint = msgPoint.multiply(G1.normalizePrivateKey(privateKey)); const sigPoint = msgPoint.multiply(G1.normPrivateKeyToScalar(privateKey));
if (message instanceof G2.Point) return sigPoint; if (message instanceof G2.ProjectivePoint) return sigPoint;
return Signature.encode(sigPoint); return Signature.encode(sigPoint);
} }
// Checks if pairing of public key & hash is equal to pairing of generator & signature. // Checks if pairing of public key & hash is equal to pairing of generator & signature.
// e(P, H(m)) == e(G, S) // e(P, H(m)) == e(G, S)
function verify(signature: G2Hex, message: G2Hex, publicKey: G1Hex): boolean { function verify(
signature: G2Hex,
message: G2Hex,
publicKey: G1Hex,
htfOpts?: htf.htfBasicOpts
): boolean {
const P = normP1(publicKey); const P = normP1(publicKey);
const Hm = normP2Hash(message); const Hm = normP2Hash(message, htfOpts);
const G = G1.Point.BASE; const G = G1.ProjectivePoint.BASE;
const S = normP2(signature); const S = normP2(signature);
// Instead of doing 2 exponentiations, we use property of billinear maps // Instead of doing 2 exponentiations, we use property of billinear maps
// and do one exp after multiplying 2 points. // and do one exp after multiplying 2 points.
const ePHm = pairing(P.negate(), Hm, false); const ePHm = pairing(P.negate(), Hm, false);
const eGS = pairing(G, S, false); const eGS = pairing(G, S, false);
const exp = Fp12.finalExponentiate(Fp12.mul(eGS, ePHm)); const exp = Fp12.finalExponentiate(Fp12.mul(eGS, ePHm));
return Fp12.equals(exp, Fp12.ONE); return Fp12.eql(exp, Fp12.ONE);
} }
// Adds a bunch of public key points together. // Adds a bunch of public key points together.
@@ -293,11 +288,9 @@ export function bls<Fp2, Fp6, Fp12>(
function aggregatePublicKeys(publicKeys: G1[]): G1; function aggregatePublicKeys(publicKeys: G1[]): G1;
function aggregatePublicKeys(publicKeys: G1Hex[]): Uint8Array | G1 { function aggregatePublicKeys(publicKeys: G1Hex[]): Uint8Array | G1 {
if (!publicKeys.length) throw new Error('Expected non-empty array'); if (!publicKeys.length) throw new Error('Expected non-empty array');
const agg = publicKeys const agg = publicKeys.map(normP1).reduce((sum, p) => sum.add(p), G1.ProjectivePoint.ZERO);
.map(normP1) const aggAffine = agg; //.toAffine();
.reduce((sum, p) => sum.add(G1.ProjectivePoint.fromAffine(p)), G1.ProjectivePoint.ZERO); if (publicKeys[0] instanceof G1.ProjectivePoint) {
const aggAffine = agg.toAffine();
if (publicKeys[0] instanceof G1.Point) {
aggAffine.assertValidity(); aggAffine.assertValidity();
return aggAffine; return aggAffine;
} }
@@ -310,11 +303,9 @@ export function bls<Fp2, Fp6, Fp12>(
function aggregateSignatures(signatures: G2[]): G2; function aggregateSignatures(signatures: G2[]): G2;
function aggregateSignatures(signatures: G2Hex[]): Uint8Array | G2 { function aggregateSignatures(signatures: G2Hex[]): Uint8Array | G2 {
if (!signatures.length) throw new Error('Expected non-empty array'); if (!signatures.length) throw new Error('Expected non-empty array');
const agg = signatures const agg = signatures.map(normP2).reduce((sum, s) => sum.add(s), G2.ProjectivePoint.ZERO);
.map(normP2) const aggAffine = agg; //.toAffine();
.reduce((sum, s) => sum.add(G2.ProjectivePoint.fromAffine(s)), G2.ProjectivePoint.ZERO); if (signatures[0] instanceof G2.ProjectivePoint) {
const aggAffine = agg.toAffine();
if (signatures[0] instanceof G2.Point) {
aggAffine.assertValidity(); aggAffine.assertValidity();
return aggAffine; return aggAffine;
} }
@@ -323,12 +314,20 @@ export function bls<Fp2, Fp6, Fp12>(
// https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407 // https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407
// e(G, S) = e(G, SUM(n)(Si)) = MUL(n)(e(G, Si)) // e(G, S) = e(G, SUM(n)(Si)) = MUL(n)(e(G, Si))
function verifyBatch(signature: G2Hex, messages: G2Hex[], publicKeys: G1Hex[]): boolean { function verifyBatch(
signature: G2Hex,
messages: G2Hex[],
publicKeys: G1Hex[],
htfOpts?: htf.htfBasicOpts
): boolean {
// @ts-ignore
// console.log('verifyBatch', bytesToHex(signature as any), messages, publicKeys.map(bytesToHex));
if (!messages.length) throw new Error('Expected non-empty messages array'); if (!messages.length) throw new Error('Expected non-empty messages array');
if (publicKeys.length !== messages.length) if (publicKeys.length !== messages.length)
throw new Error('Pubkey count should equal msg count'); throw new Error('Pubkey count should equal msg count');
const sig = normP2(signature); const sig = normP2(signature);
const nMessages = messages.map(normP2Hash); const nMessages = messages.map((i) => normP2Hash(i, htfOpts));
const nPublicKeys = publicKeys.map(normP1); const nPublicKeys = publicKeys.map(normP1);
try { try {
const paired = []; const paired = [];
@@ -336,23 +335,23 @@ export function bls<Fp2, Fp6, Fp12>(
const groupPublicKey = nMessages.reduce( const groupPublicKey = nMessages.reduce(
(groupPublicKey, subMessage, i) => (groupPublicKey, subMessage, i) =>
subMessage === message ? groupPublicKey.add(nPublicKeys[i]) : groupPublicKey, subMessage === message ? groupPublicKey.add(nPublicKeys[i]) : groupPublicKey,
G1.Point.ZERO G1.ProjectivePoint.ZERO
); );
// const msg = message instanceof PointG2 ? message : await PointG2.hashToCurve(message); // const msg = message instanceof PointG2 ? message : await PointG2.hashToCurve(message);
// Possible to batch pairing for same msg with different groupPublicKey here // Possible to batch pairing for same msg with different groupPublicKey here
paired.push(pairing(groupPublicKey, message, false)); paired.push(pairing(groupPublicKey, message, false));
} }
paired.push(pairing(G1.Point.BASE.negate(), sig, false)); paired.push(pairing(G1.ProjectivePoint.BASE.negate(), sig, false));
const product = paired.reduce((a, b) => Fp12.mul(a, b), Fp12.ONE); const product = paired.reduce((a, b) => Fp12.mul(a, b), Fp12.ONE);
const exp = Fp12.finalExponentiate(product); const exp = Fp12.finalExponentiate(product);
return Fp12.equals(exp, Fp12.ONE); return Fp12.eql(exp, Fp12.ONE);
} catch { } catch {
return false; return false;
} }
} }
// Pre-compute points. Refer to README. G1.ProjectivePoint.BASE._setWindowSize(4);
G1.Point.BASE._setWindowSize(4);
return { return {
CURVE, CURVE,
Fr, Fr,

View File

@@ -1,22 +1,41 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// Abelian group utilities // Abelian group utilities
import { IField, validateField, nLength } from './modular.js';
import { validateObject } from './utils.js';
const _0n = BigInt(0); const _0n = BigInt(0);
const _1n = BigInt(1); const _1n = BigInt(1);
export type AffinePoint<T> = {
x: T;
y: T;
} & { z?: never; t?: never };
export interface Group<T extends Group<T>> { export interface Group<T extends Group<T>> {
double(): T; double(): T;
negate(): T; negate(): T;
add(other: T): T; add(other: T): T;
subtract(other: T): T; subtract(other: T): T;
equals(other: T): boolean; equals(other: T): boolean;
multiply(scalar: number | bigint): T; multiply(scalar: bigint): T;
} }
export type GroupConstructor<T> = { export type GroupConstructor<T> = {
BASE: T; BASE: T;
ZERO: T; ZERO: T;
}; };
// Not big, but pretty complex and it is easy to break stuff. To avoid too much copy paste export type Mapper<T> = (i: T[]) => T[];
// Elliptic curve multiplication of Point by scalar. Fragile.
// Scalars should always be less than curve order: this should be checked inside of a curve itself.
// Creates precomputation tables for fast multiplication:
// - private scalar is split by fixed size windows of W bits
// - every window point is collected from window's table & added to accumulator
// - since windows are different, same point inside tables won't be accessed more than once per calc
// - each multiplication is 'Math.ceil(CURVE_ORDER / 𝑊) + 1' point additions (fixed for any scalar)
// - +1 window is neccessary for wNAF
// - wNAF reduces table size: 2x less memory + 2x faster generation, but 10% slower multiplication
// TODO: Research returning 2d JS array of windows, instead of a single window. This would allow
// windows to be in different memory locations
export function wNAF<T extends Group<T>>(c: GroupConstructor<T>, bits: number) { export function wNAF<T extends Group<T>>(c: GroupConstructor<T>, bits: number) {
const constTimeNegate = (condition: boolean, item: T): T => { const constTimeNegate = (condition: boolean, item: T): T => {
const neg = item.negate(); const neg = item.negate();
@@ -44,8 +63,12 @@ export function wNAF<T extends Group<T>>(c: GroupConstructor<T>, bits: number) {
/** /**
* Creates a wNAF precomputation window. Used for caching. * Creates a wNAF precomputation window. Used for caching.
* Default window size is set by `utils.precompute()` and is equal to 8. * Default window size is set by `utils.precompute()` and is equal to 8.
* Which means we are caching 65536 points: 256 points for every bit from 0 to 256. * Number of precomputed points depends on the curve size:
* @returns 65K precomputed points, depending on W * 2^(𝑊1) * (Math.ceil(𝑛 / 𝑊) + 1), where:
* - 𝑊 is the window size
* - 𝑛 is the bitlength of the curve order.
* For a 256-bit curve and window size 8, the number of precomputed points is 128 * 33 = 4224.
* @returns precomputed point tables flattened to a single array
*/ */
precomputeWindow(elm: T, W: number): Group<T>[] { precomputeWindow(elm: T, W: number): Group<T>[] {
const { windows, windowSize } = opts(W); const { windows, windowSize } = opts(W);
@@ -66,14 +89,14 @@ export function wNAF<T extends Group<T>>(c: GroupConstructor<T>, bits: number) {
}, },
/** /**
* Implements w-ary non-adjacent form for calculating ec multiplication. * Implements ec multiplication using precomputed tables and w-ary non-adjacent form.
* @param W window size * @param W window size
* @param affinePoint optional 2d point to save cached precompute windows on it. * @param precomputes precomputed tables
* @param n bits * @param n scalar (we don't check here, but should be less than curve order)
* @returns real and fake (for const-time) points * @returns real and fake (for const-time) points
*/ */
wNAF(W: number, precomputes: T[], n: bigint): { p: T; f: T } { wNAF(W: number, precomputes: T[], n: bigint): { p: T; f: T } {
// TODO: maybe check that scalar is less than group order? wNAF will fail otherwise // TODO: maybe check that scalar is less than group order? wNAF behavious is undefined otherwise
// But need to carefully remove other checks before wNAF. ORDER == bits here // But need to carefully remove other checks before wNAF. ORDER == bits here
const { windows, windowSize } = opts(W); const { windows, windowSize } = opts(W);
@@ -125,5 +148,56 @@ export function wNAF<T extends Group<T>>(c: GroupConstructor<T>, bits: number) {
// which makes it less const-time: around 1 bigint multiply. // which makes it less const-time: around 1 bigint multiply.
return { p, f }; return { p, f };
}, },
wNAFCached(P: T, precomputesMap: Map<T, T[]>, n: bigint, transform: Mapper<T>): { p: T; f: T } {
// @ts-ignore
const W: number = P._WINDOW_SIZE || 1;
// Calculate precomputes on a first run, reuse them after
let comp = precomputesMap.get(P);
if (!comp) {
comp = this.precomputeWindow(P, W) as T[];
if (W !== 1) {
precomputesMap.set(P, transform(comp));
}
}
return this.wNAF(W, comp, n);
},
}; };
} }
// Generic BasicCurve interface: works even for polynomial fields (BLS): P, n, h would be ok.
// Though generator can be different (Fp2 / Fp6 for BLS).
export type BasicCurve<T> = {
Fp: IField<T>; // Field over which we'll do calculations (Fp)
n: bigint; // Curve order, total count of valid points in the field
nBitLength?: number; // bit length of curve order
nByteLength?: number; // byte length of curve order
h: bigint; // cofactor. we can assign default=1, but users will just ignore it w/o validation
hEff?: bigint; // Number to multiply to clear cofactor
Gx: T; // base point X coordinate
Gy: T; // base point Y coordinate
allowInfinityPoint?: boolean; // bls12-381 requires it. ZERO point is valid, but invalid pubkey
};
export function validateBasic<FP, T>(curve: BasicCurve<FP> & T) {
validateField(curve.Fp);
validateObject(
curve,
{
n: 'bigint',
h: 'bigint',
Gx: 'field',
Gy: 'field',
},
{
nBitLength: 'isSafeInteger',
nByteLength: 'isSafeInteger',
}
);
// Set defaults
return Object.freeze({
...nLength(curve.n, curve.nBitLength),
...curve,
...{ p: curve.Fp.ORDER },
} as const);
}

View File

@@ -1,163 +1,105 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y² // Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y²
import { mod } from './modular.js';
// Differences from @noble/ed25519 1.7:
// 1. Variable field element lengths between EDDSA/ECDH:
// EDDSA (RFC8032) is 456 bits / 57 bytes, ECDH (RFC7748) is 448 bits / 56 bytes
// 2. Different addition formula (doubling is same)
// 3. uvRatio differs between curves (half-expected, not only pow fn changes)
// 4. Point decompression code is different (unexpected), now using generalized formula
// 5. Domain function was no-op for ed25519, but adds some data even with empty context for ed448
import * as mod from './modular.js';
import * as ut from './utils.js'; import * as ut from './utils.js';
import { ensureBytes, Hex, PrivKey } from './utils.js'; import { ensureBytes, FHash, Hex } from './utils.js';
import { Group, GroupConstructor, wNAF } from './group.js'; import { Group, GroupConstructor, wNAF, BasicCurve, validateBasic, AffinePoint } from './curve.js';
import { hash_to_field as hashToField, htfOpts, validateHTFOpts } from './hash-to-curve.js';
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n // Be friendly to bad ECMAScript parsers by not using bigint literals
const _0n = BigInt(0); // prettier-ignore
const _1n = BigInt(1); const _0n = BigInt(0), _1n = BigInt(1), _2n = BigInt(2), _8n = BigInt(8);
const _2n = BigInt(2);
const _8n = BigInt(8);
// Edwards curves must declare params a & d. // Edwards curves must declare params a & d.
export type CurveType = ut.BasicCurve<bigint> & { export type CurveType = BasicCurve<bigint> & {
// Params: a, d a: bigint; // curve param a
a: bigint; d: bigint; // curve param d
d: bigint; hash: FHash; // Hashing
// Hashes randomBytes: (bytesLength?: number) => Uint8Array; // CSPRNG
// The interface, because we need outputLen for DRBG adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array; // clears bits to get valid field elemtn
hash: ut.CHash; domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array; // Used for hashing
// CSPRNG uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint }; // Ratio √(u/v)
randomBytes: (bytesLength?: number) => Uint8Array; preHash?: FHash; // RFC 8032 pre-hashing of messages to sign() / verify()
// Probably clears bits in a byte array to produce a valid field element mapToCurve?: (scalar: bigint[]) => AffinePoint<bigint>; // for hash-to-curve standard
adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array;
// Used during hashing
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
// Ratio √(u/v)
uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint };
// RFC 8032 pre-hashing of messages to sign() / verify()
preHash?: ut.CHash;
// Hash to field options
htfDefaults?: htfOpts;
mapToCurve?: (scalar: bigint[]) => { x: bigint; y: bigint };
}; };
function validateOpts(curve: CurveType) { function validateOpts(curve: CurveType) {
const opts = ut.validateOpts(curve); const opts = validateBasic(curve);
if (typeof opts.hash !== 'function' || !ut.isPositiveInt(opts.hash.outputLen)) ut.validateObject(
throw new Error('Invalid hash function'); curve,
for (const i of ['a', 'd'] as const) { {
const val = opts[i]; hash: 'function',
if (typeof val !== 'bigint') throw new Error(`Invalid curve param ${i}=${val} (${typeof val})`); a: 'bigint',
d: 'bigint',
randomBytes: 'function',
},
{
adjustScalarBytes: 'function',
domain: 'function',
uvRatio: 'function',
mapToCurve: 'function',
} }
for (const fn of ['randomBytes'] as const) { );
if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
}
for (const fn of ['adjustScalarBytes', 'domain', 'uvRatio', 'mapToCurve'] as const) {
if (opts[fn] === undefined) continue; // Optional
if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
}
if (opts.htfDefaults !== undefined) validateHTFOpts(opts.htfDefaults);
// Set defaults // Set defaults
return Object.freeze({ ...opts } as const); return Object.freeze({ ...opts } as const);
} }
// Instance
export interface SignatureType {
readonly r: PointType;
readonly s: bigint;
assertValidity(): SignatureType;
toRawBytes(): Uint8Array;
toHex(): string;
}
// Static methods
export type SignatureConstructor = {
new (r: PointType, s: bigint): SignatureType;
fromHex(hex: Hex): SignatureType;
};
// Instance of Extended Point with coordinates in X, Y, Z, T // Instance of Extended Point with coordinates in X, Y, Z, T
export interface ExtendedPointType extends Group<ExtendedPointType> { export interface ExtPointType extends Group<ExtPointType> {
readonly x: bigint; readonly ex: bigint;
readonly y: bigint; readonly ey: bigint;
readonly z: bigint; readonly ez: bigint;
readonly t: bigint; readonly et: bigint;
multiply(scalar: number | bigint, affinePoint?: PointType): ExtendedPointType; get x(): bigint;
multiplyUnsafe(scalar: number | bigint): ExtendedPointType; get y(): bigint;
assertValidity(): void;
multiply(scalar: bigint): ExtPointType;
multiplyUnsafe(scalar: bigint): ExtPointType;
isSmallOrder(): boolean; isSmallOrder(): boolean;
isTorsionFree(): boolean; isTorsionFree(): boolean;
toAffine(invZ?: bigint): PointType; clearCofactor(): ExtPointType;
clearCofactor(): ExtendedPointType; toAffine(iz?: bigint): AffinePoint<bigint>;
}
// Static methods of Extended Point with coordinates in X, Y, Z, T
export interface ExtendedPointConstructor extends GroupConstructor<ExtendedPointType> {
new (x: bigint, y: bigint, z: bigint, t: bigint): ExtendedPointType;
fromAffine(p: PointType): ExtendedPointType;
toAffineBatch(points: ExtendedPointType[]): PointType[];
normalizeZ(points: ExtendedPointType[]): ExtendedPointType[];
}
// Instance of Affine Point with coordinates in X, Y
export interface PointType extends Group<PointType> {
readonly x: bigint;
readonly y: bigint;
_setWindowSize(windowSize: number): void;
toRawBytes(isCompressed?: boolean): Uint8Array; toRawBytes(isCompressed?: boolean): Uint8Array;
toHex(isCompressed?: boolean): string; toHex(isCompressed?: boolean): string;
isTorsionFree(): boolean;
clearCofactor(): PointType;
} }
// Static methods of Affine Point with coordinates in X, Y // Static methods of Extended Point with coordinates in X, Y, Z, T
export interface PointConstructor extends GroupConstructor<PointType> { export interface ExtPointConstructor extends GroupConstructor<ExtPointType> {
new (x: bigint, y: bigint): PointType; new (x: bigint, y: bigint, z: bigint, t: bigint): ExtPointType;
fromHex(hex: Hex): PointType; fromAffine(p: AffinePoint<bigint>): ExtPointType;
fromPrivateKey(privateKey: PrivKey): PointType; fromHex(hex: Hex): ExtPointType;
hashToCurve(msg: Hex, options?: Partial<htfOpts>): PointType; fromPrivateKey(privateKey: Hex): ExtPointType;
encodeToCurve(msg: Hex, options?: Partial<htfOpts>): PointType;
} }
export type PubKey = Hex | PointType;
export type SigType = Hex | SignatureType;
export type CurveFn = { export type CurveFn = {
CURVE: ReturnType<typeof validateOpts>; CURVE: ReturnType<typeof validateOpts>;
getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array; getPublicKey: (privateKey: Hex) => Uint8Array;
sign: (message: Hex, privateKey: Hex) => Uint8Array; sign: (message: Hex, privateKey: Hex) => Uint8Array;
verify: (sig: SigType, message: Hex, publicKey: PubKey) => boolean; verify: (sig: Hex, message: Hex, publicKey: Hex) => boolean;
Point: PointConstructor; ExtendedPoint: ExtPointConstructor;
ExtendedPoint: ExtendedPointConstructor;
Signature: SignatureConstructor;
utils: { utils: {
randomPrivateKey: () => Uint8Array; randomPrivateKey: () => Uint8Array;
getExtendedPublicKey: (key: PrivKey) => { getExtendedPublicKey: (key: Hex) => {
head: Uint8Array; head: Uint8Array;
prefix: Uint8Array; prefix: Uint8Array;
scalar: bigint; scalar: bigint;
point: PointType; point: ExtPointType;
pointBytes: Uint8Array; pointBytes: Uint8Array;
}; };
}; };
}; };
// NOTE: it is not generic twisted curve for now, but ed25519/ed448 generic implementation // It is not generic twisted curve for now, but ed25519/ed448 generic implementation
export function twistedEdwards(curveDef: CurveType): CurveFn { export function twistedEdwards(curveDef: CurveType): CurveFn {
const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>; const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>;
const Fp = CURVE.Fp; const { Fp, n: CURVE_ORDER, preHash, hash: cHash, randomBytes, nByteLength, h: cofactor } = CURVE;
const CURVE_ORDER = CURVE.n; const MASK = _2n ** BigInt(nByteLength * 8);
const maxGroupElement = _2n ** BigInt(CURVE.nByteLength * 8); const modP = Fp.create; // Function overrides
// Function overrides
const { randomBytes } = CURVE;
const modP = Fp.create;
// sqrt(u/v) // sqrt(u/v)
const uvRatio = const uvRatio =
CURVE.uvRatio || CURVE.uvRatio ||
((u: bigint, v: bigint) => { ((u: bigint, v: bigint) => {
try { try {
return { isValid: true, value: Fp.sqrt(u * Fp.invert(v)) }; return { isValid: true, value: Fp.sqrt(u * Fp.inv(v)) };
} catch (e) { } catch (e) {
return { isValid: false, value: _0n }; return { isValid: false, value: _0n };
} }
@@ -169,41 +111,95 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
if (ctx.length || phflag) throw new Error('Contexts/pre-hash are not supported'); if (ctx.length || phflag) throw new Error('Contexts/pre-hash are not supported');
return data; return data;
}); // NOOP }); // NOOP
const inBig = (n: bigint) => typeof n === 'bigint' && _0n < n; // n in [1..]
/** const inRange = (n: bigint, max: bigint) => inBig(n) && inBig(max) && n < max; // n in [1..max-1]
* Extended Point works in extended coordinates: (x, y, z, t) ∋ (x=x/z, y=y/z, t=xy). const in0MaskRange = (n: bigint) => n === _0n || inRange(n, MASK); // n in [0..MASK-1]
* Default Point works in affine coordinates: (x, y) function assertInRange(n: bigint, max: bigint) {
* https://en.wikipedia.org/wiki/Twisted_Edwards_curve#Extended_coordinates // n in [1..max-1]
*/ if (inRange(n, max)) return n;
class ExtendedPoint implements ExtendedPointType { throw new Error(`Expected valid scalar < ${max}, got ${typeof n} ${n}`);
constructor(readonly x: bigint, readonly y: bigint, readonly z: bigint, readonly t: bigint) {}
static BASE = new ExtendedPoint(CURVE.Gx, CURVE.Gy, _1n, modP(CURVE.Gx * CURVE.Gy));
static ZERO = new ExtendedPoint(_0n, _1n, _1n, _0n);
static fromAffine(p: Point): ExtendedPoint {
if (!(p instanceof Point)) {
throw new TypeError('ExtendedPoint#fromAffine: expected Point');
} }
if (p.equals(Point.ZERO)) return ExtendedPoint.ZERO; function assertGE0(n: bigint) {
return new ExtendedPoint(p.x, p.y, _1n, modP(p.x * p.y)); // n in [0..CURVE_ORDER-1]
return n === _0n ? n : assertInRange(n, CURVE_ORDER); // GE = prime subgroup, not full group
} }
// Takes a bunch of Jacobian Points but executes only one const pointPrecomputes = new Map<Point, Point[]>();
// invert on all of them. invert is very slow operation, function isPoint(other: unknown) {
// so this improves performance massively. if (!(other instanceof Point)) throw new Error('ExtendedPoint expected');
static toAffineBatch(points: ExtendedPoint[]): Point[] { }
const toInv = Fp.invertBatch(points.map((p) => p.z)); // Extended Point works in extended coordinates: (x, y, z, t) ∋ (x=x/z, y=y/z, t=xy).
return points.map((p, i) => p.toAffine(toInv[i])); // https://en.wikipedia.org/wiki/Twisted_Edwards_curve#Extended_coordinates
class Point implements ExtPointType {
static readonly BASE = new Point(CURVE.Gx, CURVE.Gy, _1n, modP(CURVE.Gx * CURVE.Gy));
static readonly ZERO = new Point(_0n, _1n, _1n, _0n); // 0, 1, 1, 0
constructor(
readonly ex: bigint,
readonly ey: bigint,
readonly ez: bigint,
readonly et: bigint
) {
if (!in0MaskRange(ex)) throw new Error('x required');
if (!in0MaskRange(ey)) throw new Error('y required');
if (!in0MaskRange(ez)) throw new Error('z required');
if (!in0MaskRange(et)) throw new Error('t required');
} }
static normalizeZ(points: ExtendedPoint[]): ExtendedPoint[] { get x(): bigint {
return this.toAffineBatch(points).map(this.fromAffine); return this.toAffine().x;
}
get y(): bigint {
return this.toAffine().y;
}
static fromAffine(p: AffinePoint<bigint>): Point {
if (p instanceof Point) throw new Error('extended point not allowed');
const { x, y } = p || {};
if (!in0MaskRange(x) || !in0MaskRange(y)) throw new Error('invalid affine point');
return new Point(x, y, _1n, modP(x * y));
}
static normalizeZ(points: Point[]): Point[] {
const toInv = Fp.invertBatch(points.map((p) => p.ez));
return points.map((p, i) => p.toAffine(toInv[i])).map(Point.fromAffine);
}
// We calculate precomputes for elliptic curve point multiplication
// using windowed method. This specifies window size and
// stores precomputed values. Usually only base point would be precomputed.
_WINDOW_SIZE?: number;
// "Private method", don't use it directly
_setWindowSize(windowSize: number) {
this._WINDOW_SIZE = windowSize;
pointPrecomputes.delete(this);
}
// Not required for fromHex(), which always creates valid points.
// Could be useful for fromAffine().
assertValidity(): void {
const { a, d } = CURVE;
if (this.is0()) throw new Error('bad point: ZERO'); // TODO: optimize, with vars below?
// Equation in affine coordinates: ax² + y² = 1 + dx²y²
// Equation in projective coordinates (X/Z, Y/Z, Z): (aX² + Y²)Z² = Z⁴ + dX²Y²
const { ex: X, ey: Y, ez: Z, et: T } = this;
const X2 = modP(X * X); // X²
const Y2 = modP(Y * Y); // Y²
const Z2 = modP(Z * Z); // Z²
const Z4 = modP(Z2 * Z2); // Z⁴
const aX2 = modP(X2 * a); // aX²
const left = modP(Z2 * modP(aX2 + Y2)); // (aX² + Y²)Z²
const right = modP(Z4 + modP(d * modP(X2 * Y2))); // Z⁴ + dX²Y²
if (left !== right) throw new Error('bad point: equation left != right (1)');
// In Extended coordinates we also have T, which is x*y=T/Z: check X*Y == Z*T
const XY = modP(X * Y);
const ZT = modP(Z * T);
if (XY !== ZT) throw new Error('bad point: equation left != right (2)');
} }
// Compare one point to another. // Compare one point to another.
equals(other: ExtendedPoint): boolean { equals(other: Point): boolean {
assertExtPoint(other); isPoint(other);
const { x: X1, y: Y1, z: Z1 } = this; const { ex: X1, ey: Y1, ez: Z1 } = this;
const { x: X2, y: Y2, z: Z2 } = other; const { ex: X2, ey: Y2, ez: Z2 } = other;
const X1Z2 = modP(X1 * Z2); const X1Z2 = modP(X1 * Z2);
const X2Z1 = modP(X2 * Z1); const X2Z1 = modP(X2 * Z1);
const Y1Z2 = modP(Y1 * Z2); const Y1Z2 = modP(Y1 * Z2);
@@ -211,17 +207,21 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
return X1Z2 === X2Z1 && Y1Z2 === Y2Z1; return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;
} }
// Inverses point to one corresponding to (x, -y) in Affine coordinates. protected is0(): boolean {
negate(): ExtendedPoint { return this.equals(Point.ZERO);
return new ExtendedPoint(modP(-this.x), this.y, this.z, modP(-this.t)); }
negate(): Point {
// Flips point sign to a negative one (-x, y in affine coords)
return new Point(modP(-this.ex), this.ey, this.ez, modP(-this.et));
} }
// Fast algo for doubling Extended Point. // Fast algo for doubling Extended Point.
// https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#doubling-dbl-2008-hwcd // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#doubling-dbl-2008-hwcd
// Cost: 4M + 4S + 1*a + 6add + 1*2. // Cost: 4M + 4S + 1*a + 6add + 1*2.
double(): ExtendedPoint { double(): Point {
const { a } = CURVE; const { a } = CURVE;
const { x: X1, y: Y1, z: Z1 } = this; const { ex: X1, ey: Y1, ez: Z1 } = this;
const A = modP(X1 * X1); // A = X12 const A = modP(X1 * X1); // A = X12
const B = modP(Y1 * Y1); // B = Y12 const B = modP(Y1 * Y1); // B = Y12
const C = modP(_2n * modP(Z1 * Z1)); // C = 2*Z12 const C = modP(_2n * modP(Z1 * Z1)); // C = 2*Z12
@@ -235,17 +235,17 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
const Y3 = modP(G * H); // Y3 = G*H const Y3 = modP(G * H); // Y3 = G*H
const T3 = modP(E * H); // T3 = E*H const T3 = modP(E * H); // T3 = E*H
const Z3 = modP(F * G); // Z3 = F*G const Z3 = modP(F * G); // Z3 = F*G
return new ExtendedPoint(X3, Y3, Z3, T3); return new Point(X3, Y3, Z3, T3);
} }
// Fast algo for adding 2 Extended Points. // Fast algo for adding 2 Extended Points.
// https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#addition-add-2008-hwcd // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#addition-add-2008-hwcd
// Cost: 9M + 1*a + 1*d + 7add. // Cost: 9M + 1*a + 1*d + 7add.
add(other: ExtendedPoint) { add(other: Point) {
assertExtPoint(other); isPoint(other);
const { a, d } = CURVE; const { a, d } = CURVE;
const { x: X1, y: Y1, z: Z1, t: T1 } = this; const { ex: X1, ey: Y1, ez: Z1, et: T1 } = this;
const { x: X2, y: Y2, z: Z2, t: T2 } = other; const { ex: X2, ey: Y2, ez: Z2, et: T2 } = other;
// Faster algo for adding 2 Extended Points when curve's a=-1. // Faster algo for adding 2 Extended Points when curve's a=-1.
// http://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-4 // http://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-4
// Cost: 8M + 8add + 2*2. // Cost: 8M + 8add + 2*2.
@@ -264,7 +264,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
const Y3 = modP(G * H); const Y3 = modP(G * H);
const T3 = modP(E * H); const T3 = modP(E * H);
const Z3 = modP(F * G); const Z3 = modP(F * G);
return new ExtendedPoint(X3, Y3, Z3, T3); return new Point(X3, Y3, Z3, T3);
} }
const A = modP(X1 * X2); // A = X1*X2 const A = modP(X1 * X2); // A = X1*X2
const B = modP(Y1 * Y2); // B = Y1*Y2 const B = modP(Y1 * Y2); // B = Y1*Y2
@@ -279,44 +279,32 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
const T3 = modP(E * H); // T3 = E*H const T3 = modP(E * H); // T3 = E*H
const Z3 = modP(F * G); // Z3 = F*G const Z3 = modP(F * G); // Z3 = F*G
return new ExtendedPoint(X3, Y3, Z3, T3); return new Point(X3, Y3, Z3, T3);
} }
subtract(other: ExtendedPoint): ExtendedPoint { subtract(other: Point): Point {
return this.add(other.negate()); return this.add(other.negate());
} }
private wNAF(n: bigint, affinePoint?: Point): ExtendedPoint { private wNAF(n: bigint): { p: Point; f: Point } {
if (!affinePoint && this.equals(ExtendedPoint.BASE)) affinePoint = Point.BASE; return wnaf.wNAFCached(this, pointPrecomputes, n, Point.normalizeZ);
const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1;
let precomputes = affinePoint && pointPrecomputes.get(affinePoint);
if (!precomputes) {
precomputes = wnaf.precomputeWindow(this, W) as ExtendedPoint[];
if (affinePoint && W !== 1) {
precomputes = ExtendedPoint.normalizeZ(precomputes);
pointPrecomputes.set(affinePoint, precomputes);
}
}
const { p, f } = wnaf.wNAF(W, precomputes, n);
return ExtendedPoint.normalizeZ([p, f])[0];
} }
// Constant time multiplication. // Constant-time multiplication.
// Uses wNAF method. Windowed method may be 10% faster, multiply(scalar: bigint): Point {
// but takes 2x longer to generate and consumes 2x memory. const { p, f } = this.wNAF(assertInRange(scalar, CURVE_ORDER));
multiply(scalar: number | bigint, affinePoint?: Point): ExtendedPoint { return Point.normalizeZ([p, f])[0];
return this.wNAF(normalizeScalar(scalar, CURVE_ORDER), affinePoint);
} }
// Non-constant-time multiplication. Uses double-and-add algorithm. // Non-constant-time multiplication. Uses double-and-add algorithm.
// It's faster, but should only be used when you don't care about // It's faster, but should only be used when you don't care about
// an exposed private key e.g. sig verification. // an exposed private key e.g. sig verification.
multiplyUnsafe(scalar: number | bigint): ExtendedPoint { // Does NOT allow scalars higher than CURVE.n.
let n = normalizeScalar(scalar, CURVE_ORDER, false); multiplyUnsafe(scalar: bigint): Point {
const P0 = ExtendedPoint.ZERO; let n = assertGE0(scalar); // 0 <= scalar < CURVE.n
if (n === _0n) return P0; if (n === _0n) return I;
if (this.equals(P0) || n === _1n) return this; if (this.equals(I) || n === _1n) return this;
if (this.equals(ExtendedPoint.BASE)) return this.wNAF(n); if (this.equals(G)) return this.wNAF(n).p;
return wnaf.unsafeLadder(this, n); return wnaf.unsafeLadder(this, n);
} }
@@ -325,350 +313,149 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
// point with torsion component. // point with torsion component.
// Multiplies point by cofactor and checks if the result is 0. // Multiplies point by cofactor and checks if the result is 0.
isSmallOrder(): boolean { isSmallOrder(): boolean {
return this.multiplyUnsafe(CURVE.h).equals(ExtendedPoint.ZERO); return this.multiplyUnsafe(cofactor).is0();
} }
// Multiplies point by curve order (very big scalar CURVE.n) and checks if the result is 0. // Multiplies point by curve order and checks if the result is 0.
// Returns `false` is the point is dirty. // Returns `false` is the point is dirty.
isTorsionFree(): boolean { isTorsionFree(): boolean {
return wnaf.unsafeLadder(this, CURVE_ORDER).equals(ExtendedPoint.ZERO); return wnaf.unsafeLadder(this, CURVE_ORDER).is0();
} }
// Converts Extended point to default (x, y) coordinates. // Converts Extended point to default (x, y) coordinates.
// Can accept precomputed Z^-1 - for example, from invertBatch. // Can accept precomputed Z^-1 - for example, from invertBatch.
toAffine(invZ?: bigint): Point { toAffine(iz?: bigint): AffinePoint<bigint> {
const { x, y, z } = this; const { ex: x, ey: y, ez: z } = this;
const is0 = this.equals(ExtendedPoint.ZERO); const is0 = this.is0();
if (invZ == null) invZ = is0 ? _8n : (Fp.invert(z) as bigint); // 8 was chosen arbitrarily if (iz == null) iz = is0 ? _8n : (Fp.inv(z) as bigint); // 8 was chosen arbitrarily
const ax = modP(x * invZ); const ax = modP(x * iz);
const ay = modP(y * invZ); const ay = modP(y * iz);
const zz = modP(z * invZ); const zz = modP(z * iz);
if (is0) return Point.ZERO; if (is0) return { x: _0n, y: _1n };
if (zz !== _1n) throw new Error('invZ was invalid'); if (zz !== _1n) throw new Error('invZ was invalid');
return new Point(ax, ay); return { x: ax, y: ay };
} }
clearCofactor(): ExtendedPoint {
clearCofactor(): Point {
const { h: cofactor } = CURVE; const { h: cofactor } = CURVE;
if (cofactor === _1n) return this; if (cofactor === _1n) return this;
return this.multiplyUnsafe(cofactor); return this.multiplyUnsafe(cofactor);
} }
}
const wnaf = wNAF(ExtendedPoint, CURVE.nByteLength * 8);
function assertExtPoint(other: unknown) {
if (!(other instanceof ExtendedPoint)) throw new TypeError('ExtendedPoint expected');
}
// Stores precomputed values for points.
const pointPrecomputes = new WeakMap<Point, ExtendedPoint[]>();
/**
* Default Point works in affine coordinates: (x, y)
*/
class Point implements PointType {
// Base point aka generator
// public_key = Point.BASE * private_key
static BASE: Point = new Point(CURVE.Gx, CURVE.Gy);
// Identity point aka point at infinity
// point = point + zero_point
static ZERO: Point = new Point(_0n, _1n);
// We calculate precomputes for elliptic curve point multiplication
// using windowed method. This specifies window size and
// stores precomputed values. Usually only base point would be precomputed.
_WINDOW_SIZE?: number;
constructor(readonly x: bigint, readonly y: bigint) {}
// "Private method", don't use it directly.
_setWindowSize(windowSize: number) {
this._WINDOW_SIZE = windowSize;
pointPrecomputes.delete(this);
}
// Converts hash string or Uint8Array to Point. // Converts hash string or Uint8Array to Point.
// Uses algo from RFC8032 5.1.3. // Uses algo from RFC8032 5.1.3.
static fromHex(hex: Hex, strict = true) { static fromHex(hex: Hex, strict = true): Point {
const { d, a } = CURVE; const { d, a } = CURVE;
const len = Fp.BYTES; const len = Fp.BYTES;
hex = ensureBytes(hex, len); hex = ensureBytes('pointHex', hex, len); // copy hex to a new array
// 1. First, interpret the string as an integer in little-endian const normed = hex.slice(); // copy again, we'll manipulate it
// representation. Bit 255 of this number is the least significant const lastByte = hex[len - 1]; // select last byte
// bit of the x-coordinate and denote this value x_0. The normed[len - 1] = lastByte & ~0x80; // clear last bit
// y-coordinate is recovered simply by clearing this bit. If the
// resulting value is >= p, decoding fails.
const normed = hex.slice();
const lastByte = hex[len - 1];
normed[len - 1] = lastByte & ~0x80;
const y = ut.bytesToNumberLE(normed); const y = ut.bytesToNumberLE(normed);
if (y === _0n) {
if (strict && y >= Fp.ORDER) throw new Error('Expected 0 < hex < P'); // y=0 is allowed
if (!strict && y >= maxGroupElement) throw new Error('Expected 0 < hex < CURVE.n');
// 2. To recover the x-coordinate, the curve equation implies
// Ed25519: x² = (y² - 1) / (d y² + 1) (mod p).
// Ed448: x² = (y² - 1) / (d y² - 1) (mod p).
// For generic case:
// a*x²+y²=1+d*x²*y²
// -> y²-1 = d*x²*y²-a*x²
// -> y²-1 = x² (d*y²-a)
// -> x² = (y²-1) / (d*y²-a)
// The denominator is always non-zero mod p. Let u = y² - 1 and v = d y² + 1.
const y2 = modP(y * y);
const u = modP(y2 - _1n);
const v = modP(d * y2 - a);
let { isValid, value: x } = uvRatio(u, v);
if (!isValid) throw new Error('Point.fromHex: invalid y coordinate');
// 4. Finally, use the x_0 bit to select the right square root. If
// x = 0, and x_0 = 1, decoding fails. Otherwise, if x_0 != x mod
// 2, set x <-- p - x. Return the decoded point (x,y).
const isXOdd = (x & _1n) === _1n;
const isLastByteOdd = (lastByte & 0x80) !== 0;
if (isLastByteOdd !== isXOdd) x = modP(-x);
return new Point(x, y);
}
static fromPrivateKey(privateKey: PrivKey) {
return getExtendedPublicKey(privateKey).point;
}
// There can always be only two x values (x, -x) for any y
// When compressing point, it's enough to only store its y coordinate
// and use the last byte to encode sign of x.
toRawBytes(): Uint8Array {
const bytes = ut.numberToBytesLE(this.y, Fp.BYTES);
bytes[Fp.BYTES - 1] |= this.x & _1n ? 0x80 : 0;
return bytes;
}
// Same as toRawBytes, but returns string.
toHex(): string {
return ut.bytesToHex(this.toRawBytes());
}
// Determines if point is in prime-order subgroup.
// Returns `false` is the point is dirty.
isTorsionFree(): boolean {
return ExtendedPoint.fromAffine(this).isTorsionFree();
}
equals(other: Point): boolean {
if (!(other instanceof Point)) throw new TypeError('Point#equals: expected Point');
return this.x === other.x && this.y === other.y;
}
negate(): Point {
return new Point(modP(-this.x), this.y);
}
double(): Point {
return ExtendedPoint.fromAffine(this).double().toAffine();
}
add(other: Point) {
return ExtendedPoint.fromAffine(this).add(ExtendedPoint.fromAffine(other)).toAffine();
}
subtract(other: Point) {
return this.add(other.negate());
}
/**
* Constant time multiplication.
* @param scalar Big-Endian number
* @returns new point
*/
multiply(scalar: number | bigint): Point {
return ExtendedPoint.fromAffine(this).multiply(scalar, this).toAffine();
}
clearCofactor() {
return ExtendedPoint.fromAffine(this).clearCofactor().toAffine();
}
// Encodes byte string to elliptic curve
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3
static hashToCurve(msg: Hex, options?: Partial<htfOpts>) {
const { mapToCurve, htfDefaults } = CURVE;
if (!mapToCurve) throw new Error('No mapToCurve defined for curve');
const u = hashToField(ensureBytes(msg), 2, { ...htfDefaults, ...options } as htfOpts);
const { x: x0, y: y0 } = mapToCurve(u[0]);
const { x: x1, y: y1 } = mapToCurve(u[1]);
const p = new Point(x0, y0).add(new Point(x1, y1)).clearCofactor();
return p;
}
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3
static encodeToCurve(msg: Hex, options?: Partial<htfOpts>) {
const { mapToCurve, htfDefaults } = CURVE;
if (!mapToCurve) throw new Error('No mapToCurve defined for curve');
const u = hashToField(ensureBytes(msg), 1, { ...htfDefaults, ...options } as htfOpts);
const { x, y } = mapToCurve(u[0]);
return new Point(x, y).clearCofactor();
}
}
/**
* EDDSA signature.
*/
class Signature implements SignatureType {
constructor(readonly r: Point, readonly s: bigint) {
this.assertValidity();
}
static fromHex(hex: Hex) {
const len = Fp.BYTES;
const bytes = ensureBytes(hex, 2 * len);
const r = Point.fromHex(bytes.slice(0, len), false);
const s = ut.bytesToNumberLE(bytes.slice(len, 2 * len));
return new Signature(r, s);
}
assertValidity() {
const { r, s } = this;
if (!(r instanceof Point)) throw new Error('Expected Point instance');
// 0 <= s < l
normalizeScalar(s, CURVE_ORDER, false);
return this;
}
toRawBytes() {
return ut.concatBytes(this.r.toRawBytes(), ut.numberToBytesLE(this.s, Fp.BYTES));
}
toHex() {
return ut.bytesToHex(this.toRawBytes());
}
}
// Little-endian SHA512 with modulo n
function modnLE(hash: Uint8Array): bigint {
return mod.mod(ut.bytesToNumberLE(hash), CURVE_ORDER);
}
/**
* Checks for num to be in range:
* For strict == true: `0 < num < max`.
* For strict == false: `0 <= num < max`.
* Converts non-float safe numbers to bigints.
*/
function normalizeScalar(num: number | bigint, max: bigint, strict = true): bigint {
if (!max) throw new TypeError('Specify max value');
if (ut.isPositiveInt(num)) num = BigInt(num);
if (typeof num === 'bigint' && num < max) {
if (strict) {
if (_0n < num) return num;
} else { } else {
if (_0n <= num) return num; // RFC8032 prohibits >= p, but ZIP215 doesn't
if (strict) assertInRange(y, Fp.ORDER); // strict=true [1..P-1] (2^255-19-1 for ed25519)
else assertInRange(y, MASK); // strict=false [1..MASK-1] (2^256-1 for ed25519)
}
// Ed25519: x² = (y²-1)/(dy²+1) mod p. Ed448: x² = (y²-1)/(dy²-1) mod p. Generic case:
// ax²+y²=1+dx²y² => y²-1=dx²y²-ax² => y²-1=x²(dy²-a) => x²=(y²-1)/(dy²-a)
const y2 = modP(y * y); // denominator is always non-0 mod p.
const u = modP(y2 - _1n); // u = y² - 1
const v = modP(d * y2 - a); // v = d y² + 1.
let { isValid, value: x } = uvRatio(u, v); // √(u/v)
if (!isValid) throw new Error('Point.fromHex: invalid y coordinate');
const isXOdd = (x & _1n) === _1n; // There are 2 square roots. Use x_0 bit to select proper
const isLastByteOdd = (lastByte & 0x80) !== 0; // if x=0 and x_0 = 1, fail
if (isLastByteOdd !== isXOdd) x = modP(-x); // if x_0 != x mod 2, set x = p-x
return Point.fromAffine({ x, y });
}
static fromPrivateKey(privKey: Hex) {
return getExtendedPublicKey(privKey).point;
}
toRawBytes(): Uint8Array {
const { x, y } = this.toAffine();
const bytes = ut.numberToBytesLE(y, Fp.BYTES); // each y has 2 x values (x, -y)
bytes[bytes.length - 1] |= x & _1n ? 0x80 : 0; // when compressing, it's enough to store y
return bytes; // and use the last byte to encode sign of x
}
toHex(): string {
return ut.bytesToHex(this.toRawBytes()); // Same as toRawBytes, but returns string.
} }
} }
throw new TypeError(`Expected valid scalar: 0 < scalar < ${max}`); const { BASE: G, ZERO: I } = Point;
const wnaf = wNAF(Point, nByteLength * 8);
function modN(a: bigint) {
return mod(a, CURVE_ORDER);
}
// Little-endian SHA512 with modulo n
function modN_LE(hash: Uint8Array): bigint {
return modN(ut.bytesToNumberLE(hash));
} }
/** Convenience method that creates public key and other stuff. RFC8032 5.1.5 */ /** Convenience method that creates public key and other stuff. RFC8032 5.1.5 */
function getExtendedPublicKey(key: PrivKey) { function getExtendedPublicKey(key: Hex) {
const groupLen = CURVE.nByteLength; const len = nByteLength;
// Normalize bigint / number / string to Uint8Array key = ensureBytes('private key', key, len);
const keyb =
typeof key === 'bigint' || typeof key === 'number'
? ut.numberToBytesLE(normalizeScalar(key, maxGroupElement), groupLen)
: key;
// Hash private key with curve's hash function to produce uniformingly random input // Hash private key with curve's hash function to produce uniformingly random input
// We check byte lengths e.g.: ensureBytes(64, hash(ensureBytes(32, key))) // Check byte lengths: ensure(64, h(ensure(32, key)))
const hashed = ensureBytes(CURVE.hash(ensureBytes(keyb, groupLen)), 2 * groupLen); const hashed = ensureBytes('hashed private key', cHash(key), 2 * len);
const head = adjustScalarBytes(hashed.slice(0, len)); // clear first half bits, produce FE
// First half's bits are cleared to produce a random field element. const prefix = hashed.slice(len, 2 * len); // second half is called key prefix (5.1.6)
const head = adjustScalarBytes(hashed.slice(0, groupLen)); const scalar = modN_LE(head); // The actual private scalar
// Second half is called key prefix (5.1.6) const point = G.multiply(scalar); // Point on Edwards curve aka public key
const prefix = hashed.slice(groupLen, 2 * groupLen); const pointBytes = point.toRawBytes(); // Uint8Array representation
// The actual private scalar
const scalar = modnLE(head);
// Point on Edwards curve aka public key
const point = Point.BASE.multiply(scalar);
// Uint8Array representation
const pointBytes = point.toRawBytes();
return { head, prefix, scalar, point, pointBytes }; return { head, prefix, scalar, point, pointBytes };
} }
/** // Calculates EdDSA pub key. RFC8032 5.1.5. Privkey is hashed. Use first half with 3 bits cleared
* Calculates ed25519 public key. RFC8032 5.1.5 function getPublicKey(privKey: Hex): Uint8Array {
* 1. private key is hashed with sha512, then first 32 bytes are taken from the hash return getExtendedPublicKey(privKey).pointBytes;
* 2. 3 least significant bits of the first byte are cleared
*/
function getPublicKey(privateKey: PrivKey): Uint8Array {
return getExtendedPublicKey(privateKey).pointBytes;
} }
const EMPTY = new Uint8Array(); // int('LE', SHA512(dom2(F, C) || msgs)) mod N
function hashDomainToScalar(message: Uint8Array, context: Hex = EMPTY) { function hashDomainToScalar(context: Hex = new Uint8Array(), ...msgs: Uint8Array[]) {
context = ensureBytes(context); const msg = ut.concatBytes(...msgs);
return modnLE(CURVE.hash(domain(message, context, !!CURVE.preHash))); return modN_LE(cHash(domain(msg, ensureBytes('context', context), !!preHash)));
} }
/** Signs message with privateKey. RFC8032 5.1.6 */ /** Signs message with privateKey. RFC8032 5.1.6 */
function sign(message: Hex, privateKey: Hex, context?: Hex): Uint8Array { function sign(msg: Hex, privKey: Hex, context?: Hex): Uint8Array {
message = ensureBytes(message); msg = ensureBytes('message', msg);
if (CURVE.preHash) message = CURVE.preHash(message); if (preHash) msg = preHash(msg); // for ed25519ph etc.
const { prefix, scalar, pointBytes } = getExtendedPublicKey(privateKey); const { prefix, scalar, pointBytes } = getExtendedPublicKey(privKey);
const r = hashDomainToScalar(ut.concatBytes(prefix, message), context); const r = hashDomainToScalar(context, prefix, msg); // r = dom2(F, C) || prefix || PH(M)
const R = Point.BASE.multiply(r); // R = rG const R = G.multiply(r).toRawBytes(); // R = rG
const k = hashDomainToScalar(ut.concatBytes(R.toRawBytes(), pointBytes, message), context); // k = hash(R+P+msg) const k = hashDomainToScalar(context, R, pointBytes, msg); // R || A || PH(M)
const s = mod.mod(r + k * scalar, CURVE_ORDER); // s = r + kp const s = modN(r + k * scalar); // S = (r + k * s) mod L
return new Signature(R, s).toRawBytes(); assertGE0(s); // 0 <= s < l
const res = ut.concatBytes(R, ut.numberToBytesLE(s, Fp.BYTES));
return ensureBytes('result', res, nByteLength * 2); // 64-byte signature
} }
/** function verify(sig: Hex, msg: Hex, publicKey: Hex, context?: Hex): boolean {
* Verifies EdDSA signature against message and public key. const len = Fp.BYTES; // Verifies EdDSA signature against message and public key. RFC8032 5.1.7.
* An extended group equation is checked. sig = ensureBytes('signature', sig, 2 * len); // An extended group equation is checked.
* RFC8032 5.1.7 msg = ensureBytes('message', msg); // ZIP215 compliant, which means not fully RFC8032 compliant.
* Compliant with ZIP215: if (preHash) msg = preHash(msg); // for ed25519ph, etc
* 0 <= sig.R/publicKey < 2**256 (can be >= curve.P) const A = Point.fromHex(publicKey, false); // Check for s bounds, hex validity
* 0 <= sig.s < l const R = Point.fromHex(sig.slice(0, len), false); // 0 <= R < 2^256: ZIP215 R can be >= P
* Not compliant with RFC8032: it's not possible to comply to both ZIP & RFC at the same time. const s = ut.bytesToNumberLE(sig.slice(len, 2 * len));
*/ const SB = G.multiplyUnsafe(s); // 0 <= s < l is done inside
function verify(sig: SigType, message: Hex, publicKey: PubKey, context?: Hex): boolean { const k = hashDomainToScalar(context, R.toRawBytes(), A.toRawBytes(), msg);
message = ensureBytes(message); const RkA = R.add(A.multiplyUnsafe(k));
if (CURVE.preHash) message = CURVE.preHash(message);
// When hex is passed, we check public key fully.
// When Point instance is passed, we assume it has already been checked, for performance.
// If user passes Point/Sig instance, we assume it has been already verified.
// We don't check its equations for performance. We do check for valid bounds for s though
// We always check for: a) s bounds. b) hex validity
if (publicKey instanceof Point) {
// ignore
} else if (publicKey instanceof Uint8Array || typeof publicKey === 'string') {
publicKey = Point.fromHex(publicKey, false);
} else {
throw new Error(`Invalid publicKey: ${publicKey}`);
}
if (sig instanceof Signature) sig.assertValidity();
else if (sig instanceof Uint8Array || typeof sig === 'string') sig = Signature.fromHex(sig);
else throw new Error(`Wrong signature: ${sig}`);
const { r, s } = sig;
const SB = ExtendedPoint.BASE.multiplyUnsafe(s);
const k = hashDomainToScalar(
ut.concatBytes(r.toRawBytes(), publicKey.toRawBytes(), message),
context
);
const kA = ExtendedPoint.fromAffine(publicKey).multiplyUnsafe(k);
const RkA = ExtendedPoint.fromAffine(r).add(kA);
// [8][S]B = [8]R + [8][k]A' // [8][S]B = [8]R + [8][k]A'
return RkA.subtract(SB).clearCofactor().equals(ExtendedPoint.ZERO); return RkA.subtract(SB).clearCofactor().equals(Point.ZERO);
} }
// Enable precomputes. Slows down first publicKey computation by 20ms. G._setWindowSize(8); // Enable precomputes. Slows down first publicKey computation by 20ms.
Point.BASE._setWindowSize(8);
const utils = { const utils = {
getExtendedPublicKey, getExtendedPublicKey,
/** // ed25519 private keys are uniform 32b. No need to check for modulo bias, like in secp256k1.
* Not needed for ed25519 private keys. Needed if you use scalars directly (rare).
*/
hashToPrivateScalar: (hash: Hex): bigint => ut.hashToPrivateScalar(hash, CURVE_ORDER, true),
/**
* ed25519 private keys are uniform 32-bit strings. We do not need to check for
* modulo bias like we do in secp256k1 randomPrivateKey()
*/
randomPrivateKey: (): Uint8Array => randomBytes(Fp.BYTES), randomPrivateKey: (): Uint8Array => randomBytes(Fp.BYTES),
/** /**
@@ -677,11 +464,10 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
* but allows to speed-up subsequent getPublicKey() calls up to 20x. * but allows to speed-up subsequent getPublicKey() calls up to 20x.
* @param windowSize 2, 4, 8, 16 * @param windowSize 2, 4, 8, 16
*/ */
precompute(windowSize = 8, point = Point.BASE): Point { precompute(windowSize = 8, point = Point.BASE): typeof Point.BASE {
const cached = point.equals(Point.BASE) ? point : new Point(point.x, point.y); point._setWindowSize(windowSize);
cached._setWindowSize(windowSize); point.multiply(BigInt(3));
cached.multiply(_2n); return point;
return cached;
}, },
}; };
@@ -690,9 +476,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
getPublicKey, getPublicKey,
sign, sign,
verify, verify,
ExtendedPoint, ExtendedPoint: Point,
Point,
Signature,
utils, utils,
}; };
} }

View File

@@ -1,60 +1,36 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { CHash, concatBytes } from './utils.js'; import type { Group, GroupConstructor, AffinePoint } from './curve.js';
import * as mod from './modular.js'; import { mod, IField } from './modular.js';
import { bytesToNumberBE, CHash, concatBytes, utf8ToBytes, validateObject } from './utils.js';
export type htfOpts = { /**
// DST: a domain separation tag * * `DST` is a domain separation tag, defined in section 2.2.5
// defined in section 2.2.5 * * `p` characteristic of F, where F is a finite field of characteristic p and order q = p^m
DST: string; * * `m` is extension degree (1 for prime fields)
// p: the characteristic of F * * `k` is the target security target in bits (e.g. 128), from section 5.1
// where F is a finite field of characteristic p and order q = p^m * * `expand` is `xmd` (SHA2, SHA3, BLAKE) or `xof` (SHAKE, BLAKE-XOF)
* * `hash` conforming to `utils.CHash` interface, with `outputLen` / `blockLen` props
*/
type UnicodeOrBytes = string | Uint8Array;
export type Opts = {
DST: UnicodeOrBytes;
p: bigint; p: bigint;
// m: the extension degree of F, m >= 1
// where F is a finite field of characteristic p and order q = p^m
m: number; m: number;
// k: the target security level for the suite in bits
// defined in section 5.1
k: number; k: number;
// option to use a message that has already been processed by
// expand_message_xmd
expand?: 'xmd' | 'xof'; expand?: 'xmd' | 'xof';
// Hash functions for: expand_message_xmd is appropriate for use with a
// wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others.
// BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247
// TODO: verify that hash is shake if expand==='xof' via types
hash: CHash; hash: CHash;
}; };
export function validateHTFOpts(opts: htfOpts) { function validateDST(dst: UnicodeOrBytes): Uint8Array {
if (typeof opts.DST !== 'string') throw new Error('Invalid htf/DST'); if (dst instanceof Uint8Array) return dst;
if (typeof opts.p !== 'bigint') throw new Error('Invalid htf/p'); if (typeof dst === 'string') return utf8ToBytes(dst);
if (typeof opts.m !== 'number') throw new Error('Invalid htf/m'); throw new Error('DST must be Uint8Array or string');
if (typeof opts.k !== 'number') throw new Error('Invalid htf/k');
if (opts.expand !== 'xmd' && opts.expand !== 'xof' && opts.expand !== undefined)
throw new Error('Invalid htf/expand');
if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen))
throw new Error('Invalid htf/hash function');
} }
// UTF8 to ui8a // Octet Stream to Integer. "spec" implementation of os2ip is 2.5x slower vs bytesToNumberBE.
// TODO: looks broken, ASCII only, why not TextEncoder/TextDecoder? it is in hashes anyway const os2ip = bytesToNumberBE;
export function stringToBytes(str: string) {
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) bytes[i] = str.charCodeAt(i);
return bytes;
}
// Octet Stream to Integer (bytesToNumberBE) // Integer to Octet Stream (numberToBytesBE)
function os2ip(bytes: Uint8Array): bigint {
let result = 0n;
for (let i = 0; i < bytes.length; i++) {
result <<= 8n;
result += BigInt(bytes[i]);
}
return result;
}
// Integer to Octet Stream
function i2osp(value: number, length: number): Uint8Array { function i2osp(value: number, length: number): Uint8Array {
if (value < 0 || value >= 1 << (8 * length)) { if (value < 0 || value >= 1 << (8 * length)) {
throw new Error(`bad I2OSP call: value=${value} length=${length}`); throw new Error(`bad I2OSP call: value=${value} length=${length}`);
@@ -75,6 +51,13 @@ function strxor(a: Uint8Array, b: Uint8Array): Uint8Array {
return arr; return arr;
} }
function isBytes(item: unknown): void {
if (!(item instanceof Uint8Array)) throw new Error('Uint8Array expected');
}
function isNum(item: unknown): void {
if (!Number.isSafeInteger(item)) throw new Error('number expected');
}
// Produces a uniformly random byte string using a cryptographic hash function H that outputs b bits // Produces a uniformly random byte string using a cryptographic hash function H that outputs b bits
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.4.1 // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.4.1
export function expand_message_xmd( export function expand_message_xmd(
@@ -83,15 +66,17 @@ export function expand_message_xmd(
lenInBytes: number, lenInBytes: number,
H: CHash H: CHash
): Uint8Array { ): Uint8Array {
isBytes(msg);
isBytes(DST);
isNum(lenInBytes);
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3 // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3
if (DST.length > 255) DST = H(concatBytes(stringToBytes('H2C-OVERSIZE-DST-'), DST)); if (DST.length > 255) DST = H(concatBytes(utf8ToBytes('H2C-OVERSIZE-DST-'), DST));
const b_in_bytes = H.outputLen; const { outputLen: b_in_bytes, blockLen: r_in_bytes } = H;
const r_in_bytes = H.blockLen;
const ell = Math.ceil(lenInBytes / b_in_bytes); const ell = Math.ceil(lenInBytes / b_in_bytes);
if (ell > 255) throw new Error('Invalid xmd length'); if (ell > 255) throw new Error('Invalid xmd length');
const DST_prime = concatBytes(DST, i2osp(DST.length, 1)); const DST_prime = concatBytes(DST, i2osp(DST.length, 1));
const Z_pad = i2osp(0, r_in_bytes); const Z_pad = i2osp(0, r_in_bytes);
const l_i_b_str = i2osp(lenInBytes, 2); const l_i_b_str = i2osp(lenInBytes, 2); // len_in_bytes_str
const b = new Array<Uint8Array>(ell); const b = new Array<Uint8Array>(ell);
const b_0 = H(concatBytes(Z_pad, msg, l_i_b_str, i2osp(0, 1), DST_prime)); const b_0 = H(concatBytes(Z_pad, msg, l_i_b_str, i2osp(0, 1), DST_prime));
b[0] = H(concatBytes(b_0, i2osp(1, 1), DST_prime)); b[0] = H(concatBytes(b_0, i2osp(1, 1), DST_prime));
@@ -110,11 +95,14 @@ export function expand_message_xof(
k: number, k: number,
H: CHash H: CHash
): Uint8Array { ): Uint8Array {
isBytes(msg);
isBytes(DST);
isNum(lenInBytes);
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3 // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3
// DST = H('H2C-OVERSIZE-DST-' || a_very_long_DST, Math.ceil((lenInBytes * k) / 8)); // DST = H('H2C-OVERSIZE-DST-' || a_very_long_DST, Math.ceil((lenInBytes * k) / 8));
if (DST.length > 255) { if (DST.length > 255) {
const dkLen = Math.ceil((2 * k) / 8); const dkLen = Math.ceil((2 * k) / 8);
DST = H.create({ dkLen }).update(stringToBytes('H2C-OVERSIZE-DST-')).update(DST).digest(); DST = H.create({ dkLen }).update(utf8ToBytes('H2C-OVERSIZE-DST-')).update(DST).digest();
} }
if (lenInBytes > 65535 || DST.length > 255) if (lenInBytes > 65535 || DST.length > 255)
throw new Error('expand_message_xof: invalid lenInBytes'); throw new Error('expand_message_xof: invalid lenInBytes');
@@ -134,36 +122,48 @@ export function expand_message_xof(
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3 * https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3
* @param msg a byte string containing the message to hash * @param msg a byte string containing the message to hash
* @param count the number of elements of F to output * @param count the number of elements of F to output
* @param options `{DST: string, p: bigint, m: number, k: number, expand: 'xmd' | 'xof', hash: H}` * @param options `{DST: string, p: bigint, m: number, k: number, expand: 'xmd' | 'xof', hash: H}`, see above
* @returns [u_0, ..., u_(count - 1)], a list of field elements. * @returns [u_0, ..., u_(count - 1)], a list of field elements.
*/ */
export function hash_to_field(msg: Uint8Array, count: number, options: htfOpts): bigint[][] { export function hash_to_field(msg: Uint8Array, count: number, options: Opts): bigint[][] {
// if options is provided but incomplete, fill any missing fields with the validateObject(options, {
// value in hftDefaults (ie hash to G2). DST: 'string',
const log2p = options.p.toString(2).length; p: 'bigint',
const L = Math.ceil((log2p + options.k) / 8); // section 5.1 of ietf draft link above m: 'isSafeInteger',
const len_in_bytes = count * options.m * L; k: 'isSafeInteger',
const DST = stringToBytes(options.DST); hash: 'hash',
let pseudo_random_bytes = msg; });
if (options.expand === 'xmd') { const { p, k, m, hash, expand, DST: _DST } = options;
pseudo_random_bytes = expand_message_xmd(msg, DST, len_in_bytes, options.hash); isBytes(msg);
} else if (options.expand === 'xof') { isNum(count);
pseudo_random_bytes = expand_message_xof(msg, DST, len_in_bytes, options.k, options.hash); const DST = validateDST(_DST);
const log2p = p.toString(2).length;
const L = Math.ceil((log2p + k) / 8); // section 5.1 of ietf draft link above
const len_in_bytes = count * m * L;
let prb; // pseudo_random_bytes
if (expand === 'xmd') {
prb = expand_message_xmd(msg, DST, len_in_bytes, hash);
} else if (expand === 'xof') {
prb = expand_message_xof(msg, DST, len_in_bytes, k, hash);
} else if (expand === undefined) {
prb = msg;
} else {
throw new Error('expand must be "xmd", "xof" or undefined');
} }
const u = new Array(count); const u = new Array(count);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const e = new Array(options.m); const e = new Array(m);
for (let j = 0; j < options.m; j++) { for (let j = 0; j < m; j++) {
const elm_offset = L * (j + i * options.m); const elm_offset = L * (j + i * m);
const tv = pseudo_random_bytes.subarray(elm_offset, elm_offset + L); const tv = prb.subarray(elm_offset, elm_offset + L);
e[j] = mod.mod(os2ip(tv), options.p); e[j] = mod(os2ip(tv), p);
} }
u[i] = e; u[i] = e;
} }
return u; return u;
} }
export function isogenyMap<T, F extends mod.Field<T>>(field: F, map: [T[], T[], T[], T[]]) { export function isogenyMap<T, F extends IField<T>>(field: F, map: [T[], T[], T[], T[]]) {
// Make same order as in spec // Make same order as in spec
const COEFF = map.map((i) => Array.from(i).reverse()); const COEFF = map.map((i) => Array.from(i).reverse());
return (x: T, y: T) => { return (x: T, y: T) => {
@@ -175,3 +175,48 @@ export function isogenyMap<T, F extends mod.Field<T>>(field: F, map: [T[], T[],
return { x, y }; return { x, y };
}; };
} }
export interface H2CPoint<T> extends Group<H2CPoint<T>> {
add(rhs: H2CPoint<T>): H2CPoint<T>;
toAffine(iz?: bigint): AffinePoint<T>;
clearCofactor(): H2CPoint<T>;
assertValidity(): void;
}
export interface H2CPointConstructor<T> extends GroupConstructor<H2CPoint<T>> {
fromAffine(ap: AffinePoint<T>): H2CPoint<T>;
}
export type MapToCurve<T> = (scalar: bigint[]) => AffinePoint<T>;
// Separated from initialization opts, so users won't accidentally change per-curve parameters
// (changing DST is ok!)
export type htfBasicOpts = { DST: UnicodeOrBytes };
export function createHasher<T>(
Point: H2CPointConstructor<T>,
mapToCurve: MapToCurve<T>,
def: Opts & { encodeDST?: UnicodeOrBytes }
) {
if (typeof mapToCurve !== 'function') throw new Error('mapToCurve() must be defined');
return {
// Encodes byte string to elliptic curve
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3
hashToCurve(msg: Uint8Array, options?: htfBasicOpts) {
const u = hash_to_field(msg, 2, { ...def, DST: def.DST, ...options } as Opts);
const u0 = Point.fromAffine(mapToCurve(u[0]));
const u1 = Point.fromAffine(mapToCurve(u[1]));
const P = u0.add(u1).clearCofactor();
P.assertValidity();
return P;
},
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-3
encodeToCurve(msg: Uint8Array, options?: htfBasicOpts) {
const u = hash_to_field(msg, 1, { ...def, DST: def.encodeDST, ...options } as Opts);
const P = Point.fromAffine(mapToCurve(u[0])).clearCofactor();
P.assertValidity();
return P;
},
};
}

View File

@@ -1,7 +1,14 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// TODO: remove circular imports
import * as utils from './utils.js';
// Utilities for modular arithmetics and finite fields // Utilities for modular arithmetics and finite fields
import {
bitMask,
numberToBytesBE,
numberToBytesLE,
bytesToNumberBE,
bytesToNumberLE,
ensureBytes,
validateObject,
} from './utils.js';
// prettier-ignore // prettier-ignore
const _0n = BigInt(0), _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3); const _0n = BigInt(0), _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3);
// prettier-ignore // prettier-ignore
@@ -34,7 +41,6 @@ export function pow(num: bigint, power: bigint, modulo: bigint): bigint {
} }
// Does x ^ (2 ^ power) mod p. pow2(30, 4) == 30 ^ (2 ^ 4) // Does x ^ (2 ^ power) mod p. pow2(30, 4) == 30 ^ (2 ^ 4)
// TODO: Fp version?
export function pow2(x: bigint, power: bigint, modulo: bigint): bigint { export function pow2(x: bigint, power: bigint, modulo: bigint): bigint {
let res = x; let res = x;
while (power-- > _0n) { while (power-- > _0n) {
@@ -50,6 +56,7 @@ export function invert(number: bigint, modulo: bigint): bigint {
throw new Error(`invert: expected positive integers, got n=${number} mod=${modulo}`); throw new Error(`invert: expected positive integers, got n=${number} mod=${modulo}`);
} }
// Eucledian GCD https://brilliant.org/wiki/extended-euclidean-algorithm/ // Eucledian GCD https://brilliant.org/wiki/extended-euclidean-algorithm/
// Fermat's little theorem "CT-like" version inv(n) = n^(m-2) mod m is 30x slower.
let a = mod(number, modulo); let a = mod(number, modulo);
let b = modulo; let b = modulo;
// prettier-ignore // prettier-ignore
@@ -90,35 +97,35 @@ export function tonelliShanks(P: bigint) {
// Fast-path // Fast-path
if (S === 1) { if (S === 1) {
const p1div4 = (P + _1n) / _4n; const p1div4 = (P + _1n) / _4n;
return function tonelliFast<T>(Fp: Field<T>, n: T) { return function tonelliFast<T>(Fp: IField<T>, n: T) {
const root = Fp.pow(n, p1div4); const root = Fp.pow(n, p1div4);
if (!Fp.equals(Fp.square(root), n)) throw new Error('Cannot find square root'); if (!Fp.eql(Fp.sqr(root), n)) throw new Error('Cannot find square root');
return root; return root;
}; };
} }
// Slow-path // Slow-path
const Q1div2 = (Q + _1n) / _2n; const Q1div2 = (Q + _1n) / _2n;
return function tonelliSlow<T>(Fp: Field<T>, n: T): T { return function tonelliSlow<T>(Fp: IField<T>, n: T): T {
// Step 0: Check that n is indeed a square: (n | p) should not be ≡ -1 // Step 0: Check that n is indeed a square: (n | p) should not be ≡ -1
if (Fp.pow(n, legendreC) === Fp.negate(Fp.ONE)) throw new Error('Cannot find square root'); if (Fp.pow(n, legendreC) === Fp.neg(Fp.ONE)) throw new Error('Cannot find square root');
let r = S; let r = S;
// TODO: will fail at Fp2/etc // TODO: will fail at Fp2/etc
let g = Fp.pow(Fp.mul(Fp.ONE, Z), Q); // will update both x and b let g = Fp.pow(Fp.mul(Fp.ONE, Z), Q); // will update both x and b
let x = Fp.pow(n, Q1div2); // first guess at the square root let x = Fp.pow(n, Q1div2); // first guess at the square root
let b = Fp.pow(n, Q); // first guess at the fudge factor let b = Fp.pow(n, Q); // first guess at the fudge factor
while (!Fp.equals(b, Fp.ONE)) { while (!Fp.eql(b, Fp.ONE)) {
if (Fp.equals(b, Fp.ZERO)) return Fp.ZERO; // https://en.wikipedia.org/wiki/Tonelli%E2%80%93Shanks_algorithm (4. If t = 0, return r = 0) if (Fp.eql(b, Fp.ZERO)) return Fp.ZERO; // https://en.wikipedia.org/wiki/Tonelli%E2%80%93Shanks_algorithm (4. If t = 0, return r = 0)
// Find m such b^(2^m)==1 // Find m such b^(2^m)==1
let m = 1; let m = 1;
for (let t2 = Fp.square(b); m < r; m++) { for (let t2 = Fp.sqr(b); m < r; m++) {
if (Fp.equals(t2, Fp.ONE)) break; if (Fp.eql(t2, Fp.ONE)) break;
t2 = Fp.square(t2); // t2 *= t2 t2 = Fp.sqr(t2); // t2 *= t2
} }
// NOTE: r-m-1 can be bigger than 32, need to convert to bigint before shift, otherwise there will be overflow // NOTE: r-m-1 can be bigger than 32, need to convert to bigint before shift, otherwise there will be overflow
const ge = Fp.pow(g, _1n << BigInt(r - m - 1)); // ge = 2^(r-m-1) const ge = Fp.pow(g, _1n << BigInt(r - m - 1)); // ge = 2^(r-m-1)
g = Fp.square(ge); // g = ge * ge g = Fp.sqr(ge); // g = ge * ge
x = Fp.mul(x, ge); // x *= ge x = Fp.mul(x, ge); // x *= ge
b = Fp.mul(b, g); // b *= g b = Fp.mul(b, g); // b *= g
r = m; r = m;
@@ -139,10 +146,10 @@ export function FpSqrt(P: bigint) {
// 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaabn; // 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaabn;
// const NUM = 72057594037927816n; // const NUM = 72057594037927816n;
const p1div4 = (P + _1n) / _4n; const p1div4 = (P + _1n) / _4n;
return function sqrt3mod4<T>(Fp: Field<T>, n: T) { return function sqrt3mod4<T>(Fp: IField<T>, n: T) {
const root = Fp.pow(n, p1div4); const root = Fp.pow(n, p1div4);
// Throw if root**2 != n // Throw if root**2 != n
if (!Fp.equals(Fp.square(root), n)) throw new Error('Cannot find square root'); if (!Fp.eql(Fp.sqr(root), n)) throw new Error('Cannot find square root');
return root; return root;
}; };
} }
@@ -150,13 +157,13 @@ export function FpSqrt(P: bigint) {
// Atkin algorithm for q ≡ 5 (mod 8), https://eprint.iacr.org/2012/685.pdf (page 10) // Atkin algorithm for q ≡ 5 (mod 8), https://eprint.iacr.org/2012/685.pdf (page 10)
if (P % _8n === _5n) { if (P % _8n === _5n) {
const c1 = (P - _5n) / _8n; const c1 = (P - _5n) / _8n;
return function sqrt5mod8<T>(Fp: Field<T>, n: T) { return function sqrt5mod8<T>(Fp: IField<T>, n: T) {
const n2 = Fp.mul(n, _2n); const n2 = Fp.mul(n, _2n);
const v = Fp.pow(n2, c1); const v = Fp.pow(n2, c1);
const nv = Fp.mul(n, v); const nv = Fp.mul(n, v);
const i = Fp.mul(Fp.mul(nv, _2n), v); const i = Fp.mul(Fp.mul(nv, _2n), v);
const root = Fp.mul(nv, Fp.sub(i, Fp.ONE)); const root = Fp.mul(nv, Fp.sub(i, Fp.ONE));
if (!Fp.equals(Fp.square(root), n)) throw new Error('Cannot find square root'); if (!Fp.eql(Fp.sqr(root), n)) throw new Error('Cannot find square root');
return root; return root;
}; };
} }
@@ -196,7 +203,7 @@ export const isNegativeLE = (num: bigint, modulo: bigint) => (mod(num, modulo) &
// - unreadable mess: addition, multiply, square, squareRoot, inversion, divide, power, equals, subtract // - unreadable mess: addition, multiply, square, squareRoot, inversion, divide, power, equals, subtract
// Field is not always over prime, Fp2 for example has ORDER(q)=p^m // Field is not always over prime, Fp2 for example has ORDER(q)=p^m
export interface Field<T> { export interface IField<T> {
ORDER: bigint; ORDER: bigint;
BYTES: number; BYTES: number;
BITS: number; BITS: number;
@@ -206,13 +213,13 @@ export interface Field<T> {
// 1-arg // 1-arg
create: (num: T) => T; create: (num: T) => T;
isValid: (num: T) => boolean; isValid: (num: T) => boolean;
isZero: (num: T) => boolean; is0: (num: T) => boolean;
negate(num: T): T; neg(num: T): T;
invert(num: T): T; inv(num: T): T;
sqrt(num: T): T; sqrt(num: T): T;
square(num: T): T; sqr(num: T): T;
// 2-args // 2-args
equals(lhs: T, rhs: T): boolean; eql(lhs: T, rhs: T): boolean;
add(lhs: T, rhs: T): T; add(lhs: T, rhs: T): T;
sub(lhs: T, rhs: T): T; sub(lhs: T, rhs: T): T;
mul(lhs: T, rhs: T | bigint): T; mul(lhs: T, rhs: T | bigint): T;
@@ -222,13 +229,13 @@ export interface Field<T> {
addN(lhs: T, rhs: T): T; addN(lhs: T, rhs: T): T;
subN(lhs: T, rhs: T): T; subN(lhs: T, rhs: T): T;
mulN(lhs: T, rhs: T | bigint): T; mulN(lhs: T, rhs: T | bigint): T;
squareN(num: T): T; sqrN(num: T): T;
// Optional // Optional
// Should be same as sgn0 function in https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/ // Should be same as sgn0 function in https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/
// NOTE: sgn0 is 'negative in LE', which is same as odd. And negative in LE is kinda strange definition anyway. // NOTE: sgn0 is 'negative in LE', which is same as odd. And negative in LE is kinda strange definition anyway.
isOdd?(num: T): boolean; // Odd instead of even since we have it for Fp2 isOdd?(num: T): boolean; // Odd instead of even since we have it for Fp2
legendre?(num: T): T; // legendre?(num: T): T;
pow(lhs: T, power: bigint): T; pow(lhs: T, power: bigint): T;
invertBatch: (lst: T[]) => T[]; invertBatch: (lst: T[]) => T[];
toBytes(num: T): Uint8Array; toBytes(num: T): Uint8Array;
@@ -238,27 +245,26 @@ export interface Field<T> {
} }
// prettier-ignore // prettier-ignore
const FIELD_FIELDS = [ const FIELD_FIELDS = [
'create', 'isValid', 'isZero', 'negate', 'invert', 'sqrt', 'square', 'create', 'isValid', 'is0', 'neg', 'inv', 'sqrt', 'sqr',
'equals', 'add', 'sub', 'mul', 'pow', 'div', 'eql', 'add', 'sub', 'mul', 'pow', 'div',
'addN', 'subN', 'mulN', 'squareN' 'addN', 'subN', 'mulN', 'sqrN'
] as const; ] as const;
export function validateField<T>(field: Field<T>) { export function validateField<T>(field: IField<T>) {
for (const i of ['ORDER', 'MASK'] as const) { const initial = {
if (typeof field[i] !== 'bigint') ORDER: 'bigint',
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`); MASK: 'bigint',
} BYTES: 'isSafeInteger',
for (const i of ['BYTES', 'BITS'] as const) { BITS: 'isSafeInteger',
if (typeof field[i] !== 'number') } as Record<string, string>;
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`); const opts = FIELD_FIELDS.reduce((map, val: string) => {
} map[val] = 'function';
for (const i of FIELD_FIELDS) { return map;
if (typeof field[i] !== 'function') }, initial);
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`); return validateObject(field, opts);
}
} }
// Generic field functions // Generic field functions
export function FpPow<T>(f: Field<T>, num: T, power: bigint): T { export function FpPow<T>(f: IField<T>, num: T, power: bigint): T {
// Should have same speed as pow for bigints // Should have same speed as pow for bigints
// TODO: benchmark! // TODO: benchmark!
if (power < _0n) throw new Error('Expected power > 0'); if (power < _0n) throw new Error('Expected power > 0');
@@ -268,78 +274,95 @@ export function FpPow<T>(f: Field<T>, num: T, power: bigint): T {
let d = num; let d = num;
while (power > _0n) { while (power > _0n) {
if (power & _1n) p = f.mul(p, d); if (power & _1n) p = f.mul(p, d);
d = f.square(d); d = f.sqr(d);
power >>= 1n; power >>= _1n;
} }
return p; return p;
} }
export function FpInvertBatch<T>(f: Field<T>, nums: T[]): T[] { // 0 is non-invertible: non-batched version will throw on 0
export function FpInvertBatch<T>(f: IField<T>, nums: T[]): T[] {
const tmp = new Array(nums.length); const tmp = new Array(nums.length);
// Walk from first to last, multiply them by each other MOD p // Walk from first to last, multiply them by each other MOD p
const lastMultiplied = nums.reduce((acc, num, i) => { const lastMultiplied = nums.reduce((acc, num, i) => {
if (f.isZero(num)) return acc; if (f.is0(num)) return acc;
tmp[i] = acc; tmp[i] = acc;
return f.mul(acc, num); return f.mul(acc, num);
}, f.ONE); }, f.ONE);
// Invert last element // Invert last element
const inverted = f.invert(lastMultiplied); const inverted = f.inv(lastMultiplied);
// Walk from last to first, multiply them by inverted each other MOD p // Walk from last to first, multiply them by inverted each other MOD p
nums.reduceRight((acc, num, i) => { nums.reduceRight((acc, num, i) => {
if (f.isZero(num)) return acc; if (f.is0(num)) return acc;
tmp[i] = f.mul(acc, tmp[i]); tmp[i] = f.mul(acc, tmp[i]);
return f.mul(acc, num); return f.mul(acc, num);
}, inverted); }, inverted);
return tmp; return tmp;
} }
export function FpDiv<T>(f: Field<T>, lhs: T, rhs: T | bigint): T { export function FpDiv<T>(f: IField<T>, lhs: T, rhs: T | bigint): T {
return f.mul(lhs, typeof rhs === 'bigint' ? invert(rhs, f.ORDER) : f.invert(rhs)); return f.mul(lhs, typeof rhs === 'bigint' ? invert(rhs, f.ORDER) : f.inv(rhs));
} }
// This function returns True whenever the value x is a square in the field F. // This function returns True whenever the value x is a square in the field F.
export function FpIsSquare<T>(f: Field<T>) { export function FpIsSquare<T>(f: IField<T>) {
const legendreConst = (f.ORDER - _1n) / _2n; // Integer arithmetic const legendreConst = (f.ORDER - _1n) / _2n; // Integer arithmetic
return (x: T): boolean => { return (x: T): boolean => {
const p = f.pow(x, legendreConst); const p = f.pow(x, legendreConst);
return f.equals(p, f.ZERO) || f.equals(p, f.ONE); return f.eql(p, f.ZERO) || f.eql(p, f.ONE);
}; };
} }
// NOTE: very fragile, always bench. Major performance points: // CURVE.n lengths
// - NonNormalized ops export function nLength(n: bigint, nBitLength?: number) {
// - Object.freeze // Bit size, byte size of CURVE.n
// - same shape of object (don't add/remove keys) const _nBitLength = nBitLength !== undefined ? nBitLength : n.toString(2).length;
type FpField = Field<bigint> & Required<Pick<Field<bigint>, 'isOdd'>>; const nByteLength = Math.ceil(_nBitLength / 8);
export function Fp( return { nBitLength: _nBitLength, nByteLength };
}
type FpField = IField<bigint> & Required<Pick<IField<bigint>, 'isOdd'>>;
/**
* Initializes a galois field over prime. Non-primes are not supported for now.
* Do not init in loop: slow. Very fragile: always run a benchmark on change.
* Major performance gains:
* a) non-normalized operations like mulN instead of mul
* b) `Object.freeze`
* c) Same object shape: never add or remove keys
* @param ORDER prime positive bigint
* @param bitLen how many bits the field consumes
* @param isLE (def: false) if encoding / decoding should be in little-endian
* @param redef optional faster redefinitions of sqrt and other methods
*/
export function Field(
ORDER: bigint, ORDER: bigint,
bitLen?: number, bitLen?: number,
isLE = false, isLE = false,
redef: Partial<Field<bigint>> = {} redef: Partial<IField<bigint>> = {}
): Readonly<FpField> { ): Readonly<FpField> {
if (ORDER <= _0n) throw new Error(`Expected Fp ORDER > 0, got ${ORDER}`); if (ORDER <= _0n) throw new Error(`Expected Fp ORDER > 0, got ${ORDER}`);
const { nBitLength: BITS, nByteLength: BYTES } = utils.nLength(ORDER, bitLen); const { nBitLength: BITS, nByteLength: BYTES } = nLength(ORDER, bitLen);
if (BYTES > 2048) throw new Error('Field lengths over 2048 bytes are not supported'); if (BYTES > 2048) throw new Error('Field lengths over 2048 bytes are not supported');
const sqrtP = FpSqrt(ORDER); const sqrtP = FpSqrt(ORDER);
const f: Readonly<FpField> = Object.freeze({ const f: Readonly<FpField> = Object.freeze({
ORDER, ORDER,
BITS, BITS,
BYTES, BYTES,
MASK: utils.bitMask(BITS), MASK: bitMask(BITS),
ZERO: _0n, ZERO: _0n,
ONE: _1n, ONE: _1n,
create: (num) => mod(num, ORDER), create: (num) => mod(num, ORDER),
isValid: (num) => { isValid: (num) => {
if (typeof num !== 'bigint') if (typeof num !== 'bigint')
throw new Error(`Invalid field element: expected bigint, got ${typeof num}`); throw new Error(`Invalid field element: expected bigint, got ${typeof num}`);
return _0n <= num && num < ORDER; return _0n <= num && num < ORDER; // 0 is valid element, but it's not invertible
}, },
isZero: (num) => num === _0n, is0: (num) => num === _0n,
isOdd: (num) => (num & _1n) === _1n, isOdd: (num) => (num & _1n) === _1n,
negate: (num) => mod(-num, ORDER), neg: (num) => mod(-num, ORDER),
equals: (lhs, rhs) => lhs === rhs, eql: (lhs, rhs) => lhs === rhs,
square: (num) => mod(num * num, ORDER), sqr: (num) => mod(num * num, ORDER),
add: (lhs, rhs) => mod(lhs + rhs, ORDER), add: (lhs, rhs) => mod(lhs + rhs, ORDER),
sub: (lhs, rhs) => mod(lhs - rhs, ORDER), sub: (lhs, rhs) => mod(lhs - rhs, ORDER),
mul: (lhs, rhs) => mod(lhs * rhs, ORDER), mul: (lhs, rhs) => mod(lhs * rhs, ORDER),
@@ -347,37 +370,58 @@ export function Fp(
div: (lhs, rhs) => mod(lhs * invert(rhs, ORDER), ORDER), div: (lhs, rhs) => mod(lhs * invert(rhs, ORDER), ORDER),
// Same as above, but doesn't normalize // Same as above, but doesn't normalize
squareN: (num) => num * num, sqrN: (num) => num * num,
addN: (lhs, rhs) => lhs + rhs, addN: (lhs, rhs) => lhs + rhs,
subN: (lhs, rhs) => lhs - rhs, subN: (lhs, rhs) => lhs - rhs,
mulN: (lhs, rhs) => lhs * rhs, mulN: (lhs, rhs) => lhs * rhs,
invert: (num) => invert(num, ORDER), inv: (num) => invert(num, ORDER),
sqrt: redef.sqrt || ((n) => sqrtP(f, n)), sqrt: redef.sqrt || ((n) => sqrtP(f, n)),
invertBatch: (lst) => FpInvertBatch(f, lst), invertBatch: (lst) => FpInvertBatch(f, lst),
// TODO: do we really need constant cmov? // TODO: do we really need constant cmov?
// We don't have const-time bigints anyway, so probably will be not very useful // We don't have const-time bigints anyway, so probably will be not very useful
cmov: (a, b, c) => (c ? b : a), cmov: (a, b, c) => (c ? b : a),
toBytes: (num) => toBytes: (num) => (isLE ? numberToBytesLE(num, BYTES) : numberToBytesBE(num, BYTES)),
isLE ? utils.numberToBytesLE(num, BYTES) : utils.numberToBytesBE(num, BYTES),
fromBytes: (bytes) => { fromBytes: (bytes) => {
if (bytes.length !== BYTES) if (bytes.length !== BYTES)
throw new Error(`Fp.fromBytes: expected ${BYTES}, got ${bytes.length}`); throw new Error(`Fp.fromBytes: expected ${BYTES}, got ${bytes.length}`);
return isLE ? utils.bytesToNumberLE(bytes) : utils.bytesToNumberBE(bytes); return isLE ? bytesToNumberLE(bytes) : bytesToNumberBE(bytes);
}, },
} as FpField); } as FpField);
return Object.freeze(f); return Object.freeze(f);
} }
export function FpSqrtOdd<T>(Fp: Field<T>, elm: T) { export function FpSqrtOdd<T>(Fp: IField<T>, elm: T) {
if (!Fp.isOdd) throw new Error(`Field doesn't have isOdd`); if (!Fp.isOdd) throw new Error(`Field doesn't have isOdd`);
const root = Fp.sqrt(elm); const root = Fp.sqrt(elm);
return Fp.isOdd(root) ? root : Fp.negate(root); return Fp.isOdd(root) ? root : Fp.neg(root);
} }
export function FpSqrtEven<T>(Fp: Field<T>, elm: T) { export function FpSqrtEven<T>(Fp: IField<T>, elm: T) {
if (!Fp.isOdd) throw new Error(`Field doesn't have isOdd`); if (!Fp.isOdd) throw new Error(`Field doesn't have isOdd`);
const root = Fp.sqrt(elm); const root = Fp.sqrt(elm);
return Fp.isOdd(root) ? Fp.negate(root) : root; return Fp.isOdd(root) ? Fp.neg(root) : root;
}
/**
* FIPS 186 B.4.1-compliant "constant-time" private key generation utility.
* Can take (n+8) or more bytes of uniform input e.g. from CSPRNG or KDF
* and convert them into private scalar, with the modulo bias being neglible.
* Needs at least 40 bytes of input for 32-byte private key.
* https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
* @param hash hash output from SHA3 or a similar function
* @returns valid private scalar
*/
export function hashToPrivateScalar(
hash: string | Uint8Array,
groupOrder: bigint,
isLE = false
): bigint {
hash = ensureBytes('privateHash', hash);
const hashLen = hash.length;
const minLen = nLength(groupOrder).nByteLength + 8;
if (minLen < 24 || hashLen < minLen || hashLen > 1024)
throw new Error(`hashToPrivateScalar: expected ${minLen}-1024 bytes of input, got ${hashLen}`);
const num = isLE ? bytesToNumberLE(hash) : bytesToNumberBE(hash);
return mod(num, groupOrder - _1n) + _1n;
} }

View File

@@ -1,52 +1,48 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import * as mod from './modular.js'; import { mod, pow } from './modular.js';
import { ensureBytes, numberToBytesLE, bytesToNumberLE, isPositiveInt } from './utils.js'; import { bytesToNumberLE, ensureBytes, numberToBytesLE, validateObject } from './utils.js';
const _0n = BigInt(0); const _0n = BigInt(0);
const _1n = BigInt(1); const _1n = BigInt(1);
type Hex = string | Uint8Array; type Hex = string | Uint8Array;
export type CurveType = { export type CurveType = {
// Field over which we'll do calculations. Verify with: P: bigint; // finite field prime
P: bigint;
nByteLength: number; nByteLength: number;
adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array; adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array;
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array; domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
a24: bigint; // Related to d, but cannot be derived from it a: bigint;
montgomeryBits: number; montgomeryBits: number;
powPminus2?: (x: bigint) => bigint; powPminus2?: (x: bigint) => bigint;
xyToU?: (x: bigint, y: bigint) => bigint; xyToU?: (x: bigint, y: bigint) => bigint;
Gu: string; Gu: bigint;
randomBytes?: (bytesLength?: number) => Uint8Array;
}; };
export type CurveFn = { export type CurveFn = {
scalarMult: (scalar: Hex, u: Hex) => Uint8Array; scalarMult: (scalar: Hex, u: Hex) => Uint8Array;
scalarMultBase: (scalar: Hex) => Uint8Array; scalarMultBase: (scalar: Hex) => Uint8Array;
getSharedSecret: (privateKeyA: Hex, publicKeyB: Hex) => Uint8Array; getSharedSecret: (privateKeyA: Hex, publicKeyB: Hex) => Uint8Array;
getPublicKey: (privateKey: Hex) => Uint8Array; getPublicKey: (privateKey: Hex) => Uint8Array;
Gu: string; utils: { randomPrivateKey: () => Uint8Array };
GuBytes: Uint8Array;
}; };
function validateOpts(curve: CurveType) { function validateOpts(curve: CurveType) {
for (const i of ['a24'] as const) { validateObject(
if (typeof curve[i] !== 'bigint') curve,
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); {
} a: 'bigint',
for (const i of ['montgomeryBits', 'nByteLength'] as const) { },
if (curve[i] === undefined) continue; // Optional {
if (!isPositiveInt(curve[i])) montgomeryBits: 'isSafeInteger',
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); nByteLength: 'isSafeInteger',
} adjustScalarBytes: 'function',
for (const fn of ['adjustScalarBytes', 'domain', 'powPminus2'] as const) { domain: 'function',
if (curve[fn] === undefined) continue; // Optional powPminus2: 'function',
if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`); Gu: 'bigint',
}
for (const i of ['Gu'] as const) {
if (curve[i] === undefined) continue; // Optional
if (typeof curve[i] !== 'string')
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
} }
);
// Set defaults // Set defaults
// ...nLength(curve.n, curve.nBitLength),
return Object.freeze({ ...curve } as const); return Object.freeze({ ...curve } as const);
} }
@@ -55,34 +51,14 @@ function validateOpts(curve: CurveType) {
export function montgomery(curveDef: CurveType): CurveFn { export function montgomery(curveDef: CurveType): CurveFn {
const CURVE = validateOpts(curveDef); const CURVE = validateOpts(curveDef);
const { P } = CURVE; const { P } = CURVE;
const modP = (a: bigint) => mod.mod(a, P); const modP = (n: bigint) => mod(n, P);
const montgomeryBits = CURVE.montgomeryBits; const montgomeryBits = CURVE.montgomeryBits;
const montgomeryBytes = Math.ceil(montgomeryBits / 8); const montgomeryBytes = Math.ceil(montgomeryBits / 8);
const fieldLen = CURVE.nByteLength; const fieldLen = CURVE.nByteLength;
const adjustScalarBytes = CURVE.adjustScalarBytes || ((bytes: Uint8Array) => bytes); const adjustScalarBytes = CURVE.adjustScalarBytes || ((bytes: Uint8Array) => bytes);
const powPminus2 = CURVE.powPminus2 || ((x: bigint) => mod.pow(x, P - BigInt(2), P)); const powPminus2 = CURVE.powPminus2 || ((x: bigint) => pow(x, P - BigInt(2), P));
/** // cswap from RFC7748. But it is not from RFC7748!
* Checks for num to be in range:
* For strict == true: `0 < num < max`.
* For strict == false: `0 <= num < max`.
* Converts non-float safe numbers to bigints.
*/
function normalizeScalar(num: number | bigint, max: bigint, strict = true): bigint {
if (!max) throw new TypeError('Specify max value');
if (typeof num === 'number' && Number.isSafeInteger(num)) num = BigInt(num);
if (typeof num === 'bigint' && num < max) {
if (strict) {
if (_0n < num) return num;
} else {
if (_0n <= num) return num;
}
}
throw new TypeError('Expected valid scalar: 0 < scalar < max');
}
// cswap from RFC7748
// NOTE: cswap is not from RFC7748!
/* /*
cswap(swap, x_2, x_3): cswap(swap, x_2, x_3):
dummy = mask(swap) AND (x_2 XOR x_3) dummy = mask(swap) AND (x_2 XOR x_3)
@@ -99,7 +75,15 @@ export function montgomery(curveDef: CurveType): CurveFn {
return [x_2, x_3]; return [x_2, x_3];
} }
// Accepts 0 as well
function assertFieldElement(n: bigint): bigint {
if (typeof n === 'bigint' && _0n <= n && n < P) return n;
throw new Error('Expected valid scalar 0 < scalar < CURVE.P');
}
// x25519 from 4 // x25519 from 4
// The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519
const a24 = (CURVE.a - BigInt(2)) / BigInt(4);
/** /**
* *
* @param pointU u coordinate (x) on Montgomery Curve 25519 * @param pointU u coordinate (x) on Montgomery Curve 25519
@@ -107,13 +91,10 @@ export function montgomery(curveDef: CurveType): CurveFn {
* @returns new Point on Montgomery curve * @returns new Point on Montgomery curve
*/ */
function montgomeryLadder(pointU: bigint, scalar: bigint): bigint { function montgomeryLadder(pointU: bigint, scalar: bigint): bigint {
const { P } = CURVE; const u = assertFieldElement(pointU);
const u = normalizeScalar(pointU, P);
// Section 5: Implementations MUST accept non-canonical values and process them as // Section 5: Implementations MUST accept non-canonical values and process them as
// if they had been reduced modulo the field prime. // if they had been reduced modulo the field prime.
const k = normalizeScalar(scalar, P); const k = assertFieldElement(scalar);
// The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519
const a24 = CURVE.a24;
const x_1 = u; const x_1 = u;
let x_2 = _1n; let x_2 = _1n;
let z_2 = _0n; let z_2 = _0n;
@@ -167,28 +148,21 @@ export function montgomery(curveDef: CurveType): CurveFn {
} }
function decodeUCoordinate(uEnc: Hex): bigint { function decodeUCoordinate(uEnc: Hex): bigint {
const u = ensureBytes(uEnc, montgomeryBytes);
// Section 5: When receiving such an array, implementations of X25519 // Section 5: When receiving such an array, implementations of X25519
// MUST mask the most significant bit in the final byte. // MUST mask the most significant bit in the final byte.
// This is very ugly way, but it works because fieldLen-1 is outside of bounds for X448, so this becomes NOOP // This is very ugly way, but it works because fieldLen-1 is outside of bounds for X448, so this becomes NOOP
// fieldLen - scalaryBytes = 1 for X448 and = 0 for X25519 // fieldLen - scalaryBytes = 1 for X448 and = 0 for X25519
u[fieldLen - 1] &= 127; // 0b0111_1111 const u = ensureBytes('u coordinate', uEnc, montgomeryBytes);
// u[fieldLen-1] crashes QuickJS (TypeError: out-of-bound numeric index)
if (fieldLen === montgomeryBytes) u[fieldLen - 1] &= 127; // 0b0111_1111
return bytesToNumberLE(u); return bytesToNumberLE(u);
} }
function decodeScalar(n: Hex): bigint { function decodeScalar(n: Hex): bigint {
const bytes = ensureBytes(n); const bytes = ensureBytes('scalar', n);
if (bytes.length !== montgomeryBytes && bytes.length !== fieldLen) if (bytes.length !== montgomeryBytes && bytes.length !== fieldLen)
throw new Error(`Expected ${montgomeryBytes} or ${fieldLen} bytes, got ${bytes.length}`); throw new Error(`Expected ${montgomeryBytes} or ${fieldLen} bytes, got ${bytes.length}`);
return bytesToNumberLE(adjustScalarBytes(bytes)); return bytesToNumberLE(adjustScalarBytes(bytes));
} }
/**
* Computes shared secret between private key "scalar" and public key's "u" (x) coordinate.
* We can get 'y' coordinate from 'u',
* but Point.fromHex also wants 'x' coordinate oddity flag,
* and we cannot get 'x' without knowing 'v'.
* Need to add generic conversion between twisted edwards and complimentary curve for JubJub.
*/
function scalarMult(scalar: Hex, u: Hex): Uint8Array { function scalarMult(scalar: Hex, u: Hex): Uint8Array {
const pointU = decodeUCoordinate(u); const pointU = decodeUCoordinate(u);
const _scalar = decodeScalar(scalar); const _scalar = decodeScalar(scalar);
@@ -198,14 +172,10 @@ export function montgomery(curveDef: CurveType): CurveFn {
if (pu === _0n) throw new Error('Invalid private or public key received'); if (pu === _0n) throw new Error('Invalid private or public key received');
return encodeUCoordinate(pu); return encodeUCoordinate(pu);
} }
/** // Computes public key from private. By doing scalar multiplication of base point.
* Computes public key from private. const GuBytes = encodeUCoordinate(CURVE.Gu);
* Executes scalar multiplication of curve's base point by scalar.
* @param scalar private key
* @returns new public key
*/
function scalarMultBase(scalar: Hex): Uint8Array { function scalarMultBase(scalar: Hex): Uint8Array {
return scalarMult(scalar, CURVE.Gu); return scalarMult(scalar, GuBytes);
} }
return { return {
@@ -213,6 +183,7 @@ export function montgomery(curveDef: CurveType): CurveFn {
scalarMultBase, scalarMultBase,
getSharedSecret: (privateKey: Hex, publicKey: Hex) => scalarMult(privateKey, publicKey), getSharedSecret: (privateKey: Hex, publicKey: Hex) => scalarMult(privateKey, publicKey),
getPublicKey: (privateKey: Hex): Uint8Array => scalarMultBase(privateKey), getPublicKey: (privateKey: Hex): Uint8Array => scalarMultBase(privateKey),
Gu: CURVE.Gu, utils: { randomPrivateKey: () => CURVE.randomBytes!(CURVE.nByteLength) },
GuBytes: GuBytes,
}; };
} }

119
src/abstract/poseidon.ts Normal file
View File

@@ -0,0 +1,119 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// Poseidon Hash: https://eprint.iacr.org/2019/458.pdf, https://www.poseidon-hash.info
import { IField, FpPow, validateField } from './modular.js';
// We don't provide any constants, since different implementations use different constants.
// For reference constants see './test/poseidon.test.js'.
export type PoseidonOpts = {
Fp: IField<bigint>;
t: number;
roundsFull: number;
roundsPartial: number;
sboxPower?: number;
reversePartialPowIdx?: boolean; // Hack for stark
mds: bigint[][];
roundConstants: bigint[][];
};
export function validateOpts(opts: PoseidonOpts) {
const { Fp } = opts;
validateField(Fp);
for (const i of ['t', 'roundsFull', 'roundsPartial'] as const) {
if (typeof opts[i] !== 'number' || !Number.isSafeInteger(opts[i]))
throw new Error(`Poseidon: invalid param ${i}=${opts[i]} (${typeof opts[i]})`);
}
if (opts.reversePartialPowIdx !== undefined && typeof opts.reversePartialPowIdx !== 'boolean')
throw new Error(`Poseidon: invalid param reversePartialPowIdx=${opts.reversePartialPowIdx}`);
// Default is 5, but by some reasons stark uses 3
let sboxPower = opts.sboxPower;
if (sboxPower === undefined) sboxPower = 5;
if (typeof sboxPower !== 'number' || !Number.isSafeInteger(sboxPower))
throw new Error(`Poseidon wrong sboxPower=${sboxPower}`);
const _sboxPower = BigInt(sboxPower);
let sboxFn = (n: bigint) => FpPow(Fp, n, _sboxPower);
// Unwrapped sbox power for common cases (195->142μs)
if (sboxPower === 3) sboxFn = (n: bigint) => Fp.mul(Fp.sqrN(n), n);
else if (sboxPower === 5) sboxFn = (n: bigint) => Fp.mul(Fp.sqrN(Fp.sqrN(n)), n);
if (opts.roundsFull % 2 !== 0)
throw new Error(`Poseidon roundsFull is not even: ${opts.roundsFull}`);
const rounds = opts.roundsFull + opts.roundsPartial;
if (!Array.isArray(opts.roundConstants) || opts.roundConstants.length !== rounds)
throw new Error('Poseidon: wrong round constants');
const roundConstants = opts.roundConstants.map((rc) => {
if (!Array.isArray(rc) || rc.length !== opts.t)
throw new Error(`Poseidon wrong round constants: ${rc}`);
return rc.map((i) => {
if (typeof i !== 'bigint' || !Fp.isValid(i))
throw new Error(`Poseidon wrong round constant=${i}`);
return Fp.create(i);
});
});
// MDS is TxT matrix
if (!Array.isArray(opts.mds) || opts.mds.length !== opts.t)
throw new Error('Poseidon: wrong MDS matrix');
const mds = opts.mds.map((mdsRow) => {
if (!Array.isArray(mdsRow) || mdsRow.length !== opts.t)
throw new Error(`Poseidon MDS matrix row: ${mdsRow}`);
return mdsRow.map((i) => {
if (typeof i !== 'bigint') throw new Error(`Poseidon MDS matrix value=${i}`);
return Fp.create(i);
});
});
return Object.freeze({ ...opts, rounds, sboxFn, roundConstants, mds });
}
export function splitConstants(rc: bigint[], t: number) {
if (typeof t !== 'number') throw new Error('poseidonSplitConstants: wrong t');
if (!Array.isArray(rc) || rc.length % t) throw new Error('poseidonSplitConstants: wrong rc');
const res = [];
let tmp = [];
for (let i = 0; i < rc.length; i++) {
tmp.push(rc[i]);
if (tmp.length === t) {
res.push(tmp);
tmp = [];
}
}
return res;
}
export function poseidon(opts: PoseidonOpts) {
const { t, Fp, rounds, sboxFn, reversePartialPowIdx } = validateOpts(opts);
const halfRoundsFull = Math.floor(opts.roundsFull / 2);
const partialIdx = reversePartialPowIdx ? t - 1 : 0;
const poseidonRound = (values: bigint[], isFull: boolean, idx: number) => {
values = values.map((i, j) => Fp.add(i, opts.roundConstants[idx][j]));
if (isFull) values = values.map((i) => sboxFn(i));
else values[partialIdx] = sboxFn(values[partialIdx]);
// Matrix multiplication
values = opts.mds.map((i) =>
i.reduce((acc, i, j) => Fp.add(acc, Fp.mulN(i, values[j])), Fp.ZERO)
);
return values;
};
const poseidonHash = function poseidonHash(values: bigint[]) {
if (!Array.isArray(values) || values.length !== t)
throw new Error(`Poseidon: wrong values (expected array of bigints with length ${t})`);
values = values.map((i) => {
if (typeof i !== 'bigint') throw new Error(`Poseidon: wrong value=${i} (${typeof i})`);
return Fp.create(i);
});
let round = 0;
// Apply r_f/2 full rounds.
for (let i = 0; i < halfRoundsFull; i++) values = poseidonRound(values, true, round++);
// Apply r_p partial rounds.
for (let i = 0; i < opts.roundsPartial; i++) values = poseidonRound(values, false, round++);
// Apply r_f/2 full rounds.
for (let i = 0; i < halfRoundsFull; i++) values = poseidonRound(values, true, round++);
if (round !== rounds)
throw new Error(`Poseidon: wrong number of rounds: last round=${round}, total=${rounds}`);
return values;
};
// For verification in tests
poseidonHash.roundConstants = opts.roundConstants;
return poseidonHash;
}

View File

@@ -1,75 +1,28 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import * as mod from './modular.js';
const _0n = BigInt(0); const _0n = BigInt(0);
const _1n = BigInt(1); const _1n = BigInt(1);
const _2n = BigInt(2); const _2n = BigInt(2);
const u8a = (a: any): a is Uint8Array => a instanceof Uint8Array;
// We accept hex strings besides Uint8Array for simplicity // We accept hex strings besides Uint8Array for simplicity
export type Hex = Uint8Array | string; export type Hex = Uint8Array | string;
// Very few implementations accept numbers, we do it to ease learning curve // Very few implementations accept numbers, we do it to ease learning curve
export type PrivKey = Hex | bigint | number; export type PrivKey = Hex | bigint;
export type CHash = { export type CHash = {
(message: Uint8Array | string): Uint8Array; (message: Uint8Array | string): Uint8Array;
blockLen: number; blockLen: number;
outputLen: number; outputLen: number;
create(opts?: { dkLen?: number }): any; // For shake create(opts?: { dkLen?: number }): any; // For shake
}; };
export type FHash = (message: Uint8Array | string) => Uint8Array;
// NOTE: these are generic, even if curve is on some polynominal field (bls), it will still have P/n/h
// But generator can be different (Fp2/Fp6 for bls?)
export type BasicCurve<T> = {
// Field over which we'll do calculations (Fp)
Fp: mod.Field<T>;
// Curve order, total count of valid points in the field
n: bigint;
// Bit/byte length of curve order
nBitLength?: number;
nByteLength?: number;
// Cofactor
// NOTE: we can assign default value of 1, but then users will just ignore it, without validating with spec
// Has not use for now, but nice to have in API
h: bigint;
hEff?: bigint; // Number to multiply to clear cofactor
// Base point (x, y) aka generator point
Gx: T;
Gy: T;
// Wrap private key by curve order (% CURVE.n instead of throwing error)
wrapPrivateKey?: boolean;
// Point at infinity is perfectly valid point, but not valid public key.
// Disabled by default because of compatibility reasons with @noble/secp256k1
allowInfinityPoint?: boolean;
};
// Bans floats and integers above 2^53-1
export function isPositiveInt(num: any): num is number {
return typeof num === 'number' && Number.isSafeInteger(num) && num > 0;
}
export function validateOpts<FP, T>(curve: BasicCurve<FP> & T) {
mod.validateField(curve.Fp);
for (const i of ['n', 'h'] as const) {
const val = curve[i];
if (typeof val !== 'bigint') throw new Error(`Invalid curve param ${i}=${val} (${typeof val})`);
}
if (!curve.Fp.isValid(curve.Gx)) throw new Error('Invalid generator X coordinate Fp element');
if (!curve.Fp.isValid(curve.Gy)) throw new Error('Invalid generator Y coordinate Fp element');
for (const i of ['nBitLength', 'nByteLength'] as const) {
const val = curve[i];
if (val === undefined) continue; // Optional
if (!isPositiveInt(val)) throw new Error(`Invalid curve param ${i}=${val} (${typeof val})`);
}
// Set defaults
return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const);
}
const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0')); const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0'));
export function bytesToHex(uint8a: Uint8Array): string { export function bytesToHex(bytes: Uint8Array): string {
if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array'); if (!u8a(bytes)) throw new Error('Uint8Array expected');
// pre-caching improves the speed 6x // pre-caching improves the speed 6x
let hex = ''; let hex = '';
for (let i = 0; i < uint8a.length; i++) { for (let i = 0; i < bytes.length; i++) {
hex += hexes[uint8a[i]]; hex += hexes[bytes[i]];
} }
return hex; return hex;
} }
@@ -80,25 +33,21 @@ export function numberToHexUnpadded(num: number | bigint): string {
} }
export function hexToNumber(hex: string): bigint { export function hexToNumber(hex: string): bigint {
if (typeof hex !== 'string') { if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex);
throw new TypeError('hexToNumber: expected string, got ' + typeof hex);
}
// Big Endian // Big Endian
return BigInt(`0x${hex}`); return BigInt(hex === '' ? '0' : `0x${hex}`);
} }
// Caching slows it down 2-3x // Caching slows it down 2-3x
export function hexToBytes(hex: string): Uint8Array { export function hexToBytes(hex: string): Uint8Array {
if (typeof hex !== 'string') { if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex);
throw new TypeError('hexToBytes: expected string, got ' + typeof hex); if (hex.length % 2) throw new Error('hex string is invalid: unpadded ' + hex.length);
}
if (hex.length % 2) throw new Error('hexToBytes: received invalid unpadded hex ' + hex.length);
const array = new Uint8Array(hex.length / 2); const array = new Uint8Array(hex.length / 2);
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {
const j = i * 2; const j = i * 2;
const hexByte = hex.slice(j, j + 2); const hexByte = hex.slice(j, j + 2);
const byte = Number.parseInt(hexByte, 16); const byte = Number.parseInt(hexByte, 16);
if (Number.isNaN(byte) || byte < 0) throw new Error('Invalid byte sequence'); if (Number.isNaN(byte) || byte < 0) throw new Error('invalid byte sequence');
array[i] = byte; array[i] = byte;
} }
return array; return array;
@@ -108,63 +57,48 @@ export function hexToBytes(hex: string): Uint8Array {
export function bytesToNumberBE(bytes: Uint8Array): bigint { export function bytesToNumberBE(bytes: Uint8Array): bigint {
return hexToNumber(bytesToHex(bytes)); return hexToNumber(bytesToHex(bytes));
} }
export function bytesToNumberLE(uint8a: Uint8Array): bigint { export function bytesToNumberLE(bytes: Uint8Array): bigint {
if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array'); if (!u8a(bytes)) throw new Error('Uint8Array expected');
return BigInt('0x' + bytesToHex(Uint8Array.from(uint8a).reverse())); return hexToNumber(bytesToHex(Uint8Array.from(bytes).reverse()));
} }
export const numberToBytesBE = (n: bigint, len: number) => export const numberToBytesBE = (n: bigint, len: number) =>
hexToBytes(n.toString(16).padStart(len * 2, '0')); hexToBytes(n.toString(16).padStart(len * 2, '0'));
export const numberToBytesLE = (n: bigint, len: number) => numberToBytesBE(n, len).reverse(); export const numberToBytesLE = (n: bigint, len: number) => numberToBytesBE(n, len).reverse();
// Returns variable number bytes (minimal bigint encoding?)
export const numberToVarBytesBE = (n: bigint) => hexToBytes(numberToHexUnpadded(n));
export function ensureBytes(hex: Hex, expectedLength?: number): Uint8Array { export function ensureBytes(title: string, hex: Hex, expectedLength?: number): Uint8Array {
let res: Uint8Array;
if (typeof hex === 'string') {
try {
res = hexToBytes(hex);
} catch (e) {
throw new Error(`${title} must be valid hex string, got "${hex}". Cause: ${e}`);
}
} else if (u8a(hex)) {
// Uint8Array.from() instead of hash.slice() because node.js Buffer // Uint8Array.from() instead of hash.slice() because node.js Buffer
// is instance of Uint8Array, and its slice() creates **mutable** copy // is instance of Uint8Array, and its slice() creates **mutable** copy
const bytes = hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex); res = Uint8Array.from(hex);
if (typeof expectedLength === 'number' && bytes.length !== expectedLength) } else {
throw new Error(`Expected ${expectedLength} bytes`); throw new Error(`${title} must be hex string or Uint8Array`);
return bytes; }
const len = res.length;
if (typeof expectedLength === 'number' && len !== expectedLength)
throw new Error(`${title} expected ${expectedLength} bytes, got ${len}`);
return res;
} }
// Copies several Uint8Arrays into one. // Copies several Uint8Arrays into one.
export function concatBytes(...arrays: Uint8Array[]): Uint8Array { export function concatBytes(...arrs: Uint8Array[]): Uint8Array {
if (!arrays.every((b) => b instanceof Uint8Array)) throw new Error('Uint8Array list expected'); const r = new Uint8Array(arrs.reduce((sum, a) => sum + a.length, 0));
if (arrays.length === 1) return arrays[0]; let pad = 0; // walk through each item, ensure they have proper type
const length = arrays.reduce((a, arr) => a + arr.length, 0); arrs.forEach((a) => {
const result = new Uint8Array(length); if (!u8a(a)) throw new Error('Uint8Array expected');
for (let i = 0, pad = 0; i < arrays.length; i++) { r.set(a, pad);
const arr = arrays[i]; pad += a.length;
result.set(arr, pad); });
pad += arr.length; return r;
}
return result;
}
// CURVE.n lengths
export function nLength(n: bigint, nBitLength?: number) {
// Bit size, byte size of CURVE.n
const _nBitLength = nBitLength !== undefined ? nBitLength : n.toString(2).length;
const nByteLength = Math.ceil(_nBitLength / 8);
return { nBitLength: _nBitLength, nByteLength };
}
/**
* FIPS 186 B.4.1-compliant "constant-time" private key generation utility.
* Can take (n+8) or more bytes of uniform input e.g. from CSPRNG or KDF
* and convert them into private scalar, with the modulo bias being neglible.
* Needs at least 40 bytes of input for 32-byte private key.
* https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
* @param hash hash output from SHA3 or a similar function
* @returns valid private scalar
*/
export function hashToPrivateScalar(hash: Hex, groupOrder: bigint, isLE = false): bigint {
hash = ensureBytes(hash);
const hashLen = hash.length;
const minLen = nLength(groupOrder).nByteLength + 8;
if (minLen < 24 || hashLen < minLen || hashLen > 1024)
throw new Error(`hashToPrivateScalar: expected ${minLen}-1024 bytes of input, got ${hashLen}`);
const num = isLE ? bytesToNumberLE(hash) : bytesToNumberBE(hash);
return mod.mod(num, groupOrder - _1n) + _1n;
} }
export function equalBytes(b1: Uint8Array, b2: Uint8Array) { export function equalBytes(b1: Uint8Array, b2: Uint8Array) {
@@ -174,6 +108,16 @@ export function equalBytes(b1: Uint8Array, b2: Uint8Array) {
return true; return true;
} }
// Global symbols in both browsers and Node.js since v11
// See https://github.com/microsoft/TypeScript/issues/31535
declare const TextEncoder: any;
export function utf8ToBytes(str: string): Uint8Array {
if (typeof str !== 'string') {
throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
}
return new TextEncoder().encode(str);
}
// Bit operations // Bit operations
// Amount of bits inside bigint (Same as n.toString(2).length) // Amount of bits inside bigint (Same as n.toString(2).length)
@@ -191,3 +135,112 @@ export const bitSet = (n: bigint, pos: number, value: boolean) =>
// Return mask for N bits (Same as BigInt(`0b${Array(i).fill('1').join('')}`)) // Return mask for N bits (Same as BigInt(`0b${Array(i).fill('1').join('')}`))
// Not using ** operator with bigints for old engines. // Not using ** operator with bigints for old engines.
export const bitMask = (n: number) => (_2n << BigInt(n - 1)) - _1n; export const bitMask = (n: number) => (_2n << BigInt(n - 1)) - _1n;
// DRBG
const u8n = (data?: any) => new Uint8Array(data); // creates Uint8Array
const u8fr = (arr: any) => Uint8Array.from(arr); // another shortcut
type Pred<T> = (v: Uint8Array) => T | undefined;
/**
* Minimal HMAC-DRBG from NIST 800-90 for RFC6979 sigs.
* @returns function that will call DRBG until 2nd arg returns something meaningful
* @example
* const drbg = createHmacDRBG<Key>(32, 32, hmac);
* drbg(seed, bytesToKey); // bytesToKey must return Key or undefined
*/
export function createHmacDrbg<T>(
hashLen: number,
qByteLen: number,
hmacFn: (key: Uint8Array, ...messages: Uint8Array[]) => Uint8Array
): (seed: Uint8Array, predicate: Pred<T>) => T {
if (typeof hashLen !== 'number' || hashLen < 2) throw new Error('hashLen must be a number');
if (typeof qByteLen !== 'number' || qByteLen < 2) throw new Error('qByteLen must be a number');
if (typeof hmacFn !== 'function') throw new Error('hmacFn must be a function');
// Step B, Step C: set hashLen to 8*ceil(hlen/8)
let v = u8n(hashLen); // Minimal non-full-spec HMAC-DRBG from NIST 800-90 for RFC6979 sigs.
let k = u8n(hashLen); // Steps B and C of RFC6979 3.2: set hashLen, in our case always same
let i = 0; // Iterations counter, will throw when over 1000
const reset = () => {
v.fill(1);
k.fill(0);
i = 0;
};
const h = (...b: Uint8Array[]) => hmacFn(k, v, ...b); // hmac(k)(v, ...values)
const reseed = (seed = u8n()) => {
// HMAC-DRBG reseed() function. Steps D-G
k = h(u8fr([0x00]), seed); // k = hmac(k || v || 0x00 || seed)
v = h(); // v = hmac(k || v)
if (seed.length === 0) return;
k = h(u8fr([0x01]), seed); // k = hmac(k || v || 0x01 || seed)
v = h(); // v = hmac(k || v)
};
const gen = () => {
// HMAC-DRBG generate() function
if (i++ >= 1000) throw new Error('drbg: tried 1000 values');
let len = 0;
const out: Uint8Array[] = [];
while (len < qByteLen) {
v = h();
const sl = v.slice();
out.push(sl);
len += v.length;
}
return concatBytes(...out);
};
const genUntil = (seed: Uint8Array, pred: Pred<T>): T => {
reset();
reseed(seed); // Steps D-G
let res: T | undefined = undefined; // Step H: grind until k is in [1..n-1]
while (!(res = pred(gen()))) reseed();
reset();
return res;
};
return genUntil;
}
// Validating curves and fields
const validatorFns = {
bigint: (val: any) => typeof val === 'bigint',
function: (val: any) => typeof val === 'function',
boolean: (val: any) => typeof val === 'boolean',
string: (val: any) => typeof val === 'string',
isSafeInteger: (val: any) => Number.isSafeInteger(val),
array: (val: any) => Array.isArray(val),
field: (val: any, object: any) => (object as any).Fp.isValid(val),
hash: (val: any) => typeof val === 'function' && Number.isSafeInteger(val.outputLen),
} as const;
type Validator = keyof typeof validatorFns;
type ValMap<T extends Record<string, any>> = { [K in keyof T]?: Validator };
// type Record<K extends string | number | symbol, T> = { [P in K]: T; }
export function validateObject<T extends Record<string, any>>(
object: T,
validators: ValMap<T>,
optValidators: ValMap<T> = {}
) {
const checkField = (fieldName: keyof T, type: Validator, isOptional: boolean) => {
const checkVal = validatorFns[type];
if (typeof checkVal !== 'function')
throw new Error(`Invalid validator "${type}", expected function`);
const val = object[fieldName as keyof typeof object];
if (isOptional && val === undefined) return;
if (!checkVal(val, object)) {
throw new Error(
`Invalid param ${String(fieldName)}=${val} (${typeof val}), expected ${type}`
);
}
};
for (const [fieldName, type] of Object.entries(validators)) checkField(fieldName, type!, false);
for (const [fieldName, type] of Object.entries(optValidators)) checkField(fieldName, type!, true);
return object;
}
// validate type tests
// const o: { a: number; b: number; c: number } = { a: 1, b: 5, c: 6 };
// const z0 = validateObject(o, { a: 'isSafeInteger' }, { c: 'bigint' }); // Ok!
// // Should fail type-check
// const z1 = validateObject(o, { a: 'tmp' }, { c: 'zz' });
// const z2 = validateObject(o, { a: 'isSafeInteger' }, { c: 'zz' });
// const z3 = validateObject(o, { test: 'boolean', z: 'bug' });
// const z4 = validateObject(o, { a: 'boolean', z: 'bug' });

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,43 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// The pairing-friendly Barreto-Lynn-Scott elliptic curve construction allows to: // bls12-381 pairing-friendly Barreto-Lynn-Scott elliptic curve construction allows to:
// - Construct zk-SNARKs at the 128-bit security // - Construct zk-SNARKs at the 128-bit security
// - Use threshold signatures, which allows a user to sign lots of messages with one signature and verify them swiftly in a batch, using Boneh-Lynn-Shacham signature scheme. // - Use threshold signatures, which allows a user to sign lots of messages with one signature and
// Differences from @noble/bls12-381 1.4: // verify them swiftly in a batch, using Boneh-Lynn-Shacham signature scheme.
//
// The library uses G1 for public keys and G2 for signatures. Support for G1 signatures is planned.
// Compatible with Algorand, Chia, Dfinity, Ethereum, FIL, Zcash. Matches specs
// [pairing-curves-10](https://tools.ietf.org/html/draft-irtf-cfrg-pairing-friendly-curves-10),
// [bls-sigs-04](https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-04),
// [hash-to-curve-12](https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-12).
//
// ### Summary
// 1. BLS Relies on Bilinear Pairing (expensive)
// 2. Private Keys: 32 bytes
// 3. Public Keys: 48 bytes: 381 bit affine x coordinate, encoded into 48 big-endian bytes.
// 4. Signatures: 96 bytes: two 381 bit integers (affine x coordinate), encoded into two 48 big-endian byte arrays.
// - The signature is a point on the G2 subgroup, which is defined over a finite field
// with elements twice as big as the G1 curve (G2 is over Fp2 rather than Fp. Fp2 is analogous to the complex numbers).
// 5. The 12 stands for the Embedding degree.
//
// ### Formulas
// - `P = pk x G` - public keys
// - `S = pk x H(m)` - signing
// - `e(P, H(m)) == e(G, S)` - verification using pairings
// - `e(G, S) = e(G, SUM(n)(Si)) = MUL(n)(e(G, Si))` - signature aggregation
// Filecoin uses little endian byte arrays for private keys -
// so ensure to reverse byte order if you'll use it with FIL.
//
// ### Resources
// - [BLS12-381 for the rest of us](https://hackmd.io/@benjaminion/bls12-381)
// - [Key concepts of pairings](https://medium.com/@alonmuroch_65570/bls-signatures-part-2-key-concepts-of-pairings-27a8a9533d0c)
// - Pairing over bls12-381:
// [part 1](https://research.nccgroup.com/2020/07/06/pairing-over-bls12-381-part-1-fields/),
// [part 2](https://research.nccgroup.com/2020/07/13/pairing-over-bls12-381-part-2-curves/),
// [part 3](https://research.nccgroup.com/2020/08/13/pairing-over-bls12-381-part-3-pairing/)
// - [Estimating the bit security of pairing-friendly curves](https://research.nccgroup.com/2022/02/03/estimating-the-bit-security-of-pairing-friendly-curves/)
//
// ### Differences from @noble/bls12-381 1.4
// - PointG1 -> G1.Point // - PointG1 -> G1.Point
// - PointG2 -> G2.Point // - PointG2 -> G2.Point
// - PointG2.fromSignature -> Signature.decode // - PointG2.fromSignature -> Signature.decode
@@ -16,7 +50,7 @@ import { randomBytes } from '@noble/hashes/utils';
import { bls, CurveFn } from './abstract/bls.js'; import { bls, CurveFn } from './abstract/bls.js';
import * as mod from './abstract/modular.js'; import * as mod from './abstract/modular.js';
import { import {
concatBytes, concatBytes as concatB,
ensureBytes, ensureBytes,
numberToBytesBE, numberToBytesBE,
bytesToNumberBE, bytesToNumberBE,
@@ -28,23 +62,30 @@ import {
} from './abstract/utils.js'; } from './abstract/utils.js';
// Types // Types
import { import {
PointType, ProjPointType,
ProjectivePointType, ProjConstructor,
ProjectiveConstructor,
mapToCurveSimpleSWU, mapToCurveSimpleSWU,
AffinePoint,
} from './abstract/weierstrass.js'; } from './abstract/weierstrass.js';
import { isogenyMap } from './abstract/hash-to-curve.js'; import { isogenyMap } from './abstract/hash-to-curve.js';
// Be friendly to bad ECMAScript parsers by not using bigint literals
// prettier-ignore
const _0n = BigInt(0), _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3), _4n = BigInt(4);
const _8n = BigInt(8),
_16n = BigInt(16);
// CURVE FIELDS // CURVE FIELDS
// Finite field over p. // Finite field over p.
const Fp = const Fp = mod.Field(
mod.Fp( BigInt(
0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaabn '0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab'
)
); );
type Fp = bigint; type Fp = bigint;
// Finite field over r. // Finite field over r.
// This particular field is not used anywhere in bls12-381, but it is still useful. // This particular field is not used anywhere in bls12-381, but it is still useful.
const Fr = mod.Fp(0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001n); const Fr = mod.Field(BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001'));
// Fp₂ over complex plane // Fp₂ over complex plane
type BigintTuple = [bigint, bigint]; type BigintTuple = [bigint, bigint];
@@ -87,10 +128,11 @@ type Fp2Utils = {
// h2q // h2q
// NOTE: ORDER was wrong! // NOTE: ORDER was wrong!
const FP2_ORDER = const FP2_ORDER =
0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaabn ** BigInt(
2n; '0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab'
) ** _2n;
const Fp2: mod.Field<Fp2> & Fp2Utils = { const Fp2: mod.IField<Fp2> & Fp2Utils = {
ORDER: FP2_ORDER, ORDER: FP2_ORDER,
BITS: bitLen(FP2_ORDER), BITS: bitLen(FP2_ORDER),
BYTES: Math.ceil(bitLen(FP2_ORDER) / 8), BYTES: Math.ceil(bitLen(FP2_ORDER) / 8),
@@ -99,25 +141,24 @@ const Fp2: mod.Field<Fp2> & Fp2Utils = {
ONE: { c0: Fp.ONE, c1: Fp.ZERO }, ONE: { c0: Fp.ONE, c1: Fp.ZERO },
create: (num) => num, create: (num) => num,
isValid: ({ c0, c1 }) => typeof c0 === 'bigint' && typeof c1 === 'bigint', isValid: ({ c0, c1 }) => typeof c0 === 'bigint' && typeof c1 === 'bigint',
isZero: ({ c0, c1 }) => Fp.isZero(c0) && Fp.isZero(c1), is0: ({ c0, c1 }) => Fp.is0(c0) && Fp.is0(c1),
equals: ({ c0, c1 }: Fp2, { c0: r0, c1: r1 }: Fp2) => Fp.equals(c0, r0) && Fp.equals(c1, r1), eql: ({ c0, c1 }: Fp2, { c0: r0, c1: r1 }: Fp2) => Fp.eql(c0, r0) && Fp.eql(c1, r1),
negate: ({ c0, c1 }) => ({ c0: Fp.negate(c0), c1: Fp.negate(c1) }), neg: ({ c0, c1 }) => ({ c0: Fp.neg(c0), c1: Fp.neg(c1) }),
pow: (num, power) => mod.FpPow(Fp2, num, power), pow: (num, power) => mod.FpPow(Fp2, num, power),
invertBatch: (nums) => mod.FpInvertBatch(Fp2, nums), invertBatch: (nums) => mod.FpInvertBatch(Fp2, nums),
// Normalized // Normalized
add: Fp2Add, add: Fp2Add,
sub: Fp2Subtract, sub: Fp2Subtract,
mul: Fp2Multiply, mul: Fp2Multiply,
square: Fp2Square, sqr: Fp2Square,
// NonNormalized stuff // NonNormalized stuff
addN: Fp2Add, addN: Fp2Add,
subN: Fp2Subtract, subN: Fp2Subtract,
mulN: Fp2Multiply, mulN: Fp2Multiply,
squareN: Fp2Square, sqrN: Fp2Square,
// Why inversion for bigint inside Fp instead of Fp2? it is even used in that context? // Why inversion for bigint inside Fp instead of Fp2? it is even used in that context?
div: (lhs, rhs) => div: (lhs, rhs) => Fp2.mul(lhs, typeof rhs === 'bigint' ? Fp.inv(Fp.create(rhs)) : Fp2.inv(rhs)),
Fp2.mul(lhs, typeof rhs === 'bigint' ? Fp.invert(Fp.create(rhs)) : Fp2.invert(rhs)), inv: ({ c0: a, c1: b }) => {
invert: ({ c0: a, c1: b }) => {
// We wish to find the multiplicative inverse of a nonzero // We wish to find the multiplicative inverse of a nonzero
// element a + bu in Fp2. We leverage an identity // element a + bu in Fp2. We leverage an identity
// //
@@ -131,27 +172,27 @@ const Fp2: mod.Field<Fp2> & Fp2Utils = {
// This gives that (a - bu)/(a² + b²) is the inverse // This gives that (a - bu)/(a² + b²) is the inverse
// of (a + bu). Importantly, this can be computing using // of (a + bu). Importantly, this can be computing using
// only a single inversion in Fp. // only a single inversion in Fp.
const factor = Fp.invert(Fp.create(a * a + b * b)); const factor = Fp.inv(Fp.create(a * a + b * b));
return { c0: Fp.mul(factor, Fp.create(a)), c1: Fp.mul(factor, Fp.create(-b)) }; return { c0: Fp.mul(factor, Fp.create(a)), c1: Fp.mul(factor, Fp.create(-b)) };
}, },
sqrt: (num) => { sqrt: (num) => {
if (Fp2.equals(num, Fp2.ZERO)) return Fp2.ZERO; // Algo doesn't handles this case if (Fp2.eql(num, Fp2.ZERO)) return Fp2.ZERO; // Algo doesn't handles this case
// TODO: Optimize this line. It's extremely slow. // TODO: Optimize this line. It's extremely slow.
// Speeding this up would boost aggregateSignatures. // Speeding this up would boost aggregateSignatures.
// https://eprint.iacr.org/2012/685.pdf applicable? // https://eprint.iacr.org/2012/685.pdf applicable?
// https://github.com/zkcrypto/bls12_381/blob/080eaa74ec0e394377caa1ba302c8c121df08b07/src/fp2.rs#L250 // https://github.com/zkcrypto/bls12_381/blob/080eaa74ec0e394377caa1ba302c8c121df08b07/src/fp2.rs#L250
// https://github.com/supranational/blst/blob/aae0c7d70b799ac269ff5edf29d8191dbd357876/src/exp2.c#L1 // https://github.com/supranational/blst/blob/aae0c7d70b799ac269ff5edf29d8191dbd357876/src/exp2.c#L1
// Inspired by https://github.com/dalek-cryptography/curve25519-dalek/blob/17698df9d4c834204f83a3574143abacb4fc81a5/src/field.rs#L99 // Inspired by https://github.com/dalek-cryptography/curve25519-dalek/blob/17698df9d4c834204f83a3574143abacb4fc81a5/src/field.rs#L99
const candidateSqrt = Fp2.pow(num, (Fp2.ORDER + 8n) / 16n); const candidateSqrt = Fp2.pow(num, (Fp2.ORDER + _8n) / _16n);
const check = Fp2.div(Fp2.square(candidateSqrt), num); // candidateSqrt.square().div(this); const check = Fp2.div(Fp2.sqr(candidateSqrt), num); // candidateSqrt.square().div(this);
const R = FP2_ROOTS_OF_UNITY; const R = FP2_ROOTS_OF_UNITY;
const divisor = [R[0], R[2], R[4], R[6]].find((r) => Fp2.equals(r, check)); const divisor = [R[0], R[2], R[4], R[6]].find((r) => Fp2.eql(r, check));
if (!divisor) throw new Error('No root'); if (!divisor) throw new Error('No root');
const index = R.indexOf(divisor); const index = R.indexOf(divisor);
const root = R[index / 2]; const root = R[index / 2];
if (!root) throw new Error('Invalid root'); if (!root) throw new Error('Invalid root');
const x1 = Fp2.div(candidateSqrt, root); const x1 = Fp2.div(candidateSqrt, root);
const x2 = Fp2.negate(x1); const x2 = Fp2.neg(x1);
const { re: re1, im: im1 } = Fp2.reim(x1); const { re: re1, im: im1 } = Fp2.reim(x1);
const { re: re2, im: im2 } = Fp2.reim(x2); const { re: re2, im: im2 } = Fp2.reim(x2);
if (im1 > im2 || (im1 === im2 && re1 > re2)) return x1; if (im1 > im2 || (im1 === im2 && re1 > re2)) return x1;
@@ -160,17 +201,17 @@ const Fp2: mod.Field<Fp2> & Fp2Utils = {
// Same as sgn0_fp2 in draft-irtf-cfrg-hash-to-curve-16 // Same as sgn0_fp2 in draft-irtf-cfrg-hash-to-curve-16
isOdd: (x: Fp2) => { isOdd: (x: Fp2) => {
const { re: x0, im: x1 } = Fp2.reim(x); const { re: x0, im: x1 } = Fp2.reim(x);
const sign_0 = x0 % 2n; const sign_0 = x0 % _2n;
const zero_0 = x0 === 0n; const zero_0 = x0 === _0n;
const sign_1 = x1 % 2n; const sign_1 = x1 % _2n;
return BigInt(sign_0 || (zero_0 && sign_1)) == 1n; return BigInt(sign_0 || (zero_0 && sign_1)) == _1n;
}, },
// Bytes util // Bytes util
fromBytes(b: Uint8Array): Fp2 { fromBytes(b: Uint8Array): Fp2 {
if (b.length !== Fp2.BYTES) throw new Error(`fromBytes wrong length=${b.length}`); if (b.length !== Fp2.BYTES) throw new Error(`fromBytes wrong length=${b.length}`);
return { c0: Fp.fromBytes(b.subarray(0, Fp.BYTES)), c1: Fp.fromBytes(b.subarray(Fp.BYTES)) }; return { c0: Fp.fromBytes(b.subarray(0, Fp.BYTES)), c1: Fp.fromBytes(b.subarray(Fp.BYTES)) };
}, },
toBytes: ({ c0, c1 }) => concatBytes(Fp.toBytes(c0), Fp.toBytes(c1)), toBytes: ({ c0, c1 }) => concatB(Fp.toBytes(c0), Fp.toBytes(c1)),
cmov: ({ c0, c1 }, { c0: r0, c1: r1 }, c) => ({ cmov: ({ c0, c1 }, { c0: r0, c1: r1 }, c) => ({
c0: Fp.cmov(c0, r0, c), c0: Fp.cmov(c0, r0, c),
c1: Fp.cmov(c1, r1, c), c1: Fp.cmov(c1, r1, c),
@@ -183,8 +224,8 @@ const Fp2: mod.Field<Fp2> & Fp2Utils = {
// multiply by u + 1 // multiply by u + 1
mulByNonresidue: ({ c0, c1 }) => ({ c0: Fp.sub(c0, c1), c1: Fp.add(c0, c1) }), mulByNonresidue: ({ c0, c1 }) => ({ c0: Fp.sub(c0, c1), c1: Fp.add(c0, c1) }),
multiplyByB: ({ c0, c1 }) => { multiplyByB: ({ c0, c1 }) => {
let t0 = Fp.mul(c0, 4n); // 4 * c0 let t0 = Fp.mul(c0, _4n); // 4 * c0
let t1 = Fp.mul(c1, 4n); // 4 * c1 let t1 = Fp.mul(c1, _4n); // 4 * c1
// (T0-T1) + (T0+T1)*i // (T0-T1) + (T0+T1)*i
return { c0: Fp.sub(t0, t1), c1: Fp.add(t0, t1) }; return { c0: Fp.sub(t0, t1), c1: Fp.add(t0, t1) };
}, },
@@ -201,33 +242,36 @@ const Fp2: mod.Field<Fp2> & Fp2Utils = {
// Finite extension field over irreducible polynominal. // Finite extension field over irreducible polynominal.
// Fp(u) / (u² - β) where β = -1 // Fp(u) / (u² - β) where β = -1
const FP2_FROBENIUS_COEFFICIENTS = [ const FP2_FROBENIUS_COEFFICIENTS = [
0x1n, BigInt('0x1'),
0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaan, BigInt(
'0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa'
),
].map((item) => Fp.create(item)); ].map((item) => Fp.create(item));
// For Fp2 roots of unity. // For Fp2 roots of unity.
const rv1 = const rv1 = BigInt(
0x6af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09n; '0x6af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09'
);
// const ev1 = // const ev1 =
// 0x699be3b8c6870965e5bf892ad5d2cc7b0e85a117402dfd83b7f4a947e02d978498255a2aaec0ac627b5afbdf1bf1c90n; // BigInt('0x699be3b8c6870965e5bf892ad5d2cc7b0e85a117402dfd83b7f4a947e02d978498255a2aaec0ac627b5afbdf1bf1c90');
// const ev2 = // const ev2 =
// 0x8157cd83046453f5dd0972b6e3949e4288020b5b8a9cc99ca07e27089a2ce2436d965026adad3ef7baba37f2183e9b5n; // BigInt('0x8157cd83046453f5dd0972b6e3949e4288020b5b8a9cc99ca07e27089a2ce2436d965026adad3ef7baba37f2183e9b5');
// const ev3 = // const ev3 =
// 0xab1c2ffdd6c253ca155231eb3e71ba044fd562f6f72bc5bad5ec46a0b7a3b0247cf08ce6c6317f40edbc653a72dee17n; // BigInt('0xab1c2ffdd6c253ca155231eb3e71ba044fd562f6f72bc5bad5ec46a0b7a3b0247cf08ce6c6317f40edbc653a72dee17');
// const ev4 = // const ev4 =
// 0xaa404866706722864480885d68ad0ccac1967c7544b447873cc37e0181271e006df72162a3d3e0287bf597fbf7f8fc1n; // BigInt('0xaa404866706722864480885d68ad0ccac1967c7544b447873cc37e0181271e006df72162a3d3e0287bf597fbf7f8fc1');
// Eighth roots of unity, used for computing square roots in Fp2. // Eighth roots of unity, used for computing square roots in Fp2.
// To verify or re-calculate: // To verify or re-calculate:
// Array(8).fill(new Fp2([1n, 1n])).map((fp2, k) => fp2.pow(Fp2.ORDER * BigInt(k) / 8n)) // Array(8).fill(new Fp2([1n, 1n])).map((fp2, k) => fp2.pow(Fp2.ORDER * BigInt(k) / 8n))
const FP2_ROOTS_OF_UNITY = [ const FP2_ROOTS_OF_UNITY = [
[1n, 0n], [_1n, _0n],
[rv1, -rv1], [rv1, -rv1],
[0n, 1n], [_0n, _1n],
[rv1, rv1], [rv1, rv1],
[-1n, 0n], [-_1n, _0n],
[-rv1, rv1], [-rv1, rv1],
[0n, -1n], [_0n, -_1n],
[-rv1, -rv1], [-rv1, -rv1],
].map((pair) => Fp2.fromBigTuple(pair)); ].map((pair) => Fp2.fromBigTuple(pair));
// eta values, used for computing sqrt(g(X1(t))) // eta values, used for computing sqrt(g(X1(t)))
@@ -280,18 +324,15 @@ const Fp6Multiply = ({ c0, c1, c2 }: Fp6, rhs: Fp6 | bigint) => {
}; };
}; };
const Fp6Square = ({ c0, c1, c2 }: Fp6) => { const Fp6Square = ({ c0, c1, c2 }: Fp6) => {
let t0 = Fp2.square(c0); // c0² let t0 = Fp2.sqr(c0); // c0²
let t1 = Fp2.mul(Fp2.mul(c0, c1), 2n); // 2 * c0 * c1 let t1 = Fp2.mul(Fp2.mul(c0, c1), _2n); // 2 * c0 * c1
let t3 = Fp2.mul(Fp2.mul(c1, c2), 2n); // 2 * c1 * c2 let t3 = Fp2.mul(Fp2.mul(c1, c2), _2n); // 2 * c1 * c2
let t4 = Fp2.square(c2); // c2² let t4 = Fp2.sqr(c2); // c2²
return { return {
c0: Fp2.add(Fp2.mulByNonresidue(t3), t0), // T3 * (u + 1) + T0 c0: Fp2.add(Fp2.mulByNonresidue(t3), t0), // T3 * (u + 1) + T0
c1: Fp2.add(Fp2.mulByNonresidue(t4), t1), // T4 * (u + 1) + T1 c1: Fp2.add(Fp2.mulByNonresidue(t4), t1), // T4 * (u + 1) + T1
// T1 + (c0 - c1 + c2)² + T3 - T0 - T4 // T1 + (c0 - c1 + c2)² + T3 - T0 - T4
c2: Fp2.sub( c2: Fp2.sub(Fp2.sub(Fp2.add(Fp2.add(t1, Fp2.sqr(Fp2.add(Fp2.sub(c0, c1), c2))), t3), t0), t4),
Fp2.sub(Fp2.add(Fp2.add(t1, Fp2.square(Fp2.add(Fp2.sub(c0, c1), c2))), t3), t0),
t4
),
}; };
}; };
type Fp6Utils = { type Fp6Utils = {
@@ -303,7 +344,7 @@ type Fp6Utils = {
multiplyByFp2(lhs: Fp6, rhs: Fp2): Fp6; multiplyByFp2(lhs: Fp6, rhs: Fp2): Fp6;
}; };
const Fp6: mod.Field<Fp6> & Fp6Utils = { const Fp6: mod.IField<Fp6> & Fp6Utils = {
ORDER: Fp2.ORDER, // TODO: unused, but need to verify ORDER: Fp2.ORDER, // TODO: unused, but need to verify
BITS: 3 * Fp2.BITS, BITS: 3 * Fp2.BITS,
BYTES: 3 * Fp2.BYTES, BYTES: 3 * Fp2.BYTES,
@@ -312,35 +353,34 @@ const Fp6: mod.Field<Fp6> & Fp6Utils = {
ONE: { c0: Fp2.ONE, c1: Fp2.ZERO, c2: Fp2.ZERO }, ONE: { c0: Fp2.ONE, c1: Fp2.ZERO, c2: Fp2.ZERO },
create: (num) => num, create: (num) => num,
isValid: ({ c0, c1, c2 }) => Fp2.isValid(c0) && Fp2.isValid(c1) && Fp2.isValid(c2), isValid: ({ c0, c1, c2 }) => Fp2.isValid(c0) && Fp2.isValid(c1) && Fp2.isValid(c2),
isZero: ({ c0, c1, c2 }) => Fp2.isZero(c0) && Fp2.isZero(c1) && Fp2.isZero(c2), is0: ({ c0, c1, c2 }) => Fp2.is0(c0) && Fp2.is0(c1) && Fp2.is0(c2),
negate: ({ c0, c1, c2 }) => ({ c0: Fp2.negate(c0), c1: Fp2.negate(c1), c2: Fp2.negate(c2) }), neg: ({ c0, c1, c2 }) => ({ c0: Fp2.neg(c0), c1: Fp2.neg(c1), c2: Fp2.neg(c2) }),
equals: ({ c0, c1, c2 }, { c0: r0, c1: r1, c2: r2 }) => eql: ({ c0, c1, c2 }, { c0: r0, c1: r1, c2: r2 }) =>
Fp2.equals(c0, r0) && Fp2.equals(c1, r1) && Fp2.equals(c2, r2), Fp2.eql(c0, r0) && Fp2.eql(c1, r1) && Fp2.eql(c2, r2),
sqrt: () => { sqrt: () => {
throw new Error('Not implemented'); throw new Error('Not implemented');
}, },
// Do we need division by bigint at all? Should be done via order: // Do we need division by bigint at all? Should be done via order:
div: (lhs, rhs) => div: (lhs, rhs) => Fp6.mul(lhs, typeof rhs === 'bigint' ? Fp.inv(Fp.create(rhs)) : Fp6.inv(rhs)),
Fp6.mul(lhs, typeof rhs === 'bigint' ? Fp.invert(Fp.create(rhs)) : Fp6.invert(rhs)),
pow: (num, power) => mod.FpPow(Fp6, num, power), pow: (num, power) => mod.FpPow(Fp6, num, power),
invertBatch: (nums) => mod.FpInvertBatch(Fp6, nums), invertBatch: (nums) => mod.FpInvertBatch(Fp6, nums),
// Normalized // Normalized
add: Fp6Add, add: Fp6Add,
sub: Fp6Subtract, sub: Fp6Subtract,
mul: Fp6Multiply, mul: Fp6Multiply,
square: Fp6Square, sqr: Fp6Square,
// NonNormalized stuff // NonNormalized stuff
addN: Fp6Add, addN: Fp6Add,
subN: Fp6Subtract, subN: Fp6Subtract,
mulN: Fp6Multiply, mulN: Fp6Multiply,
squareN: Fp6Square, sqrN: Fp6Square,
invert: ({ c0, c1, c2 }) => { inv: ({ c0, c1, c2 }) => {
let t0 = Fp2.sub(Fp2.square(c0), Fp2.mulByNonresidue(Fp2.mul(c2, c1))); // c0² - c2 * c1 * (u + 1) let t0 = Fp2.sub(Fp2.sqr(c0), Fp2.mulByNonresidue(Fp2.mul(c2, c1))); // c0² - c2 * c1 * (u + 1)
let t1 = Fp2.sub(Fp2.mulByNonresidue(Fp2.square(c2)), Fp2.mul(c0, c1)); // c2² * (u + 1) - c0 * c1 let t1 = Fp2.sub(Fp2.mulByNonresidue(Fp2.sqr(c2)), Fp2.mul(c0, c1)); // c2² * (u + 1) - c0 * c1
let t2 = Fp2.sub(Fp2.square(c1), Fp2.mul(c0, c2)); // c1² - c0 * c2 let t2 = Fp2.sub(Fp2.sqr(c1), Fp2.mul(c0, c2)); // c1² - c0 * c2
// 1/(((c2 * T1 + c1 * T2) * v) + c0 * T0) // 1/(((c2 * T1 + c1 * T2) * v) + c0 * T0)
let t4 = Fp2.invert( let t4 = Fp2.inv(
Fp2.add(Fp2.mulByNonresidue(Fp2.add(Fp2.mul(c2, t1), Fp2.mul(c1, t2))), Fp2.mul(c0, t0)) Fp2.add(Fp2.mulByNonresidue(Fp2.add(Fp2.mul(c2, t1), Fp2.mul(c1, t2))), Fp2.mul(c0, t0))
); );
return { c0: Fp2.mul(t4, t0), c1: Fp2.mul(t4, t1), c2: Fp2.mul(t4, t2) }; return { c0: Fp2.mul(t4, t0), c1: Fp2.mul(t4, t1), c2: Fp2.mul(t4, t2) };
@@ -355,7 +395,7 @@ const Fp6: mod.Field<Fp6> & Fp6Utils = {
}; };
}, },
toBytes: ({ c0, c1, c2 }): Uint8Array => toBytes: ({ c0, c1, c2 }): Uint8Array =>
concatBytes(Fp2.toBytes(c0), Fp2.toBytes(c1), Fp2.toBytes(c2)), concatB(Fp2.toBytes(c0), Fp2.toBytes(c1), Fp2.toBytes(c2)),
cmov: ({ c0, c1, c2 }: Fp6, { c0: r0, c1: r1, c2: r2 }: Fp6, c) => ({ cmov: ({ c0, c1, c2 }: Fp6, { c0: r0, c1: r1, c2: r2 }: Fp6, c) => ({
c0: Fp2.cmov(c0, r0, c), c0: Fp2.cmov(c0, r0, c),
c1: Fp2.cmov(c1, r1, c), c1: Fp2.cmov(c1, r1, c),
@@ -411,46 +451,64 @@ const Fp6: mod.Field<Fp6> & Fp6Utils = {
}; };
const FP6_FROBENIUS_COEFFICIENTS_1 = [ const FP6_FROBENIUS_COEFFICIENTS_1 = [
[0x1n, 0x0n], [BigInt('0x1'), BigInt('0x0')],
[ [
0x0n, BigInt('0x0'),
0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaacn, BigInt(
'0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac'
),
], ],
[ [
0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffen, BigInt(
0x0n, '0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe'
),
BigInt('0x0'),
], ],
[0x0n, 0x1n], [BigInt('0x0'), BigInt('0x1')],
[ [
0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaacn, BigInt(
0x0n, '0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac'
),
BigInt('0x0'),
], ],
[ [
0x0n, BigInt('0x0'),
0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffen, BigInt(
'0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe'
),
], ],
].map((pair) => Fp2.fromBigTuple(pair)); ].map((pair) => Fp2.fromBigTuple(pair));
const FP6_FROBENIUS_COEFFICIENTS_2 = [ const FP6_FROBENIUS_COEFFICIENTS_2 = [
[0x1n, 0x0n], [BigInt('0x1'), BigInt('0x0')],
[ [
0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaadn, BigInt(
0x0n, '0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaad'
),
BigInt('0x0'),
], ],
[ [
0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaacn, BigInt(
0x0n, '0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac'
),
BigInt('0x0'),
], ],
[ [
0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaan, BigInt(
0x0n, '0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa'
),
BigInt('0x0'),
], ],
[ [
0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffen, BigInt(
0x0n, '0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe'
),
BigInt('0x0'),
], ],
[ [
0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffeffffn, BigInt(
0x0n, '0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffeffff'
),
BigInt('0x0'),
], ],
].map((pair) => Fp2.fromBigTuple(pair)); ].map((pair) => Fp2.fromBigTuple(pair));
@@ -459,7 +517,7 @@ const FP6_FROBENIUS_COEFFICIENTS_2 = [
// Fp₆(w) / (w² - γ) where γ = v // Fp₆(w) / (w² - γ) where γ = v
type Fp12 = { c0: Fp6; c1: Fp6 }; type Fp12 = { c0: Fp6; c1: Fp6 };
// The BLS parameter x for BLS12-381 // The BLS parameter x for BLS12-381
const BLS_X = 0xd201000000010000n; const BLS_X = BigInt('0xd201000000010000');
const BLS_X_LEN = bitLen(BLS_X); const BLS_X_LEN = bitLen(BLS_X);
// prettier-ignore // prettier-ignore
@@ -498,11 +556,11 @@ const Fp12Square = ({ c0, c1 }: Fp12) => {
}; // AB + AB }; // AB + AB
}; };
function Fp4Square(a: Fp2, b: Fp2): { first: Fp2; second: Fp2 } { function Fp4Square(a: Fp2, b: Fp2): { first: Fp2; second: Fp2 } {
const a2 = Fp2.square(a); const a2 = Fp2.sqr(a);
const b2 = Fp2.square(b); const b2 = Fp2.sqr(b);
return { return {
first: Fp2.add(Fp2.mulByNonresidue(b2), a2), // b² * Nonresidue + a² first: Fp2.add(Fp2.mulByNonresidue(b2), a2), // b² * Nonresidue + a²
second: Fp2.sub(Fp2.sub(Fp2.square(Fp2.add(a, b)), a2), b2), // (a + b)² - a² - b² second: Fp2.sub(Fp2.sub(Fp2.sqr(Fp2.add(a, b)), a2), b2), // (a + b)² - a² - b²
}; };
} }
type Fp12Utils = { type Fp12Utils = {
@@ -516,7 +574,7 @@ type Fp12Utils = {
_cyclotomicExp(num: Fp12, n: bigint): Fp12; _cyclotomicExp(num: Fp12, n: bigint): Fp12;
}; };
const Fp12: mod.Field<Fp12> & Fp12Utils = { const Fp12: mod.IField<Fp12> & Fp12Utils = {
ORDER: Fp2.ORDER, // TODO: unused, but need to verify ORDER: Fp2.ORDER, // TODO: unused, but need to verify
BITS: 2 * Fp2.BITS, BITS: 2 * Fp2.BITS,
BYTES: 2 * Fp2.BYTES, BYTES: 2 * Fp2.BYTES,
@@ -525,30 +583,30 @@ const Fp12: mod.Field<Fp12> & Fp12Utils = {
ONE: { c0: Fp6.ONE, c1: Fp6.ZERO }, ONE: { c0: Fp6.ONE, c1: Fp6.ZERO },
create: (num) => num, create: (num) => num,
isValid: ({ c0, c1 }) => Fp6.isValid(c0) && Fp6.isValid(c1), isValid: ({ c0, c1 }) => Fp6.isValid(c0) && Fp6.isValid(c1),
isZero: ({ c0, c1 }) => Fp6.isZero(c0) && Fp6.isZero(c1), is0: ({ c0, c1 }) => Fp6.is0(c0) && Fp6.is0(c1),
negate: ({ c0, c1 }) => ({ c0: Fp6.negate(c0), c1: Fp6.negate(c1) }), neg: ({ c0, c1 }) => ({ c0: Fp6.neg(c0), c1: Fp6.neg(c1) }),
equals: ({ c0, c1 }, { c0: r0, c1: r1 }) => Fp6.equals(c0, r0) && Fp6.equals(c1, r1), eql: ({ c0, c1 }, { c0: r0, c1: r1 }) => Fp6.eql(c0, r0) && Fp6.eql(c1, r1),
sqrt: () => { sqrt: () => {
throw new Error('Not implemented'); throw new Error('Not implemented');
}, },
invert: ({ c0, c1 }) => { inv: ({ c0, c1 }) => {
let t = Fp6.invert(Fp6.sub(Fp6.square(c0), Fp6.mulByNonresidue(Fp6.square(c1)))); // 1 / (c0² - c1² * v) let t = Fp6.inv(Fp6.sub(Fp6.sqr(c0), Fp6.mulByNonresidue(Fp6.sqr(c1)))); // 1 / (c0² - c1² * v)
return { c0: Fp6.mul(c0, t), c1: Fp6.negate(Fp6.mul(c1, t)) }; // ((C0 * T) * T) + (-C1 * T) * w return { c0: Fp6.mul(c0, t), c1: Fp6.neg(Fp6.mul(c1, t)) }; // ((C0 * T) * T) + (-C1 * T) * w
}, },
div: (lhs, rhs) => div: (lhs, rhs) =>
Fp12.mul(lhs, typeof rhs === 'bigint' ? Fp.invert(Fp.create(rhs)) : Fp12.invert(rhs)), Fp12.mul(lhs, typeof rhs === 'bigint' ? Fp.inv(Fp.create(rhs)) : Fp12.inv(rhs)),
pow: (num, power) => mod.FpPow(Fp12, num, power), pow: (num, power) => mod.FpPow(Fp12, num, power),
invertBatch: (nums) => mod.FpInvertBatch(Fp12, nums), invertBatch: (nums) => mod.FpInvertBatch(Fp12, nums),
// Normalized // Normalized
add: Fp12Add, add: Fp12Add,
sub: Fp12Subtract, sub: Fp12Subtract,
mul: Fp12Multiply, mul: Fp12Multiply,
square: Fp12Square, sqr: Fp12Square,
// NonNormalized stuff // NonNormalized stuff
addN: Fp12Add, addN: Fp12Add,
subN: Fp12Subtract, subN: Fp12Subtract,
mulN: Fp12Multiply, mulN: Fp12Multiply,
squareN: Fp12Square, sqrN: Fp12Square,
// Bytes utils // Bytes utils
fromBytes: (b: Uint8Array): Fp12 => { fromBytes: (b: Uint8Array): Fp12 => {
@@ -558,7 +616,7 @@ const Fp12: mod.Field<Fp12> & Fp12Utils = {
c1: Fp6.fromBytes(b.subarray(Fp6.BYTES)), c1: Fp6.fromBytes(b.subarray(Fp6.BYTES)),
}; };
}, },
toBytes: ({ c0, c1 }): Uint8Array => concatBytes(Fp6.toBytes(c0), Fp6.toBytes(c1)), toBytes: ({ c0, c1 }): Uint8Array => concatB(Fp6.toBytes(c0), Fp6.toBytes(c1)),
cmov: ({ c0, c1 }, { c0: r0, c1: r1 }, c) => ({ cmov: ({ c0, c1 }, { c0: r0, c1: r1 }, c) => ({
c0: Fp6.cmov(c0, r0, c), c0: Fp6.cmov(c0, r0, c),
c1: Fp6.cmov(c1, r1, c), c1: Fp6.cmov(c1, r1, c),
@@ -602,7 +660,7 @@ const Fp12: mod.Field<Fp12> & Fp12Utils = {
c0: Fp6.multiplyByFp2(c0, rhs), c0: Fp6.multiplyByFp2(c0, rhs),
c1: Fp6.multiplyByFp2(c1, rhs), c1: Fp6.multiplyByFp2(c1, rhs),
}), }),
conjugate: ({ c0, c1 }): Fp12 => ({ c0, c1: Fp6.negate(c1) }), conjugate: ({ c0, c1 }): Fp12 => ({ c0, c1: Fp6.neg(c1) }),
// A cyclotomic group is a subgroup of Fp^n defined by // A cyclotomic group is a subgroup of Fp^n defined by
// GΦₙ(p) = {α ∈ Fpⁿ : α^Φₙ(p) = 1} // GΦₙ(p) = {α ∈ Fpⁿ : α^Φₙ(p) = 1}
@@ -617,14 +675,14 @@ const Fp12: mod.Field<Fp12> & Fp12Utils = {
let t9 = Fp2.mulByNonresidue(t8); // T8 * (u + 1) let t9 = Fp2.mulByNonresidue(t8); // T8 * (u + 1)
return { return {
c0: Fp6.create({ c0: Fp6.create({
c0: Fp2.add(Fp2.mul(Fp2.sub(t3, c0c0), 2n), t3), // 2 * (T3 - c0c0) + T3 c0: Fp2.add(Fp2.mul(Fp2.sub(t3, c0c0), _2n), t3), // 2 * (T3 - c0c0) + T3
c1: Fp2.add(Fp2.mul(Fp2.sub(t5, c0c1), 2n), t5), // 2 * (T5 - c0c1) + T5 c1: Fp2.add(Fp2.mul(Fp2.sub(t5, c0c1), _2n), t5), // 2 * (T5 - c0c1) + T5
c2: Fp2.add(Fp2.mul(Fp2.sub(t7, c0c2), 2n), t7), c2: Fp2.add(Fp2.mul(Fp2.sub(t7, c0c2), _2n), t7),
}), // 2 * (T7 - c0c2) + T7 }), // 2 * (T7 - c0c2) + T7
c1: Fp6.create({ c1: Fp6.create({
c0: Fp2.add(Fp2.mul(Fp2.add(t9, c1c0), 2n), t9), // 2 * (T9 + c1c0) + T9 c0: Fp2.add(Fp2.mul(Fp2.add(t9, c1c0), _2n), t9), // 2 * (T9 + c1c0) + T9
c1: Fp2.add(Fp2.mul(Fp2.add(t4, c1c1), 2n), t4), // 2 * (T4 + c1c1) + T4 c1: Fp2.add(Fp2.mul(Fp2.add(t4, c1c1), _2n), t4), // 2 * (T4 + c1c1) + T4
c2: Fp2.add(Fp2.mul(Fp2.add(t6, c1c2), 2n), t6), c2: Fp2.add(Fp2.mul(Fp2.add(t6, c1c2), _2n), t6),
}), }),
}; // 2 * (T6 + c1c2) + T6 }; // 2 * (T6 + c1c2) + T6
}, },
@@ -659,50 +717,84 @@ const Fp12: mod.Field<Fp12> & Fp12Utils = {
}, },
}; };
const FP12_FROBENIUS_COEFFICIENTS = [ const FP12_FROBENIUS_COEFFICIENTS = [
[0x1n, 0x0n], [BigInt('0x1'), BigInt('0x0')],
[ [
0x1904d3bf02bb0667c231beb4202c0d1f0fd603fd3cbd5f4f7b2443d784bab9c4f67ea53d63e7813d8d0775ed92235fb8n, BigInt(
0x00fc3e2b36c4e03288e9e902231f9fb854a14787b6c7b36fec0c8ec971f63c5f282d5ac14d6c7ec22cf78a126ddc4af3n, '0x1904d3bf02bb0667c231beb4202c0d1f0fd603fd3cbd5f4f7b2443d784bab9c4f67ea53d63e7813d8d0775ed92235fb8'
),
BigInt(
'0x00fc3e2b36c4e03288e9e902231f9fb854a14787b6c7b36fec0c8ec971f63c5f282d5ac14d6c7ec22cf78a126ddc4af3'
),
], ],
[ [
0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffeffffn, BigInt(
0x0n, '0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffeffff'
),
BigInt('0x0'),
], ],
[ [
0x135203e60180a68ee2e9c448d77a2cd91c3dedd930b1cf60ef396489f61eb45e304466cf3e67fa0af1ee7b04121bdea2n, BigInt(
0x06af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09n, '0x135203e60180a68ee2e9c448d77a2cd91c3dedd930b1cf60ef396489f61eb45e304466cf3e67fa0af1ee7b04121bdea2'
),
BigInt(
'0x06af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09'
),
], ],
[ [
0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffen, BigInt(
0x0n, '0x00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe'
),
BigInt('0x0'),
], ],
[ [
0x144e4211384586c16bd3ad4afa99cc9170df3560e77982d0db45f3536814f0bd5871c1908bd478cd1ee605167ff82995n, BigInt(
0x05b2cfd9013a5fd8df47fa6b48b1e045f39816240c0b8fee8beadf4d8e9c0566c63a3e6e257f87329b18fae980078116n, '0x144e4211384586c16bd3ad4afa99cc9170df3560e77982d0db45f3536814f0bd5871c1908bd478cd1ee605167ff82995'
),
BigInt(
'0x05b2cfd9013a5fd8df47fa6b48b1e045f39816240c0b8fee8beadf4d8e9c0566c63a3e6e257f87329b18fae980078116'
),
], ],
[ [
0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaan, BigInt(
0x0n, '0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa'
),
BigInt('0x0'),
], ],
[ [
0x00fc3e2b36c4e03288e9e902231f9fb854a14787b6c7b36fec0c8ec971f63c5f282d5ac14d6c7ec22cf78a126ddc4af3n, BigInt(
0x1904d3bf02bb0667c231beb4202c0d1f0fd603fd3cbd5f4f7b2443d784bab9c4f67ea53d63e7813d8d0775ed92235fb8n, '0x00fc3e2b36c4e03288e9e902231f9fb854a14787b6c7b36fec0c8ec971f63c5f282d5ac14d6c7ec22cf78a126ddc4af3'
),
BigInt(
'0x1904d3bf02bb0667c231beb4202c0d1f0fd603fd3cbd5f4f7b2443d784bab9c4f67ea53d63e7813d8d0775ed92235fb8'
),
], ],
[ [
0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaacn, BigInt(
0x0n, '0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac'
),
BigInt('0x0'),
], ],
[ [
0x06af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09n, BigInt(
0x135203e60180a68ee2e9c448d77a2cd91c3dedd930b1cf60ef396489f61eb45e304466cf3e67fa0af1ee7b04121bdea2n, '0x06af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09'
),
BigInt(
'0x135203e60180a68ee2e9c448d77a2cd91c3dedd930b1cf60ef396489f61eb45e304466cf3e67fa0af1ee7b04121bdea2'
),
], ],
[ [
0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaadn, BigInt(
0x0n, '0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaad'
),
BigInt('0x0'),
], ],
[ [
0x05b2cfd9013a5fd8df47fa6b48b1e045f39816240c0b8fee8beadf4d8e9c0566c63a3e6e257f87329b18fae980078116n, BigInt(
0x144e4211384586c16bd3ad4afa99cc9170df3560e77982d0db45f3536814f0bd5871c1908bd478cd1ee605167ff82995n, '0x05b2cfd9013a5fd8df47fa6b48b1e045f39816240c0b8fee8beadf4d8e9c0566c63a3e6e257f87329b18fae980078116'
),
BigInt(
'0x144e4211384586c16bd3ad4afa99cc9170df3560e77982d0db45f3536814f0bd5871c1908bd478cd1ee605167ff82995'
),
], ],
].map((n) => Fp2.fromBigTuple(n)); ].map((n) => Fp2.fromBigTuple(n));
// END OF CURVE FIELDS // END OF CURVE FIELDS
@@ -858,17 +950,21 @@ const isogenyMapG1 = isogenyMap(
// SWU Map - Fp2 to G2': y² = x³ + 240i * x + 1012 + 1012i // SWU Map - Fp2 to G2': y² = x³ + 240i * x + 1012 + 1012i
const G2_SWU = mapToCurveSimpleSWU(Fp2, { const G2_SWU = mapToCurveSimpleSWU(Fp2, {
A: Fp2.create({ c0: Fp.create(0n), c1: Fp.create(240n) }), // A' = 240 * I A: Fp2.create({ c0: Fp.create(_0n), c1: Fp.create(240n) }), // A' = 240 * I
B: Fp2.create({ c0: Fp.create(1012n), c1: Fp.create(1012n) }), // B' = 1012 * (1 + I) B: Fp2.create({ c0: Fp.create(1012n), c1: Fp.create(1012n) }), // B' = 1012 * (1 + I)
Z: Fp2.create({ c0: Fp.create(-2n), c1: Fp.create(-1n) }), // Z: -(2 + I) Z: Fp2.create({ c0: Fp.create(-2n), c1: Fp.create(-1n) }), // Z: -(2 + I)
}); });
// Optimized SWU Map - Fp to G1 // Optimized SWU Map - Fp to G1
const G1_SWU = mapToCurveSimpleSWU(Fp, { const G1_SWU = mapToCurveSimpleSWU(Fp, {
A: Fp.create( A: Fp.create(
0x144698a3b8e9433d693a02c96d4982b0ea985383ee66a8d8e8981aefd881ac98936f8da0e0f97f5cf428082d584c1dn BigInt(
'0x144698a3b8e9433d693a02c96d4982b0ea985383ee66a8d8e8981aefd881ac98936f8da0e0f97f5cf428082d584c1d'
)
), ),
B: Fp.create( B: Fp.create(
0x12e2908d11688030018b12e8753eee3b2016c1f0f24f4070a0b9c14fcef35ef55a23215a316ceaa5d1cc48e98e172be0n BigInt(
'0x12e2908d11688030018b12e8753eee3b2016c1f0f24f4070a0b9c14fcef35ef55a23215a316ceaa5d1cc48e98e172be0'
)
), ),
Z: Fp.create(11n), Z: Fp.create(11n),
}); });
@@ -886,20 +982,21 @@ function psi(x: Fp2, y: Fp2): [Fp2, Fp2] {
return [x2, y2]; return [x2, y2];
} }
// Ψ endomorphism // Ψ endomorphism
function G2psi(c: ProjectiveConstructor<Fp2>, P: ProjectivePointType<Fp2>) { function G2psi(c: ProjConstructor<Fp2>, P: ProjPointType<Fp2>) {
const affine = P.toAffine(); const affine = P.toAffine();
const p = psi(affine.x, affine.y); const p = psi(affine.x, affine.y);
return new c(p[0], p[1], Fp2.ONE); return new c(p[0], p[1], Fp2.ONE);
} }
// Ψ²(P) endomorphism // Ψ²(P) endomorphism
// 1 / F2(2)^((p-1)/3) in GF(p²) // 1 / F2(2)^((p-1)/3) in GF(p²)
const PSI2_C1 = const PSI2_C1 = BigInt(
0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaacn; '0x1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaac'
);
function psi2(x: Fp2, y: Fp2): [Fp2, Fp2] { function psi2(x: Fp2, y: Fp2): [Fp2, Fp2] {
return [Fp2.mul(x, PSI2_C1), Fp2.negate(y)]; return [Fp2.mul(x, PSI2_C1), Fp2.neg(y)];
} }
function G2psi2(c: ProjectiveConstructor<Fp2>, P: ProjectivePointType<Fp2>) { function G2psi2(c: ProjConstructor<Fp2>, P: ProjPointType<Fp2>) {
const affine = P.toAffine(); const affine = P.toAffine();
const p = psi2(affine.x, affine.y); const p = psi2(affine.x, affine.y);
return new c(p[0], p[1], Fp2.ONE); return new c(p[0], p[1], Fp2.ONE);
@@ -915,11 +1012,12 @@ function G2psi2(c: ProjectiveConstructor<Fp2>, P: ProjectivePointType<Fp2>) {
// p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab // p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
// m = 2 (or 1 for G1 see section 8.8.1) // m = 2 (or 1 for G1 see section 8.8.1)
// k = 128 // k = 128
const htfDefaults = { const htfDefaults = Object.freeze({
// DST: a domain separation tag // DST: a domain separation tag
// defined in section 2.2.5 // defined in section 2.2.5
// Use utils.getDSTLabel(), utils.setDSTLabel(value) // Use utils.getDSTLabel(), utils.setDSTLabel(value)
DST: 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_', DST: 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_',
encodeDST: 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_',
// p: the characteristic of F // p: the characteristic of F
// where F is a finite field of characteristic p and order q = p^m // where F is a finite field of characteristic p and order q = p^m
p: Fp.ORDER, p: Fp.ORDER,
@@ -936,7 +1034,7 @@ const htfDefaults = {
// wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others. // wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others.
// BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247 // BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247
hash: sha256, hash: sha256,
} as const; } as const);
// Encoding utils // Encoding utils
// Point on G1 curve: (x, y) // Point on G1 curve: (x, y)
@@ -970,14 +1068,18 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
G1: { G1: {
Fp, Fp,
// cofactor; (z - 1)²/3 // cofactor; (z - 1)²/3
h: 0x396c8c005555e1568c00aaab0000aaabn, h: BigInt('0x396c8c005555e1568c00aaab0000aaab'),
// generator's coordinates // generator's coordinates
// x = 3685416753713387016781088315183077757961620795782546409894578378688607592378376318836054947676345821548104185464507 // x = 3685416753713387016781088315183077757961620795782546409894578378688607592378376318836054947676345821548104185464507
// y = 1339506544944476473020471379941921221584933875938349620426543736416511423956333506472724655353366534992391756441569 // y = 1339506544944476473020471379941921221584933875938349620426543736416511423956333506472724655353366534992391756441569
Gx: 0x17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bbn, Gx: BigInt(
Gy: 0x08b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1n, '0x17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb'
),
Gy: BigInt(
'0x08b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1'
),
a: Fp.ZERO, a: Fp.ZERO,
b: 4n, b: _4n,
htfDefaults: { ...htfDefaults, m: 1 }, htfDefaults: { ...htfDefaults, m: 1 },
wrapPrivateKey: true, wrapPrivateKey: true,
allowInfinityPoint: true, allowInfinityPoint: true,
@@ -987,9 +1089,10 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
// https://eprint.iacr.org/2021/1130.pdf // https://eprint.iacr.org/2021/1130.pdf
isTorsionFree: (c, point): boolean => { isTorsionFree: (c, point): boolean => {
// φ endomorphism // φ endomorphism
const cubicRootOfUnityModP = const cubicRootOfUnityModP = BigInt(
0x5f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffen; '0x5f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe'
const phi = new c(Fp.mul(point.x, cubicRootOfUnityModP), point.y, point.z); );
const phi = new c(Fp.mul(point.px, cubicRootOfUnityModP), point.py, point.pz);
// todo: unroll // todo: unroll
const xP = point.multiplyUnsafe(bls12_381.CURVE.x).negate(); // [x]P const xP = point.multiplyUnsafe(bls12_381.CURVE.x).negate(); // [x]P
@@ -998,7 +1101,7 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
// https://eprint.iacr.org/2019/814.pdf // https://eprint.iacr.org/2019/814.pdf
// (z² 1)/3 // (z² 1)/3
// const c1 = 0x396c8c005555e1560000000055555555n; // const c1 = BigInt('0x396c8c005555e1560000000055555555');
// const P = this; // const P = this;
// const S = P.sigma(); // const S = P.sigma();
// const Q = S.double(); // const Q = S.double();
@@ -1018,23 +1121,23 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
const { x, y } = G1_SWU(Fp.create(scalars[0])); const { x, y } = G1_SWU(Fp.create(scalars[0]));
return isogenyMapG1(x, y); return isogenyMapG1(x, y);
}, },
fromBytes: (bytes: Uint8Array): { x: Fp; y: Fp } => { fromBytes: (bytes: Uint8Array): AffinePoint<Fp> => {
if (bytes.length === 48) { if (bytes.length === 48) {
const P = Fp.ORDER; const P = Fp.ORDER;
const compressedValue = bytesToNumberBE(bytes); const compressedValue = bytesToNumberBE(bytes);
const bflag = bitGet(compressedValue, I_BIT_POS); const bflag = bitGet(compressedValue, I_BIT_POS);
// Zero // Zero
if (bflag === 1n) return { x: 0n, y: 0n }; if (bflag === _1n) return { x: _0n, y: _0n };
const x = Fp.create(compressedValue & Fp.MASK); const x = Fp.create(compressedValue & Fp.MASK);
const right = Fp.add(Fp.pow(x, 3n), Fp.create(bls12_381.CURVE.G1.b)); // y² = x³ + b const right = Fp.add(Fp.pow(x, _3n), Fp.create(bls12_381.CURVE.G1.b)); // y² = x³ + b
let y = Fp.sqrt(right); let y = Fp.sqrt(right);
if (!y) throw new Error('Invalid compressed G1 point'); if (!y) throw new Error('Invalid compressed G1 point');
const aflag = bitGet(compressedValue, C_BIT_POS); const aflag = bitGet(compressedValue, C_BIT_POS);
if ((y * 2n) / P !== aflag) y = Fp.negate(y); if ((y * _2n) / P !== aflag) y = Fp.neg(y);
return { x: Fp.create(x), y: Fp.create(y) }; return { x: Fp.create(x), y: Fp.create(y) };
} else if (bytes.length === 96) { } else if (bytes.length === 96) {
// Check if the infinity flag is set // Check if the infinity flag is set
if ((bytes[0] & (1 << 6)) !== 0) return bls12_381.G1.Point.ZERO; if ((bytes[0] & (1 << 6)) !== 0) return bls12_381.G1.ProjectivePoint.ZERO.toAffine();
const x = bytesToNumberBE(bytes.slice(0, Fp.BYTES)); const x = bytesToNumberBE(bytes.slice(0, Fp.BYTES));
const y = bytesToNumberBE(bytes.slice(Fp.BYTES)); const y = bytesToNumberBE(bytes.slice(Fp.BYTES));
return { x: Fp.create(x), y: Fp.create(y) }; return { x: Fp.create(x), y: Fp.create(y) };
@@ -1044,21 +1147,21 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
}, },
toBytes: (c, point, isCompressed) => { toBytes: (c, point, isCompressed) => {
const isZero = point.equals(c.ZERO); const isZero = point.equals(c.ZERO);
const { x, y } = point; const { x, y } = point.toAffine();
if (isCompressed) { if (isCompressed) {
if (isZero) return COMPRESSED_ZERO.slice(); if (isZero) return COMPRESSED_ZERO.slice();
const P = Fp.ORDER; const P = Fp.ORDER;
let num; let num;
num = bitSet(x, C_BIT_POS, Boolean((y * 2n) / P)); // set aflag num = bitSet(x, C_BIT_POS, Boolean((y * _2n) / P)); // set aflag
num = bitSet(num, S_BIT_POS, true); num = bitSet(num, S_BIT_POS, true);
return numberToBytesBE(num, Fp.BYTES); return numberToBytesBE(num, Fp.BYTES);
} else { } else {
if (isZero) { if (isZero) {
// 2x PUBLIC_KEY_LENGTH // 2x PUBLIC_KEY_LENGTH
const x = concatBytes(new Uint8Array([0x40]), new Uint8Array(2 * Fp.BYTES - 1)); const x = concatB(new Uint8Array([0x40]), new Uint8Array(2 * Fp.BYTES - 1));
return x; return x;
} else { } else {
return concatBytes(numberToBytesBE(x, Fp.BYTES), numberToBytesBE(y, Fp.BYTES)); return concatB(numberToBytesBE(x, Fp.BYTES), numberToBytesBE(y, Fp.BYTES));
} }
} }
}, },
@@ -1070,21 +1173,33 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
G2: { G2: {
Fp: Fp2, Fp: Fp2,
// cofactor // cofactor
h: 0x5d543a95414e7f1091d50792876a202cd91de4547085abaa68a205b2e5a7ddfa628f1cb4d9e82ef21537e293a6691ae1616ec6e786f0c70cf1c38e31c7238e5n, h: BigInt(
'0x5d543a95414e7f1091d50792876a202cd91de4547085abaa68a205b2e5a7ddfa628f1cb4d9e82ef21537e293a6691ae1616ec6e786f0c70cf1c38e31c7238e5'
),
Gx: Fp2.fromBigTuple([ Gx: Fp2.fromBigTuple([
0x024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8n, BigInt(
0x13e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7en, '0x024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8'
),
BigInt(
'0x13e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e'
),
]), ]),
// y = // y =
// 927553665492332455747201965776037880757740193453592970025027978793976877002675564980949289727957565575433344219582, // 927553665492332455747201965776037880757740193453592970025027978793976877002675564980949289727957565575433344219582,
// 1985150602287291935568054521177171638300868978215655730859378665066344726373823718423869104263333984641494340347905 // 1985150602287291935568054521177171638300868978215655730859378665066344726373823718423869104263333984641494340347905
Gy: Fp2.fromBigTuple([ Gy: Fp2.fromBigTuple([
0x0ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801n, BigInt(
0x0606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79ben, '0x0ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801'
),
BigInt(
'0x0606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be'
),
]), ]),
a: Fp2.ZERO, a: Fp2.ZERO,
b: Fp2.fromBigTuple([4n, 4n]), b: Fp2.fromBigTuple([4n, _4n]),
hEff: 0xbc69f08f2ee75b3584c6a0ea91b352888e2a8e9145ad7689986ff031508ffe1329c2f178731db956d82bf015d1212b02ec0ec69d7477c1ae954cbc06689f6a359894c0adebbf6b4e8020005aaa95551n, hEff: BigInt(
'0xbc69f08f2ee75b3584c6a0ea91b352888e2a8e9145ad7689986ff031508ffe1329c2f178731db956d82bf015d1212b02ec0ec69d7477c1ae954cbc06689f6a359894c0adebbf6b4e8020005aaa95551'
),
htfDefaults: { ...htfDefaults }, htfDefaults: { ...htfDefaults },
wrapPrivateKey: true, wrapPrivateKey: true,
allowInfinityPoint: true, allowInfinityPoint: true,
@@ -1120,7 +1235,7 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
const Q = t3.subtract(P); // Ψ²(2P) - Ψ(P) + [x²]P - [x]Ψ(P) + [x]P - 1P const Q = t3.subtract(P); // Ψ²(2P) - Ψ(P) + [x²]P - [x]Ψ(P) + [x]P - 1P
return Q; // [x²-x-1]P + [x-1]Ψ(P) + Ψ²(2P) return Q; // [x²-x-1]P + [x-1]Ψ(P) + Ψ²(2P)
}, },
fromBytes: (bytes: Uint8Array): { x: Fp2; y: Fp2 } => { fromBytes: (bytes: Uint8Array): AffinePoint<Fp2> => {
const m_byte = bytes[0] & 0xe0; const m_byte = bytes[0] & 0xe0;
if (m_byte === 0x20 || m_byte === 0x60 || m_byte === 0xe0) { if (m_byte === 0x20 || m_byte === 0x60 || m_byte === 0xe0) {
throw new Error('Invalid encoding flag: ' + m_byte); throw new Error('Invalid encoding flag: ' + m_byte);
@@ -1128,6 +1243,8 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
const bitC = m_byte & 0x80; // compression bit const bitC = m_byte & 0x80; // compression bit
const bitI = m_byte & 0x40; // point at infinity bit const bitI = m_byte & 0x40; // point at infinity bit
const bitS = m_byte & 0x20; // sign bit const bitS = m_byte & 0x20; // sign bit
const L = Fp.BYTES;
const slc = (b: Uint8Array, from: number, to?: number) => bytesToNumberBE(b.slice(from, to));
if (bytes.length === 96 && bitC) { if (bytes.length === 96 && bitC) {
const { b } = bls12_381.CURVE.G2; const { b } = bls12_381.CURVE.G2;
const P = Fp.ORDER; const P = Fp.ORDER;
@@ -1140,23 +1257,23 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
} }
return { x: Fp2.ZERO, y: Fp2.ZERO }; return { x: Fp2.ZERO, y: Fp2.ZERO };
} }
const x_1 = bytesToNumberBE(bytes.slice(0, Fp.BYTES)); const x_1 = slc(bytes, 0, L);
const x_0 = bytesToNumberBE(bytes.slice(Fp.BYTES)); const x_0 = slc(bytes, L, 2 * L);
const x = Fp2.create({ c0: Fp.create(x_0), c1: Fp.create(x_1) }); const x = Fp2.create({ c0: Fp.create(x_0), c1: Fp.create(x_1) });
const right = Fp2.add(Fp2.pow(x, 3n), b); // y² = x³ + 4 * (u+1) = x³ + b const right = Fp2.add(Fp2.pow(x, _3n), b); // y² = x³ + 4 * (u+1) = x³ + b
let y = Fp2.sqrt(right); let y = Fp2.sqrt(right);
const Y_bit = y.c1 === 0n ? (y.c0 * 2n) / P : (y.c1 * 2n) / P ? 1n : 0n; const Y_bit = y.c1 === _0n ? (y.c0 * _2n) / P : (y.c1 * _2n) / P ? _1n : _0n;
y = bitS > 0 && Y_bit > 0 ? y : Fp2.negate(y); y = bitS > 0 && Y_bit > 0 ? y : Fp2.neg(y);
return { x, y }; return { x, y };
} else if (bytes.length === 192 && !bitC) { } else if (bytes.length === 192 && !bitC) {
// Check if the infinity flag is set // Check if the infinity flag is set
if ((bytes[0] & (1 << 6)) !== 0) { if ((bytes[0] & (1 << 6)) !== 0) {
return { x: Fp2.ZERO, y: Fp2.ZERO }; return { x: Fp2.ZERO, y: Fp2.ZERO };
} }
const x1 = bytesToNumberBE(bytes.slice(0, Fp.BYTES)); const x1 = slc(bytes, 0, L);
const x0 = bytesToNumberBE(bytes.slice(Fp.BYTES, 2 * Fp.BYTES)); const x0 = slc(bytes, L, 2 * L);
const y1 = bytesToNumberBE(bytes.slice(2 * Fp.BYTES, 3 * Fp.BYTES)); const y1 = slc(bytes, 2 * L, 3 * L);
const y0 = bytesToNumberBE(bytes.slice(3 * Fp.BYTES)); const y0 = slc(bytes, 3 * L, 4 * L);
return { x: Fp2.fromBigTuple([x0, x1]), y: Fp2.fromBigTuple([y0, y1]) }; return { x: Fp2.fromBigTuple([x0, x1]), y: Fp2.fromBigTuple([y0, y1]) };
} else { } else {
throw new Error('Invalid point G2, expected 96/192 bytes'); throw new Error('Invalid point G2, expected 96/192 bytes');
@@ -1164,20 +1281,20 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
}, },
toBytes: (c, point, isCompressed) => { toBytes: (c, point, isCompressed) => {
const isZero = point.equals(c.ZERO); const isZero = point.equals(c.ZERO);
const { x, y } = point; const { x, y } = point.toAffine();
if (isCompressed) { if (isCompressed) {
const P = Fp.ORDER; const P = Fp.ORDER;
if (isZero) return concatBytes(COMPRESSED_ZERO, numberToBytesBE(0n, Fp.BYTES)); if (isZero) return concatB(COMPRESSED_ZERO, numberToBytesBE(0n, Fp.BYTES));
const flag = Boolean(y.c1 === 0n ? (y.c0 * 2n) / P : (y.c1 * 2n) / P); const flag = Boolean(y.c1 === _0n ? (y.c0 * _2n) / P : (y.c1 * _2n) / P);
// set compressed & sign bits (looks like different offsets than for G1/Fp?) // set compressed & sign bits (looks like different offsets than for G1/Fp?)
let x_1 = bitSet(x.c1, C_BIT_POS, flag); let x_1 = bitSet(x.c1, C_BIT_POS, flag);
x_1 = bitSet(x_1, S_BIT_POS, true); x_1 = bitSet(x_1, S_BIT_POS, true);
return concatBytes(numberToBytesBE(x_1, Fp.BYTES), numberToBytesBE(x.c0, Fp.BYTES)); return concatB(numberToBytesBE(x_1, Fp.BYTES), numberToBytesBE(x.c0, Fp.BYTES));
} else { } else {
if (isZero) return concatBytes(new Uint8Array([0x40]), new Uint8Array(4 * Fp.BYTES - 1)); // bytes[0] |= 1 << 6; if (isZero) return concatB(new Uint8Array([0x40]), new Uint8Array(4 * Fp.BYTES - 1)); // bytes[0] |= 1 << 6;
const { re: x0, im: x1 } = Fp2.reim(x); const { re: x0, im: x1 } = Fp2.reim(x);
const { re: y0, im: y1 } = Fp2.reim(y); const { re: y0, im: y1 } = Fp2.reim(y);
return concatBytes( return concatB(
numberToBytesBE(x1, Fp.BYTES), numberToBytesBE(x1, Fp.BYTES),
numberToBytesBE(x0, Fp.BYTES), numberToBytesBE(x0, Fp.BYTES),
numberToBytesBE(y1, Fp.BYTES), numberToBytesBE(y1, Fp.BYTES),
@@ -1187,8 +1304,8 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
}, },
Signature: { Signature: {
// TODO: Optimize, it's very slow because of sqrt. // TODO: Optimize, it's very slow because of sqrt.
decode(hex: Hex): PointType<Fp2> { decode(hex: Hex): ProjPointType<Fp2> {
hex = ensureBytes(hex); hex = ensureBytes('signatureHex', hex);
const P = Fp.ORDER; const P = Fp.ORDER;
const half = hex.length / 2; const half = hex.length / 2;
if (half !== 48 && half !== 96) if (half !== 48 && half !== 96)
@@ -1197,12 +1314,12 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
const z2 = bytesToNumberBE(hex.slice(half)); const z2 = bytesToNumberBE(hex.slice(half));
// Indicates the infinity point // Indicates the infinity point
const bflag1 = bitGet(z1, I_BIT_POS); const bflag1 = bitGet(z1, I_BIT_POS);
if (bflag1 === 1n) return bls12_381.G2.Point.ZERO; if (bflag1 === _1n) return bls12_381.G2.ProjectivePoint.ZERO;
const x1 = Fp.create(z1 & Fp.MASK); const x1 = Fp.create(z1 & Fp.MASK);
const x2 = Fp.create(z2); const x2 = Fp.create(z2);
const x = Fp2.create({ c0: x2, c1: x1 }); const x = Fp2.create({ c0: x2, c1: x1 });
const y2 = Fp2.add(Fp2.pow(x, 3n), bls12_381.CURVE.G2.b); // y² = x³ + 4 const y2 = Fp2.add(Fp2.pow(x, _3n), bls12_381.CURVE.G2.b); // y² = x³ + 4
// The slow part // The slow part
let y = Fp2.sqrt(y2); let y = Fp2.sqrt(y2);
if (!y) throw new Error('Failed to find a square root'); if (!y) throw new Error('Failed to find a square root');
@@ -1211,25 +1328,26 @@ export const bls12_381: CurveFn<Fp, Fp2, Fp6, Fp12> = bls({
// If y1 happens to be zero, then use the bit of y0 // If y1 happens to be zero, then use the bit of y0
const { re: y0, im: y1 } = Fp2.reim(y); const { re: y0, im: y1 } = Fp2.reim(y);
const aflag1 = bitGet(z1, 381); const aflag1 = bitGet(z1, 381);
const isGreater = y1 > 0n && (y1 * 2n) / P !== aflag1; const isGreater = y1 > _0n && (y1 * _2n) / P !== aflag1;
const isZero = y1 === 0n && (y0 * 2n) / P !== aflag1; const isZero = y1 === _0n && (y0 * _2n) / P !== aflag1;
if (isGreater || isZero) y = Fp2.negate(y); if (isGreater || isZero) y = Fp2.neg(y);
const point = new bls12_381.G2.Point(x, y); const point = bls12_381.G2.ProjectivePoint.fromAffine({ x, y });
point.assertValidity(); point.assertValidity();
return point; return point;
}, },
encode(point: PointType<Fp2>) { encode(point: ProjPointType<Fp2>) {
// NOTE: by some reasons it was missed in bls12-381, looks like bug // NOTE: by some reasons it was missed in bls12-381, looks like bug
point.assertValidity(); point.assertValidity();
if (point.equals(bls12_381.G2.Point.ZERO)) if (point.equals(bls12_381.G2.ProjectivePoint.ZERO))
return concatBytes(COMPRESSED_ZERO, numberToBytesBE(0n, Fp.BYTES)); return concatB(COMPRESSED_ZERO, numberToBytesBE(0n, Fp.BYTES));
const { re: x0, im: x1 } = Fp2.reim(point.x); const a = point.toAffine();
const { re: y0, im: y1 } = Fp2.reim(point.y); const { re: x0, im: x1 } = Fp2.reim(a.x);
const tmp = y1 > 0n ? y1 * 2n : y0 * 2n; const { re: y0, im: y1 } = Fp2.reim(a.y);
const aflag1 = Boolean((tmp / Fp.ORDER) & 1n); const tmp = y1 > _0n ? y1 * _2n : y0 * _2n;
const aflag1 = Boolean((tmp / Fp.ORDER) & _1n);
const z1 = bitSet(bitSet(x1, 381, aflag1), S_BIT_POS, true); const z1 = bitSet(bitSet(x1, 381, aflag1), S_BIT_POS, true);
const z2 = x0; const z2 = x0;
return concatBytes(numberToBytesBE(z1, Fp.BYTES), numberToBytesBE(z2, Fp.BYTES)); return concatB(numberToBytesBE(z1, Fp.BYTES), numberToBytesBE(z2, Fp.BYTES));
}, },
}, },
}, },

View File

@@ -1,8 +1,8 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { weierstrass } from './abstract/weierstrass.js';
import { sha256 } from '@noble/hashes/sha256'; import { sha256 } from '@noble/hashes/sha256';
import { weierstrass } from './abstract/weierstrass.js';
import { getHash } from './_shortw_utils.js'; import { getHash } from './_shortw_utils.js';
import { Fp } from './abstract/modular.js'; import { Field } from './abstract/modular.js';
/** /**
* bn254 pairing-friendly curve. * bn254 pairing-friendly curve.
* Previously known as alt_bn_128, when it had 128-bit security. * Previously known as alt_bn_128, when it had 128-bit security.
@@ -12,7 +12,7 @@ import { Fp } from './abstract/modular.js';
export const bn254 = weierstrass({ export const bn254 = weierstrass({
a: BigInt(0), a: BigInt(0),
b: BigInt(3), b: BigInt(3),
Fp: Fp(BigInt('0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47')), Fp: Field(BigInt('0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47')),
n: BigInt('0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001'), n: BigInt('0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001'),
Gx: BigInt(1), Gx: BigInt(1),
Gy: BigInt(2), Gy: BigInt(2),

View File

@@ -1,17 +1,19 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { sha512 } from '@noble/hashes/sha512'; import { sha512 } from '@noble/hashes/sha512';
import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'; import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
import { twistedEdwards, ExtendedPointType } from './abstract/edwards.js'; import { twistedEdwards, ExtPointType } from './abstract/edwards.js';
import { montgomery } from './abstract/montgomery.js'; import { montgomery } from './abstract/montgomery.js';
import { mod, pow2, isNegativeLE, Fp as Field, FpSqrtEven } from './abstract/modular.js'; import { mod, pow2, isNegativeLE, Field, FpSqrtEven } from './abstract/modular.js';
import { import {
ensureBytes,
equalBytes, equalBytes,
bytesToHex, bytesToHex,
bytesToNumberLE, bytesToNumberLE,
numberToBytesLE, numberToBytesLE,
Hex, Hex,
ensureBytes,
} from './abstract/utils.js'; } from './abstract/utils.js';
import * as htf from './abstract/hash-to-curve.js';
import { AffinePoint } from './abstract/curve.js';
/** /**
* ed25519 Twisted Edwards curve with following addons: * ed25519 Twisted Edwards curve with following addons:
@@ -93,79 +95,6 @@ export const ED25519_TORSION_SUBGROUP = [
const Fp = Field(ED25519_P, undefined, true); const Fp = Field(ED25519_P, undefined, true);
// Hash To Curve Elligator2 Map (NOTE: different from ristretto255 elligator)
// NOTE: very important part is usage of FpSqrtEven for ELL2_C1_EDWARDS, since
// SageMath returns different root first and everything falls apart
const ELL2_C1 = (Fp.ORDER + BigInt(3)) / BigInt(8); // 1. c1 = (q + 3) / 8 # Integer arithmetic
const ELL2_C2 = Fp.pow(_2n, ELL2_C1); // 2. c2 = 2^c1
const ELL2_C3 = Fp.sqrt(Fp.negate(Fp.ONE)); // 3. c3 = sqrt(-1)
const ELL2_C4 = (Fp.ORDER - BigInt(5)) / BigInt(8); // 4. c4 = (q - 5) / 8 # Integer arithmetic
const ELL2_J = BigInt(486662);
// prettier-ignore
function map_to_curve_elligator2_curve25519(u: bigint) {
let tv1 = Fp.square(u); // 1. tv1 = u^2
tv1 = Fp.mul(tv1, _2n); // 2. tv1 = 2 * tv1
let xd = Fp.add(tv1, Fp.ONE); // 3. xd = tv1 + 1 # Nonzero: -1 is square (mod p), tv1 is not
let x1n = Fp.negate(ELL2_J); // 4. x1n = -J # x1 = x1n / xd = -J / (1 + 2 * u^2)
let tv2 = Fp.square(xd); // 5. tv2 = xd^2
let gxd = Fp.mul(tv2, xd); // 6. gxd = tv2 * xd # gxd = xd^3
let gx1 = Fp.mul(tv1, ELL2_J); // 7. gx1 = J * tv1 # x1n + J * xd
gx1 = Fp.mul(gx1, x1n); // 8. gx1 = gx1 * x1n # x1n^2 + J * x1n * xd
gx1 = Fp.add(gx1, tv2); // 9. gx1 = gx1 + tv2 # x1n^2 + J * x1n * xd + xd^2
gx1 = Fp.mul(gx1, x1n); // 10. gx1 = gx1 * x1n # x1n^3 + J * x1n^2 * xd + x1n * xd^2
let tv3 = Fp.square(gxd); // 11. tv3 = gxd^2
tv2 = Fp.square(tv3); // 12. tv2 = tv3^2 # gxd^4
tv3 = Fp.mul(tv3, gxd); // 13. tv3 = tv3 * gxd # gxd^3
tv3 = Fp.mul(tv3, gx1); // 14. tv3 = tv3 * gx1 # gx1 * gxd^3
tv2 = Fp.mul(tv2, tv3); // 15. tv2 = tv2 * tv3 # gx1 * gxd^7
let y11 = Fp.pow(tv2, ELL2_C4); // 16. y11 = tv2^c4 # (gx1 * gxd^7)^((p - 5) / 8)
y11 = Fp.mul(y11, tv3); // 17. y11 = y11 * tv3 # gx1*gxd^3*(gx1*gxd^7)^((p-5)/8)
let y12 = Fp.mul(y11, ELL2_C3); // 18. y12 = y11 * c3
tv2 = Fp.square(y11); // 19. tv2 = y11^2
tv2 = Fp.mul(tv2, gxd); // 20. tv2 = tv2 * gxd
let e1 = Fp.equals(tv2, gx1); // 21. e1 = tv2 == gx1
let y1 = Fp.cmov(y12, y11, e1); // 22. y1 = CMOV(y12, y11, e1) # If g(x1) is square, this is its sqrt
let x2n = Fp.mul(x1n, tv1); // 23. x2n = x1n * tv1 # x2 = x2n / xd = 2 * u^2 * x1n / xd
let y21 = Fp.mul(y11, u); // 24. y21 = y11 * u
y21 = Fp.mul(y21, ELL2_C2); // 25. y21 = y21 * c2
let y22 = Fp.mul(y21, ELL2_C3); // 26. y22 = y21 * c3
let gx2 = Fp.mul(gx1, tv1); // 27. gx2 = gx1 * tv1 # g(x2) = gx2 / gxd = 2 * u^2 * g(x1)
tv2 = Fp.square(y21); // 28. tv2 = y21^2
tv2 = Fp.mul(tv2, gxd); // 29. tv2 = tv2 * gxd
let e2 = Fp.equals(tv2, gx2); // 30. e2 = tv2 == gx2
let y2 = Fp.cmov(y22, y21, e2); // 31. y2 = CMOV(y22, y21, e2) # If g(x2) is square, this is its sqrt
tv2 = Fp.square(y1); // 32. tv2 = y1^2
tv2 = Fp.mul(tv2, gxd); // 33. tv2 = tv2 * gxd
let e3 = Fp.equals(tv2, gx1); // 34. e3 = tv2 == gx1
let xn = Fp.cmov(x2n, x1n, e3); // 35. xn = CMOV(x2n, x1n, e3) # If e3, x = x1, else x = x2
let y = Fp.cmov(y2, y1, e3); // 36. y = CMOV(y2, y1, e3) # If e3, y = y1, else y = y2
let e4 = Fp.isOdd(y); // 37. e4 = sgn0(y) == 1 # Fix sign of y
y = Fp.cmov(y, Fp.negate(y), e3 !== e4); // 38. y = CMOV(y, -y, e3 XOR e4)
return { xMn: xn, xMd: xd, yMn: y, yMd: 1n }; // 39. return (xn, xd, y, 1)
}
const ELL2_C1_EDWARDS = FpSqrtEven(Fp, Fp.negate(BigInt(486664))); // sgn0(c1) MUST equal 0
function map_to_curve_elligator2_edwards25519(u: bigint) {
const { xMn, xMd, yMn, yMd } = map_to_curve_elligator2_curve25519(u); // 1. (xMn, xMd, yMn, yMd) = map_to_curve_elligator2_curve25519(u)
let xn = Fp.mul(xMn, yMd); // 2. xn = xMn * yMd
xn = Fp.mul(xn, ELL2_C1_EDWARDS); // 3. xn = xn * c1
let xd = Fp.mul(xMd, yMn); // 4. xd = xMd * yMn # xn / xd = c1 * xM / yM
let yn = Fp.sub(xMn, xMd); // 5. yn = xMn - xMd
let yd = Fp.add(xMn, xMd); // 6. yd = xMn + xMd # (n / d - 1) / (n / d + 1) = (n - d) / (n + d)
let tv1 = Fp.mul(xd, yd); // 7. tv1 = xd * yd
let e = Fp.equals(tv1, Fp.ZERO); // 8. e = tv1 == 0
xn = Fp.cmov(xn, Fp.ZERO, e); // 9. xn = CMOV(xn, 0, e)
xd = Fp.cmov(xd, Fp.ONE, e); // 10. xd = CMOV(xd, 1, e)
yn = Fp.cmov(yn, Fp.ONE, e); // 11. yn = CMOV(yn, 1, e)
yd = Fp.cmov(yd, Fp.ONE, e); // 12. yd = CMOV(yd, 1, e)
const inv = Fp.invertBatch([xd, yd]); // batch division
return { x: Fp.mul(xn, inv[0]), y: Fp.mul(yn, inv[1]) }; // 13. return (xn, xd, yn, yd)
}
const ED25519_DEF = { const ED25519_DEF = {
// Param: a // Param: a
a: BigInt(-1), a: BigInt(-1),
@@ -189,15 +118,6 @@ const ED25519_DEF = {
// Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3. // Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3.
// Constant-time, u/√v // Constant-time, u/√v
uvRatio, uvRatio,
htfDefaults: {
DST: 'edwards25519_XMD:SHA-512_ELL2_RO_',
p: Fp.ORDER,
m: 1,
k: 128,
expand: 'xmd',
hash: sha512,
},
mapToCurve: (scalars: bigint[]) => map_to_curve_elligator2_edwards25519(scalars[0]),
} as const; } as const;
export const ed25519 = twistedEdwards(ED25519_DEF); export const ed25519 = twistedEdwards(ED25519_DEF);
@@ -219,10 +139,10 @@ export const ed25519ph = twistedEdwards({
export const x25519 = montgomery({ export const x25519 = montgomery({
P: ED25519_P, P: ED25519_P,
a24: BigInt('121665'), a: BigInt(486662),
montgomeryBits: 255, // n is 253 bits montgomeryBits: 255, // n is 253 bits
nByteLength: 32, nByteLength: 32,
Gu: '0900000000000000000000000000000000000000000000000000000000000000', Gu: BigInt(9),
powPminus2: (x: bigint): bigint => { powPminus2: (x: bigint): bigint => {
const P = ED25519_P; const P = ED25519_P;
// x^(p-2) aka x^(2^255-21) // x^(p-2) aka x^(2^255-21)
@@ -230,10 +150,98 @@ export const x25519 = montgomery({
return mod(pow2(pow_p_5_8, BigInt(3), P) * b2, P); return mod(pow2(pow_p_5_8, BigInt(3), P) * b2, P);
}, },
adjustScalarBytes, adjustScalarBytes,
randomBytes,
}); });
// Hash To Curve Elligator2 Map (NOTE: different from ristretto255 elligator)
// NOTE: very important part is usage of FpSqrtEven for ELL2_C1_EDWARDS, since
// SageMath returns different root first and everything falls apart
const ELL2_C1 = (Fp.ORDER + BigInt(3)) / BigInt(8); // 1. c1 = (q + 3) / 8 # Integer arithmetic
const ELL2_C2 = Fp.pow(_2n, ELL2_C1); // 2. c2 = 2^c1
const ELL2_C3 = Fp.sqrt(Fp.neg(Fp.ONE)); // 3. c3 = sqrt(-1)
const ELL2_C4 = (Fp.ORDER - BigInt(5)) / BigInt(8); // 4. c4 = (q - 5) / 8 # Integer arithmetic
const ELL2_J = BigInt(486662);
// prettier-ignore
function map_to_curve_elligator2_curve25519(u: bigint) {
let tv1 = Fp.sqr(u); // 1. tv1 = u^2
tv1 = Fp.mul(tv1, _2n); // 2. tv1 = 2 * tv1
let xd = Fp.add(tv1, Fp.ONE); // 3. xd = tv1 + 1 # Nonzero: -1 is square (mod p), tv1 is not
let x1n = Fp.neg(ELL2_J); // 4. x1n = -J # x1 = x1n / xd = -J / (1 + 2 * u^2)
let tv2 = Fp.sqr(xd); // 5. tv2 = xd^2
let gxd = Fp.mul(tv2, xd); // 6. gxd = tv2 * xd # gxd = xd^3
let gx1 = Fp.mul(tv1, ELL2_J); // 7. gx1 = J * tv1 # x1n + J * xd
gx1 = Fp.mul(gx1, x1n); // 8. gx1 = gx1 * x1n # x1n^2 + J * x1n * xd
gx1 = Fp.add(gx1, tv2); // 9. gx1 = gx1 + tv2 # x1n^2 + J * x1n * xd + xd^2
gx1 = Fp.mul(gx1, x1n); // 10. gx1 = gx1 * x1n # x1n^3 + J * x1n^2 * xd + x1n * xd^2
let tv3 = Fp.sqr(gxd); // 11. tv3 = gxd^2
tv2 = Fp.sqr(tv3); // 12. tv2 = tv3^2 # gxd^4
tv3 = Fp.mul(tv3, gxd); // 13. tv3 = tv3 * gxd # gxd^3
tv3 = Fp.mul(tv3, gx1); // 14. tv3 = tv3 * gx1 # gx1 * gxd^3
tv2 = Fp.mul(tv2, tv3); // 15. tv2 = tv2 * tv3 # gx1 * gxd^7
let y11 = Fp.pow(tv2, ELL2_C4); // 16. y11 = tv2^c4 # (gx1 * gxd^7)^((p - 5) / 8)
y11 = Fp.mul(y11, tv3); // 17. y11 = y11 * tv3 # gx1*gxd^3*(gx1*gxd^7)^((p-5)/8)
let y12 = Fp.mul(y11, ELL2_C3); // 18. y12 = y11 * c3
tv2 = Fp.sqr(y11); // 19. tv2 = y11^2
tv2 = Fp.mul(tv2, gxd); // 20. tv2 = tv2 * gxd
let e1 = Fp.eql(tv2, gx1); // 21. e1 = tv2 == gx1
let y1 = Fp.cmov(y12, y11, e1); // 22. y1 = CMOV(y12, y11, e1) # If g(x1) is square, this is its sqrt
let x2n = Fp.mul(x1n, tv1); // 23. x2n = x1n * tv1 # x2 = x2n / xd = 2 * u^2 * x1n / xd
let y21 = Fp.mul(y11, u); // 24. y21 = y11 * u
y21 = Fp.mul(y21, ELL2_C2); // 25. y21 = y21 * c2
let y22 = Fp.mul(y21, ELL2_C3); // 26. y22 = y21 * c3
let gx2 = Fp.mul(gx1, tv1); // 27. gx2 = gx1 * tv1 # g(x2) = gx2 / gxd = 2 * u^2 * g(x1)
tv2 = Fp.sqr(y21); // 28. tv2 = y21^2
tv2 = Fp.mul(tv2, gxd); // 29. tv2 = tv2 * gxd
let e2 = Fp.eql(tv2, gx2); // 30. e2 = tv2 == gx2
let y2 = Fp.cmov(y22, y21, e2); // 31. y2 = CMOV(y22, y21, e2) # If g(x2) is square, this is its sqrt
tv2 = Fp.sqr(y1); // 32. tv2 = y1^2
tv2 = Fp.mul(tv2, gxd); // 33. tv2 = tv2 * gxd
let e3 = Fp.eql(tv2, gx1); // 34. e3 = tv2 == gx1
let xn = Fp.cmov(x2n, x1n, e3); // 35. xn = CMOV(x2n, x1n, e3) # If e3, x = x1, else x = x2
let y = Fp.cmov(y2, y1, e3); // 36. y = CMOV(y2, y1, e3) # If e3, y = y1, else y = y2
let e4 = Fp.isOdd(y); // 37. e4 = sgn0(y) == 1 # Fix sign of y
y = Fp.cmov(y, Fp.neg(y), e3 !== e4); // 38. y = CMOV(y, -y, e3 XOR e4)
return { xMn: xn, xMd: xd, yMn: y, yMd: _1n }; // 39. return (xn, xd, y, 1)
}
const ELL2_C1_EDWARDS = FpSqrtEven(Fp, Fp.neg(BigInt(486664))); // sgn0(c1) MUST equal 0
function map_to_curve_elligator2_edwards25519(u: bigint) {
const { xMn, xMd, yMn, yMd } = map_to_curve_elligator2_curve25519(u); // 1. (xMn, xMd, yMn, yMd) = map_to_curve_elligator2_curve25519(u)
let xn = Fp.mul(xMn, yMd); // 2. xn = xMn * yMd
xn = Fp.mul(xn, ELL2_C1_EDWARDS); // 3. xn = xn * c1
let xd = Fp.mul(xMd, yMn); // 4. xd = xMd * yMn # xn / xd = c1 * xM / yM
let yn = Fp.sub(xMn, xMd); // 5. yn = xMn - xMd
let yd = Fp.add(xMn, xMd); // 6. yd = xMn + xMd # (n / d - 1) / (n / d + 1) = (n - d) / (n + d)
let tv1 = Fp.mul(xd, yd); // 7. tv1 = xd * yd
let e = Fp.eql(tv1, Fp.ZERO); // 8. e = tv1 == 0
xn = Fp.cmov(xn, Fp.ZERO, e); // 9. xn = CMOV(xn, 0, e)
xd = Fp.cmov(xd, Fp.ONE, e); // 10. xd = CMOV(xd, 1, e)
yn = Fp.cmov(yn, Fp.ONE, e); // 11. yn = CMOV(yn, 1, e)
yd = Fp.cmov(yd, Fp.ONE, e); // 12. yd = CMOV(yd, 1, e)
const inv = Fp.invertBatch([xd, yd]); // batch division
return { x: Fp.mul(xn, inv[0]), y: Fp.mul(yn, inv[1]) }; // 13. return (xn, xd, yn, yd)
}
const { hashToCurve, encodeToCurve } = htf.createHasher(
ed25519.ExtendedPoint,
(scalars: bigint[]) => map_to_curve_elligator2_edwards25519(scalars[0]),
{
DST: 'edwards25519_XMD:SHA-512_ELL2_RO_',
encodeDST: 'edwards25519_XMD:SHA-512_ELL2_NU_',
p: Fp.ORDER,
m: 1,
k: 128,
expand: 'xmd',
hash: sha512,
}
);
export { hashToCurve, encodeToCurve };
function assertRstPoint(other: unknown) { function assertRstPoint(other: unknown) {
if (!(other instanceof RistrettoPoint)) throw new TypeError('RistrettoPoint expected'); if (!(other instanceof RistrettoPoint)) throw new Error('RistrettoPoint expected');
} }
// √(-1) aka √(a) aka 2^((p-1)/4) // √(-1) aka √(a) aka 2^((p-1)/4)
const SQRT_M1 = BigInt( const SQRT_M1 = BigInt(
@@ -262,7 +270,7 @@ const MAX_255B = BigInt('0x7ffffffffffffffffffffffffffffffffffffffffffffffffffff
const bytes255ToNumberLE = (bytes: Uint8Array) => const bytes255ToNumberLE = (bytes: Uint8Array) =>
ed25519.CURVE.Fp.create(bytesToNumberLE(bytes) & MAX_255B); ed25519.CURVE.Fp.create(bytesToNumberLE(bytes) & MAX_255B);
type ExtendedPoint = ExtendedPointType; type ExtendedPoint = ExtPointType;
// Computes Elligator map for Ristretto // Computes Elligator map for Ristretto
// https://ristretto.group/formulas/elligator.html // https://ristretto.group/formulas/elligator.html
@@ -302,6 +310,11 @@ export class RistrettoPoint {
// Private property to discourage combining ExtendedPoint + RistrettoPoint // Private property to discourage combining ExtendedPoint + RistrettoPoint
// Always use Ristretto encoding/decoding instead. // Always use Ristretto encoding/decoding instead.
constructor(private readonly ep: ExtendedPoint) {} constructor(private readonly ep: ExtendedPoint) {}
static fromAffine(ap: AffinePoint<bigint>) {
return new RistrettoPoint(ed25519.ExtendedPoint.fromAffine(ap));
}
/** /**
* Takes uniform output of 64-bit hash function like sha512 and converts it to `RistrettoPoint`. * Takes uniform output of 64-bit hash function like sha512 and converts it to `RistrettoPoint`.
* The hash-to-group operation applies Elligator twice and adds the results. * The hash-to-group operation applies Elligator twice and adds the results.
@@ -310,7 +323,7 @@ export class RistrettoPoint {
* @param hex 64-bit output of a hash function * @param hex 64-bit output of a hash function
*/ */
static hashToCurve(hex: Hex): RistrettoPoint { static hashToCurve(hex: Hex): RistrettoPoint {
hex = ensureBytes(hex, 64); hex = ensureBytes('ristrettoHash', hex, 64);
const r1 = bytes255ToNumberLE(hex.slice(0, 32)); const r1 = bytes255ToNumberLE(hex.slice(0, 32));
const R1 = calcElligatorRistrettoMap(r1); const R1 = calcElligatorRistrettoMap(r1);
const r2 = bytes255ToNumberLE(hex.slice(32, 64)); const r2 = bytes255ToNumberLE(hex.slice(32, 64));
@@ -324,7 +337,7 @@ export class RistrettoPoint {
* @param hex Ristretto-encoded 32 bytes. Not every 32-byte string is valid ristretto encoding * @param hex Ristretto-encoded 32 bytes. Not every 32-byte string is valid ristretto encoding
*/ */
static fromHex(hex: Hex): RistrettoPoint { static fromHex(hex: Hex): RistrettoPoint {
hex = ensureBytes(hex, 32); hex = ensureBytes('ristrettoHex', hex, 32);
const { a, d } = ed25519.CURVE; const { a, d } = ed25519.CURVE;
const P = ed25519.CURVE.Fp.ORDER; const P = ed25519.CURVE.Fp.ORDER;
const mod = ed25519.CURVE.Fp.create; const mod = ed25519.CURVE.Fp.create;
@@ -355,7 +368,7 @@ export class RistrettoPoint {
* https://ristretto.group/formulas/encoding.html * https://ristretto.group/formulas/encoding.html
*/ */
toRawBytes(): Uint8Array { toRawBytes(): Uint8Array {
let { x, y, z, t } = this.ep; let { ex: x, ey: y, ez: z, et: t } = this.ep;
const P = ed25519.CURVE.Fp.ORDER; const P = ed25519.CURVE.Fp.ORDER;
const mod = ed25519.CURVE.Fp.create; const mod = ed25519.CURVE.Fp.create;
const u1 = mod(mod(z + y) * mod(z - y)); // 1 const u1 = mod(mod(z + y) * mod(z - y)); // 1
@@ -393,12 +406,12 @@ export class RistrettoPoint {
// Compare one point to another. // Compare one point to another.
equals(other: RistrettoPoint): boolean { equals(other: RistrettoPoint): boolean {
assertRstPoint(other); assertRstPoint(other);
const a = this.ep; const { ex: X1, ey: Y1 } = this.ep;
const b = other.ep; const { ex: X2, ey: Y2 } = other.ep;
const mod = ed25519.CURVE.Fp.create; const mod = ed25519.CURVE.Fp.create;
// (x1 * y2 == y1 * x2) | (y1 * y2 == x1 * x2) // (x1 * y2 == y1 * x2) | (y1 * y2 == x1 * x2)
const one = mod(a.x * b.y) === mod(a.y * b.x); const one = mod(X1 * Y2) === mod(Y1 * X2);
const two = mod(a.y * b.y) === mod(a.x * b.x); const two = mod(Y1 * Y2) === mod(X1 * X2);
return one || two; return one || two;
} }
@@ -412,11 +425,21 @@ export class RistrettoPoint {
return new RistrettoPoint(this.ep.subtract(other.ep)); return new RistrettoPoint(this.ep.subtract(other.ep));
} }
multiply(scalar: number | bigint): RistrettoPoint { multiply(scalar: bigint): RistrettoPoint {
return new RistrettoPoint(this.ep.multiply(scalar)); return new RistrettoPoint(this.ep.multiply(scalar));
} }
multiplyUnsafe(scalar: number | bigint): RistrettoPoint { multiplyUnsafe(scalar: bigint): RistrettoPoint {
return new RistrettoPoint(this.ep.multiplyUnsafe(scalar)); return new RistrettoPoint(this.ep.multiplyUnsafe(scalar));
} }
} }
// https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/14/
// Appendix B. Hashing to ristretto255
export const hash_to_ristretto255 = (msg: Uint8Array, options: htf.htfBasicOpts) => {
const d = options.DST;
const DST = typeof d === 'string' ? utf8ToBytes(d) : d;
const uniform_bytes = htf.expand_message_xmd(msg, DST, 64, sha512);
const P = RistrettoPoint.hashToCurve(uniform_bytes);
return P;
};

View File

@@ -2,8 +2,9 @@
import { shake256 } from '@noble/hashes/sha3'; import { shake256 } from '@noble/hashes/sha3';
import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/hashes/utils'; import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/hashes/utils';
import { twistedEdwards } from './abstract/edwards.js'; import { twistedEdwards } from './abstract/edwards.js';
import { mod, pow2, Fp as Field } from './abstract/modular.js'; import { mod, pow2, Field } from './abstract/modular.js';
import { montgomery } from './abstract/montgomery.js'; import { montgomery } from './abstract/montgomery.js';
import * as htf from './abstract/hash-to-curve.js';
/** /**
* Edwards448 (not Ed448-Goldilocks) curve with following addons: * Edwards448 (not Ed448-Goldilocks) curve with following addons:
@@ -53,81 +54,7 @@ function adjustScalarBytes(bytes: Uint8Array): Uint8Array {
} }
const Fp = Field(ed448P, 456, true); const Fp = Field(ed448P, 456, true);
const _4n = BigInt(4);
// Hash To Curve Elligator2 Map
const ELL2_C1 = (Fp.ORDER - BigInt(3)) / BigInt(4); // 1. c1 = (q - 3) / 4 # Integer arithmetic
const ELL2_J = BigInt(156326);
function map_to_curve_elligator2_curve448(u: bigint) {
let tv1 = Fp.square(u); // 1. tv1 = u^2
let e1 = Fp.equals(tv1, Fp.ONE); // 2. e1 = tv1 == 1
tv1 = Fp.cmov(tv1, Fp.ZERO, e1); // 3. tv1 = CMOV(tv1, 0, e1) # If Z * u^2 == -1, set tv1 = 0
let xd = Fp.sub(Fp.ONE, tv1); // 4. xd = 1 - tv1
let x1n = Fp.negate(ELL2_J); // 5. x1n = -J
let tv2 = Fp.square(xd); // 6. tv2 = xd^2
let gxd = Fp.mul(tv2, xd); // 7. gxd = tv2 * xd # gxd = xd^3
let gx1 = Fp.mul(tv1, Fp.negate(ELL2_J)); // 8. gx1 = -J * tv1 # x1n + J * xd
gx1 = Fp.mul(gx1, x1n); // 9. gx1 = gx1 * x1n # x1n^2 + J * x1n * xd
gx1 = Fp.add(gx1, tv2); // 10. gx1 = gx1 + tv2 # x1n^2 + J * x1n * xd + xd^2
gx1 = Fp.mul(gx1, x1n); // 11. gx1 = gx1 * x1n # x1n^3 + J * x1n^2 * xd + x1n * xd^2
let tv3 = Fp.square(gxd); // 12. tv3 = gxd^2
tv2 = Fp.mul(gx1, gxd); // 13. tv2 = gx1 * gxd # gx1 * gxd
tv3 = Fp.mul(tv3, tv2); // 14. tv3 = tv3 * tv2 # gx1 * gxd^3
let y1 = Fp.pow(tv3, ELL2_C1); // 15. y1 = tv3^c1 # (gx1 * gxd^3)^((p - 3) / 4)
y1 = Fp.mul(y1, tv2); // 16. y1 = y1 * tv2 # gx1 * gxd * (gx1 * gxd^3)^((p - 3) / 4)
let x2n = Fp.mul(x1n, Fp.negate(tv1)); // 17. x2n = -tv1 * x1n # x2 = x2n / xd = -1 * u^2 * x1n / xd
let y2 = Fp.mul(y1, u); // 18. y2 = y1 * u
y2 = Fp.cmov(y2, Fp.ZERO, e1); // 19. y2 = CMOV(y2, 0, e1)
tv2 = Fp.square(y1); // 20. tv2 = y1^2
tv2 = Fp.mul(tv2, gxd); // 21. tv2 = tv2 * gxd
let e2 = Fp.equals(tv2, gx1); // 22. e2 = tv2 == gx1
let xn = Fp.cmov(x2n, x1n, e2); // 23. xn = CMOV(x2n, x1n, e2) # If e2, x = x1, else x = x2
let y = Fp.cmov(y2, y1, e2); // 24. y = CMOV(y2, y1, e2) # If e2, y = y1, else y = y2
let e3 = Fp.isOdd(y); // 25. e3 = sgn0(y) == 1 # Fix sign of y
y = Fp.cmov(y, Fp.negate(y), e2 !== e3); // 26. y = CMOV(y, -y, e2 XOR e3)
return { xn, xd, yn: y, yd: Fp.ONE }; // 27. return (xn, xd, y, 1)
}
function map_to_curve_elligator2_edwards448(u: bigint) {
let { xn, xd, yn, yd } = map_to_curve_elligator2_curve448(u); // 1. (xn, xd, yn, yd) = map_to_curve_elligator2_curve448(u)
let xn2 = Fp.square(xn); // 2. xn2 = xn^2
let xd2 = Fp.square(xd); // 3. xd2 = xd^2
let xd4 = Fp.square(xd2); // 4. xd4 = xd2^2
let yn2 = Fp.square(yn); // 5. yn2 = yn^2
let yd2 = Fp.square(yd); // 6. yd2 = yd^2
let xEn = Fp.sub(xn2, xd2); // 7. xEn = xn2 - xd2
let tv2 = Fp.sub(xEn, xd2); // 8. tv2 = xEn - xd2
xEn = Fp.mul(xEn, xd2); // 9. xEn = xEn * xd2
xEn = Fp.mul(xEn, yd); // 10. xEn = xEn * yd
xEn = Fp.mul(xEn, yn); // 11. xEn = xEn * yn
xEn = Fp.mul(xEn, 4n); // 12. xEn = xEn * 4
tv2 = Fp.mul(tv2, xn2); // 13. tv2 = tv2 * xn2
tv2 = Fp.mul(tv2, yd2); // 14. tv2 = tv2 * yd2
let tv3 = Fp.mul(yn2, 4n); // 15. tv3 = 4 * yn2
let tv1 = Fp.add(tv3, yd2); // 16. tv1 = tv3 + yd2
tv1 = Fp.mul(tv1, xd4); // 17. tv1 = tv1 * xd4
let xEd = Fp.add(tv1, tv2); // 18. xEd = tv1 + tv2
tv2 = Fp.mul(tv2, xn); // 19. tv2 = tv2 * xn
let tv4 = Fp.mul(xn, xd4); // 20. tv4 = xn * xd4
let yEn = Fp.sub(tv3, yd2); // 21. yEn = tv3 - yd2
yEn = Fp.mul(yEn, tv4); // 22. yEn = yEn * tv4
yEn = Fp.sub(yEn, tv2); // 23. yEn = yEn - tv2
tv1 = Fp.add(xn2, xd2); // 24. tv1 = xn2 + xd2
tv1 = Fp.mul(tv1, xd2); // 25. tv1 = tv1 * xd2
tv1 = Fp.mul(tv1, xd); // 26. tv1 = tv1 * xd
tv1 = Fp.mul(tv1, yn2); // 27. tv1 = tv1 * yn2
tv1 = Fp.mul(tv1, BigInt(-2)); // 28. tv1 = -2 * tv1
let yEd = Fp.add(tv2, tv1); // 29. yEd = tv2 + tv1
tv4 = Fp.mul(tv4, yd2); // 30. tv4 = tv4 * yd2
yEd = Fp.add(yEd, tv4); // 31. yEd = yEd + tv4
tv1 = Fp.mul(xEd, yEd); // 32. tv1 = xEd * yEd
let e = Fp.equals(tv1, Fp.ZERO); // 33. e = tv1 == 0
xEn = Fp.cmov(xEn, Fp.ZERO, e); // 34. xEn = CMOV(xEn, 0, e)
xEd = Fp.cmov(xEd, Fp.ONE, e); // 35. xEd = CMOV(xEd, 1, e)
yEn = Fp.cmov(yEn, Fp.ONE, e); // 36. yEn = CMOV(yEn, 1, e)
yEd = Fp.cmov(yEd, Fp.ONE, e); // 37. yEd = CMOV(yEd, 1, e)
const inv = Fp.invertBatch([xEd, yEd]); // batch division
return { x: Fp.mul(xEn, inv[0]), y: Fp.mul(yEn, inv[1]) }; // 38. return (xEn, xEd, yEn, yEd)
}
const ED448_DEF = { const ED448_DEF = {
// Param: a // Param: a
@@ -189,15 +116,6 @@ const ED448_DEF = {
// square root exists, and the decoding fails. // square root exists, and the decoding fails.
return { isValid: mod(x2 * v, P) === u, value: x }; return { isValid: mod(x2 * v, P) === u, value: x };
}, },
htfDefaults: {
DST: 'edwards448_XOF:SHAKE256_ELL2_RO_',
p: Fp.ORDER,
m: 1,
k: 224,
expand: 'xof',
hash: shake256,
},
mapToCurve: (scalars: bigint[]) => map_to_curve_elligator2_edwards448(scalars[0]),
} as const; } as const;
export const ed448 = twistedEdwards(ED448_DEF); export const ed448 = twistedEdwards(ED448_DEF);
@@ -205,11 +123,11 @@ export const ed448 = twistedEdwards(ED448_DEF);
export const ed448ph = twistedEdwards({ ...ED448_DEF, preHash: shake256_64 }); export const ed448ph = twistedEdwards({ ...ED448_DEF, preHash: shake256_64 });
export const x448 = montgomery({ export const x448 = montgomery({
a24: BigInt(39081), a: BigInt(156326),
montgomeryBits: 448, montgomeryBits: 448,
nByteLength: 57, nByteLength: 57,
P: ed448P, P: ed448P,
Gu: '0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', Gu: BigInt(5),
powPminus2: (x: bigint): bigint => { powPminus2: (x: bigint): bigint => {
const P = ed448P; const P = ed448P;
const Pminus3div4 = ed448_pow_Pminus3div4(x); const Pminus3div4 = ed448_pow_Pminus3div4(x);
@@ -217,6 +135,7 @@ export const x448 = montgomery({
return mod(Pminus3 * x, P); // Pminus3 * x = Pminus2 return mod(Pminus3 * x, P); // Pminus3 * x = Pminus2
}, },
adjustScalarBytes, adjustScalarBytes,
randomBytes,
// The 4-isogeny maps between the Montgomery curve and this Edwards // The 4-isogeny maps between the Montgomery curve and this Edwards
// curve are: // curve are:
// (u, v) = (y^2/x^2, (2 - x^2 - y^2)*y/x^3) // (u, v) = (y^2/x^2, (2 - x^2 - y^2)*y/x^3)
@@ -232,3 +151,93 @@ export const x448 = montgomery({
// return numberToBytesLE(u, 56); // return numberToBytesLE(u, 56);
// }, // },
}); });
// Hash To Curve Elligator2 Map
const ELL2_C1 = (Fp.ORDER - BigInt(3)) / BigInt(4); // 1. c1 = (q - 3) / 4 # Integer arithmetic
const ELL2_J = BigInt(156326);
function map_to_curve_elligator2_curve448(u: bigint) {
let tv1 = Fp.sqr(u); // 1. tv1 = u^2
let e1 = Fp.eql(tv1, Fp.ONE); // 2. e1 = tv1 == 1
tv1 = Fp.cmov(tv1, Fp.ZERO, e1); // 3. tv1 = CMOV(tv1, 0, e1) # If Z * u^2 == -1, set tv1 = 0
let xd = Fp.sub(Fp.ONE, tv1); // 4. xd = 1 - tv1
let x1n = Fp.neg(ELL2_J); // 5. x1n = -J
let tv2 = Fp.sqr(xd); // 6. tv2 = xd^2
let gxd = Fp.mul(tv2, xd); // 7. gxd = tv2 * xd # gxd = xd^3
let gx1 = Fp.mul(tv1, Fp.neg(ELL2_J)); // 8. gx1 = -J * tv1 # x1n + J * xd
gx1 = Fp.mul(gx1, x1n); // 9. gx1 = gx1 * x1n # x1n^2 + J * x1n * xd
gx1 = Fp.add(gx1, tv2); // 10. gx1 = gx1 + tv2 # x1n^2 + J * x1n * xd + xd^2
gx1 = Fp.mul(gx1, x1n); // 11. gx1 = gx1 * x1n # x1n^3 + J * x1n^2 * xd + x1n * xd^2
let tv3 = Fp.sqr(gxd); // 12. tv3 = gxd^2
tv2 = Fp.mul(gx1, gxd); // 13. tv2 = gx1 * gxd # gx1 * gxd
tv3 = Fp.mul(tv3, tv2); // 14. tv3 = tv3 * tv2 # gx1 * gxd^3
let y1 = Fp.pow(tv3, ELL2_C1); // 15. y1 = tv3^c1 # (gx1 * gxd^3)^((p - 3) / 4)
y1 = Fp.mul(y1, tv2); // 16. y1 = y1 * tv2 # gx1 * gxd * (gx1 * gxd^3)^((p - 3) / 4)
let x2n = Fp.mul(x1n, Fp.neg(tv1)); // 17. x2n = -tv1 * x1n # x2 = x2n / xd = -1 * u^2 * x1n / xd
let y2 = Fp.mul(y1, u); // 18. y2 = y1 * u
y2 = Fp.cmov(y2, Fp.ZERO, e1); // 19. y2 = CMOV(y2, 0, e1)
tv2 = Fp.sqr(y1); // 20. tv2 = y1^2
tv2 = Fp.mul(tv2, gxd); // 21. tv2 = tv2 * gxd
let e2 = Fp.eql(tv2, gx1); // 22. e2 = tv2 == gx1
let xn = Fp.cmov(x2n, x1n, e2); // 23. xn = CMOV(x2n, x1n, e2) # If e2, x = x1, else x = x2
let y = Fp.cmov(y2, y1, e2); // 24. y = CMOV(y2, y1, e2) # If e2, y = y1, else y = y2
let e3 = Fp.isOdd(y); // 25. e3 = sgn0(y) == 1 # Fix sign of y
y = Fp.cmov(y, Fp.neg(y), e2 !== e3); // 26. y = CMOV(y, -y, e2 XOR e3)
return { xn, xd, yn: y, yd: Fp.ONE }; // 27. return (xn, xd, y, 1)
}
function map_to_curve_elligator2_edwards448(u: bigint) {
let { xn, xd, yn, yd } = map_to_curve_elligator2_curve448(u); // 1. (xn, xd, yn, yd) = map_to_curve_elligator2_curve448(u)
let xn2 = Fp.sqr(xn); // 2. xn2 = xn^2
let xd2 = Fp.sqr(xd); // 3. xd2 = xd^2
let xd4 = Fp.sqr(xd2); // 4. xd4 = xd2^2
let yn2 = Fp.sqr(yn); // 5. yn2 = yn^2
let yd2 = Fp.sqr(yd); // 6. yd2 = yd^2
let xEn = Fp.sub(xn2, xd2); // 7. xEn = xn2 - xd2
let tv2 = Fp.sub(xEn, xd2); // 8. tv2 = xEn - xd2
xEn = Fp.mul(xEn, xd2); // 9. xEn = xEn * xd2
xEn = Fp.mul(xEn, yd); // 10. xEn = xEn * yd
xEn = Fp.mul(xEn, yn); // 11. xEn = xEn * yn
xEn = Fp.mul(xEn, _4n); // 12. xEn = xEn * 4
tv2 = Fp.mul(tv2, xn2); // 13. tv2 = tv2 * xn2
tv2 = Fp.mul(tv2, yd2); // 14. tv2 = tv2 * yd2
let tv3 = Fp.mul(yn2, _4n); // 15. tv3 = 4 * yn2
let tv1 = Fp.add(tv3, yd2); // 16. tv1 = tv3 + yd2
tv1 = Fp.mul(tv1, xd4); // 17. tv1 = tv1 * xd4
let xEd = Fp.add(tv1, tv2); // 18. xEd = tv1 + tv2
tv2 = Fp.mul(tv2, xn); // 19. tv2 = tv2 * xn
let tv4 = Fp.mul(xn, xd4); // 20. tv4 = xn * xd4
let yEn = Fp.sub(tv3, yd2); // 21. yEn = tv3 - yd2
yEn = Fp.mul(yEn, tv4); // 22. yEn = yEn * tv4
yEn = Fp.sub(yEn, tv2); // 23. yEn = yEn - tv2
tv1 = Fp.add(xn2, xd2); // 24. tv1 = xn2 + xd2
tv1 = Fp.mul(tv1, xd2); // 25. tv1 = tv1 * xd2
tv1 = Fp.mul(tv1, xd); // 26. tv1 = tv1 * xd
tv1 = Fp.mul(tv1, yn2); // 27. tv1 = tv1 * yn2
tv1 = Fp.mul(tv1, BigInt(-2)); // 28. tv1 = -2 * tv1
let yEd = Fp.add(tv2, tv1); // 29. yEd = tv2 + tv1
tv4 = Fp.mul(tv4, yd2); // 30. tv4 = tv4 * yd2
yEd = Fp.add(yEd, tv4); // 31. yEd = yEd + tv4
tv1 = Fp.mul(xEd, yEd); // 32. tv1 = xEd * yEd
let e = Fp.eql(tv1, Fp.ZERO); // 33. e = tv1 == 0
xEn = Fp.cmov(xEn, Fp.ZERO, e); // 34. xEn = CMOV(xEn, 0, e)
xEd = Fp.cmov(xEd, Fp.ONE, e); // 35. xEd = CMOV(xEd, 1, e)
yEn = Fp.cmov(yEn, Fp.ONE, e); // 36. yEn = CMOV(yEn, 1, e)
yEd = Fp.cmov(yEd, Fp.ONE, e); // 37. yEd = CMOV(yEd, 1, e)
const inv = Fp.invertBatch([xEd, yEd]); // batch division
return { x: Fp.mul(xEn, inv[0]), y: Fp.mul(yEn, inv[1]) }; // 38. return (xEn, xEd, yEn, yEd)
}
const { hashToCurve, encodeToCurve } = htf.createHasher(
ed448.ExtendedPoint,
(scalars: bigint[]) => map_to_curve_elligator2_edwards448(scalars[0]),
{
DST: 'edwards448_XOF:SHAKE256_ELL2_RO_',
encodeDST: 'edwards448_XOF:SHAKE256_ELL2_NU_',
p: Fp.ORDER,
m: 1,
k: 224,
expand: 'xof',
hash: shake256,
}
);
export { hashToCurve, encodeToCurve };

View File

@@ -3,7 +3,7 @@ import { sha512 } from '@noble/hashes/sha512';
import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'; import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
import { twistedEdwards } from './abstract/edwards.js'; import { twistedEdwards } from './abstract/edwards.js';
import { blake2s } from '@noble/hashes/blake2s'; import { blake2s } from '@noble/hashes/blake2s';
import { Fp } from './abstract/modular.js'; import { Field } from './abstract/modular.js';
/** /**
* jubjub Twisted Edwards curve. * jubjub Twisted Edwards curve.
@@ -17,7 +17,7 @@ export const jubjub = twistedEdwards({
d: BigInt('0x2a9318e74bfa2b48f5fd9207e6bd7fd4292d7f6d37579d2601065fd6d6343eb1'), d: BigInt('0x2a9318e74bfa2b48f5fd9207e6bd7fd4292d7f6d37579d2601065fd6d6343eb1'),
// Finite field 𝔽p over which we'll do calculations // Finite field 𝔽p over which we'll do calculations
// Same value as bls12-381 Fr (not Fp) // Same value as bls12-381 Fr (not Fp)
Fp: Fp(BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001')), Fp: Field(BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001')),
// Subgroup order: how many points curve has // Subgroup order: how many points curve has
n: BigInt('0xe7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7'), n: BigInt('0xe7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7'),
// Cofactor // Cofactor
@@ -39,7 +39,7 @@ export function groupHash(tag: Uint8Array, personalization: Uint8Array) {
h.update(GH_FIRST_BLOCK); h.update(GH_FIRST_BLOCK);
h.update(tag); h.update(tag);
// NOTE: returns ExtendedPoint, in case it will be multiplied later // NOTE: returns ExtendedPoint, in case it will be multiplied later
let p = jubjub.ExtendedPoint.fromAffine(jubjub.Point.fromHex(h.digest())); let p = jubjub.ExtendedPoint.fromHex(h.digest());
// NOTE: cannot replace with isSmallOrder, returns Point*8 // NOTE: cannot replace with isSmallOrder, returns Point*8
p = p.multiply(jubjub.CURVE.h); p = p.multiply(jubjub.CURVE.h);
if (p.equals(jubjub.ExtendedPoint.ZERO)) throw new Error('Point has small order'); if (p.equals(jubjub.ExtendedPoint.ZERO)) throw new Error('Point has small order');

View File

@@ -1,25 +0,0 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { createCurve } from './_shortw_utils.js';
import { sha256 } from '@noble/hashes/sha256';
import { Fp } from './abstract/modular.js';
// NIST secp192r1 aka P192
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/secg/secp192r1
export const P192 = createCurve(
{
// Params: a, b
a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffc'),
b: BigInt('0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1'),
// Field over which we'll do calculations; 2n ** 192n - 2n ** 64n - 1n
Fp: Fp(BigInt('0xfffffffffffffffffffffffffffffffeffffffffffffffff')),
// Curve order, total count of valid points in the field.
n: BigInt('0xffffffffffffffffffffffff99def836146bc9b1b4d22831'),
// Base point (x, y) aka generator point
Gx: BigInt('0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012'),
Gy: BigInt('0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811'),
h: BigInt(1),
lowS: false,
} as const,
sha256
);
export const secp192r1 = P192;

View File

@@ -1,25 +0,0 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { createCurve } from './_shortw_utils.js';
import { sha224 } from '@noble/hashes/sha256';
import { Fp } from './abstract/modular.js';
// NIST secp224r1 aka P224
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-224
export const P224 = createCurve(
{
// Params: a, b
a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffffffffffe'),
b: BigInt('0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4'),
// Field over which we'll do calculations; 2n**224n - 2n**96n + 1n
Fp: Fp(BigInt('0xffffffffffffffffffffffffffffffff000000000000000000000001')),
// Curve order, total count of valid points in the field
n: BigInt('0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d'),
// Base point (x, y) aka generator point
Gx: BigInt('0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21'),
Gy: BigInt('0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34'),
h: BigInt(1),
lowS: false,
} as const,
sha224
);
export const secp224r1 = P224;

View File

@@ -1,8 +1,9 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { createCurve } from './_shortw_utils.js'; import { createCurve } from './_shortw_utils.js';
import { sha256 } from '@noble/hashes/sha256'; import { sha256 } from '@noble/hashes/sha256';
import { Fp as Field } from './abstract/modular.js'; import { Field } from './abstract/modular.js';
import { mapToCurveSimpleSWU } from './abstract/weierstrass.js'; import { mapToCurveSimpleSWU } from './abstract/weierstrass.js';
import * as htf from './abstract/hash-to-curve.js';
// NIST secp256r1 aka P256 // NIST secp256r1 aka P256
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-256 // https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-256
@@ -31,16 +32,22 @@ export const P256 = createCurve(
Gy: BigInt('0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5'), Gy: BigInt('0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5'),
h: BigInt(1), h: BigInt(1),
lowS: false, lowS: false,
mapToCurve: (scalars: bigint[]) => mapSWU(scalars[0]), } as const,
htfDefaults: { sha256
);
export const secp256r1 = P256;
const { hashToCurve, encodeToCurve } = htf.createHasher(
secp256r1.ProjectivePoint,
(scalars: bigint[]) => mapSWU(scalars[0]),
{
DST: 'P256_XMD:SHA-256_SSWU_RO_', DST: 'P256_XMD:SHA-256_SSWU_RO_',
encodeDST: 'P256_XMD:SHA-256_SSWU_NU_',
p: Fp.ORDER, p: Fp.ORDER,
m: 1, m: 1,
k: 128, k: 128,
expand: 'xmd', expand: 'xmd',
hash: sha256, hash: sha256,
}, }
} as const,
sha256
); );
export const secp256r1 = P256; export { hashToCurve, encodeToCurve };

View File

@@ -1,8 +1,9 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { createCurve } from './_shortw_utils.js'; import { createCurve } from './_shortw_utils.js';
import { sha384 } from '@noble/hashes/sha512'; import { sha384 } from '@noble/hashes/sha512';
import { Fp as Field } from './abstract/modular.js'; import { Field } from './abstract/modular.js';
import { mapToCurveSimpleSWU } from './abstract/weierstrass.js'; import { mapToCurveSimpleSWU } from './abstract/weierstrass.js';
import * as htf from './abstract/hash-to-curve.js';
// NIST secp384r1 aka P384 // NIST secp384r1 aka P384
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-384 // https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-384
@@ -35,16 +36,22 @@ export const P384 = createCurve({
Gy: BigInt('0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f'), Gy: BigInt('0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f'),
h: BigInt(1), h: BigInt(1),
lowS: false, lowS: false,
mapToCurve: (scalars: bigint[]) => mapSWU(scalars[0]), } as const,
htfDefaults: { sha384
);
export const secp384r1 = P384;
const { hashToCurve, encodeToCurve } = htf.createHasher(
secp384r1.ProjectivePoint,
(scalars: bigint[]) => mapSWU(scalars[0]),
{
DST: 'P384_XMD:SHA-384_SSWU_RO_', DST: 'P384_XMD:SHA-384_SSWU_RO_',
encodeDST: 'P384_XMD:SHA-384_SSWU_NU_',
p: Fp.ORDER, p: Fp.ORDER,
m: 1, m: 1,
k: 192, k: 192,
expand: 'xmd', expand: 'xmd',
hash: sha384, hash: sha384,
}, }
} as const,
sha384
); );
export const secp384r1 = P384; export { hashToCurve, encodeToCurve };

View File

@@ -1,9 +1,9 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { createCurve } from './_shortw_utils.js'; import { createCurve } from './_shortw_utils.js';
import { sha512 } from '@noble/hashes/sha512'; import { sha512 } from '@noble/hashes/sha512';
import { bytesToHex, PrivKey } from './abstract/utils.js'; import { Field } from './abstract/modular.js';
import { Fp as Field } from './abstract/modular.js';
import { mapToCurveSimpleSWU } from './abstract/weierstrass.js'; import { mapToCurveSimpleSWU } from './abstract/weierstrass.js';
import * as htf from './abstract/hash-to-curve.js';
// NIST secp521r1 aka P521 // NIST secp521r1 aka P521
// Note that it's 521, which differs from 512 of its hash function. // Note that it's 521, which differs from 512 of its hash function.
@@ -37,25 +37,21 @@ export const P521 = createCurve({
Gy: BigInt('0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650'), Gy: BigInt('0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650'),
h: BigInt(1), h: BigInt(1),
lowS: false, lowS: false,
// P521 keys could be 130, 131, 132 bytes - which doesn't play nicely. allowedPrivateKeyLengths: [130, 131, 132] // P521 keys are variable-length. Normalize to 132b
// We ensure all keys are 132 bytes. } as const, sha512);
// Does not replace validation; invalid keys would still be rejected. export const secp521r1 = P521;
normalizePrivateKey(key: PrivKey) {
if (typeof key === 'bigint') return key; const { hashToCurve, encodeToCurve } = htf.createHasher(
if (key instanceof Uint8Array) key = bytesToHex(key); secp521r1.ProjectivePoint,
if (typeof key !== 'string' || !([130, 131, 132].includes(key.length))) { (scalars: bigint[]) => mapSWU(scalars[0]),
throw new Error('Invalid key'); {
}
return key.padStart(66 * 2, '0');
},
mapToCurve: (scalars: bigint[]) => mapSWU(scalars[0]),
htfDefaults: {
DST: 'P521_XMD:SHA-512_SSWU_RO_', DST: 'P521_XMD:SHA-512_SSWU_RO_',
encodeDST: 'P521_XMD:SHA-512_SSWU_NU_',
p: Fp.ORDER, p: Fp.ORDER,
m: 1, m: 1,
k: 256, k: 256,
expand: 'xmd', expand: 'xmd',
hash: sha512, hash: sha512,
}, }
} as const, sha512); );
export const secp521r1 = P521; export { hashToCurve, encodeToCurve };

View File

@@ -11,7 +11,7 @@ export const q = BigInt('0x40000000000000000000000000000000224698fc0994a8dd8c46e
export const pallas = weierstrass({ export const pallas = weierstrass({
a: BigInt(0), a: BigInt(0),
b: BigInt(5), b: BigInt(5),
Fp: mod.Fp(p), Fp: mod.Field(p),
n: q, n: q,
Gx: mod.mod(BigInt(-1), p), Gx: mod.mod(BigInt(-1), p),
Gy: BigInt(2), Gy: BigInt(2),
@@ -22,7 +22,7 @@ export const pallas = weierstrass({
export const vesta = weierstrass({ export const vesta = weierstrass({
a: BigInt(0), a: BigInt(0),
b: BigInt(5), b: BigInt(5),
Fp: mod.Fp(q), Fp: mod.Field(q),
n: p, n: p,
Gx: mod.mod(BigInt(-1), q), Gx: mod.mod(BigInt(-1), q),
Gy: BigInt(2), Gy: BigInt(2),

View File

@@ -1,26 +1,12 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { sha256 } from '@noble/hashes/sha256'; import { sha256 } from '@noble/hashes/sha256';
import { Fp as Field, mod, pow2 } from './abstract/modular.js';
import { createCurve } from './_shortw_utils.js';
import { PointType, mapToCurveSimpleSWU } from './abstract/weierstrass.js';
import {
ensureBytes,
concatBytes,
Hex,
hexToBytes,
bytesToNumberBE,
PrivKey,
} from './abstract/utils.js';
import { randomBytes } from '@noble/hashes/utils'; import { randomBytes } from '@noble/hashes/utils';
import { isogenyMap } from './abstract/hash-to-curve.js'; import { Field, mod, pow2 } from './abstract/modular.js';
import { ProjPointType as PointType, mapToCurveSimpleSWU } from './abstract/weierstrass.js';
/** import type { Hex, PrivKey } from './abstract/utils.js';
* secp256k1 belongs to Koblitz curves: it has efficiently computable endomorphism. import { bytesToNumberBE, concatBytes, ensureBytes, numberToBytesBE } from './abstract/utils.js';
* Endomorphism uses 2x less RAM, speeds up precomputation by 2x and ECDH / key recovery by 20%. import * as htf from './abstract/hash-to-curve.js';
* Should always be used for Projective's double-and-add multiplication. import { createCurve } from './_shortw_utils.js';
* For affines cached multiplication, it trades off 1/2 init time & 1/3 ram for 20% perf hit.
* https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066
*/
const secp256k1P = BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'); const secp256k1P = BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f');
const secp256k1N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'); const secp256k1N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141');
@@ -29,10 +15,7 @@ const _2n = BigInt(2);
const divNearest = (a: bigint, b: bigint) => (a + b / _2n) / b; const divNearest = (a: bigint, b: bigint) => (a + b / _2n) / b;
/** /**
* Allows to compute square root √y 2x faster. * √n = n^((p+1)/4) for fields p = 3 mod 4. We unwrap the loop and multiply bit-by-bit.
* To calculate √y, we need to exponentiate it to a very big number:
* `y² = x³ + ax + b; y = y² ^ (p+1)/4`
* We are unwrapping the loop and multiplying it bit-by-bit.
* (P+1n/4n).toString(2) would produce bits [223x 1, 0, 22x 1, 4x 0, 11, 00] * (P+1n/4n).toString(2) would produce bits [223x 1, 0, 22x 1, 4x 0, 11, 00]
*/ */
function sqrtMod(y: bigint): bigint { function sqrtMod(y: bigint): bigint {
@@ -55,14 +38,183 @@ function sqrtMod(y: bigint): bigint {
const t1 = (pow2(b223, _23n, P) * b22) % P; const t1 = (pow2(b223, _23n, P) * b22) % P;
const t2 = (pow2(t1, _6n, P) * b2) % P; const t2 = (pow2(t1, _6n, P) * b2) % P;
const root = pow2(t2, _2n, P); const root = pow2(t2, _2n, P);
if (!Fp.equals(Fp.square(root), y)) throw new Error('Cannot find square root'); if (!Fp.eql(Fp.sqr(root), y)) throw new Error('Cannot find square root');
return root; return root;
} }
const Fp = Field(secp256k1P, undefined, undefined, { sqrt: sqrtMod }); const Fp = Field(secp256k1P, undefined, undefined, { sqrt: sqrtMod });
type Fp = bigint;
const isoMap = isogenyMap( export const secp256k1 = createCurve(
{
a: BigInt(0), // equation params: a, b
b: BigInt(7), // Seem to be rigid: bitcointalk.org/index.php?topic=289795.msg3183975#msg3183975
Fp, // Field's prime: 2n**256n - 2n**32n - 2n**9n - 2n**8n - 2n**7n - 2n**6n - 2n**4n - 1n
n: secp256k1N, // Curve order, total count of valid points in the field
// Base point (x, y) aka generator point
Gx: BigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'),
Gy: BigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424'),
h: BigInt(1), // Cofactor
lowS: true, // Allow only low-S signatures by default in sign() and verify()
/**
* secp256k1 belongs to Koblitz curves: it has efficiently computable endomorphism.
* Endomorphism uses 2x less RAM, speeds up precomputation by 2x and ECDH / key recovery by 20%.
* For precomputed wNAF it trades off 1/2 init time & 1/3 ram for 20% perf hit.
* Explanation: https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066
*/
endo: {
beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'),
splitScalar: (k: bigint) => {
const n = secp256k1N;
const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15');
const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3');
const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8');
const b2 = a1;
const POW_2_128 = BigInt('0x100000000000000000000000000000000'); // (2n**128n).toString(16)
const c1 = divNearest(b2 * k, n);
const c2 = divNearest(-b1 * k, n);
let k1 = mod(k - c1 * a1 - c2 * a2, n);
let k2 = mod(-c1 * b1 - c2 * b2, n);
const k1neg = k1 > POW_2_128;
const k2neg = k2 > POW_2_128;
if (k1neg) k1 = n - k1;
if (k2neg) k2 = n - k2;
if (k1 > POW_2_128 || k2 > POW_2_128) {
throw new Error('splitScalar: Endomorphism failed, k=' + k);
}
return { k1neg, k1, k2neg, k2 };
},
},
},
sha256
);
// Schnorr signatures are superior to ECDSA from above. Below is Schnorr-specific BIP0340 code.
// https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
const _0n = BigInt(0);
const fe = (x: bigint) => typeof x === 'bigint' && _0n < x && x < secp256k1P;
const ge = (x: bigint) => typeof x === 'bigint' && _0n < x && x < secp256k1N;
/** An object mapping tags to their tagged hash prefix of [SHA256(tag) | SHA256(tag)] */
const TAGGED_HASH_PREFIXES: { [tag: string]: Uint8Array } = {};
function taggedHash(tag: string, ...messages: Uint8Array[]): Uint8Array {
let tagP = TAGGED_HASH_PREFIXES[tag];
if (tagP === undefined) {
const tagH = sha256(Uint8Array.from(tag, (c) => c.charCodeAt(0)));
tagP = concatBytes(tagH, tagH);
TAGGED_HASH_PREFIXES[tag] = tagP;
}
return sha256(concatBytes(tagP, ...messages));
}
// ECDSA compact points are 33-byte. Schnorr is 32: we strip first byte 0x02 or 0x03
const pointToBytes = (point: PointType<bigint>) => point.toRawBytes(true).slice(1);
const numTo32b = (n: bigint) => numberToBytesBE(n, 32);
const modP = (x: bigint) => mod(x, secp256k1P);
const modN = (x: bigint) => mod(x, secp256k1N);
const Point = secp256k1.ProjectivePoint;
const GmulAdd = (Q: PointType<bigint>, a: bigint, b: bigint) =>
Point.BASE.multiplyAndAddUnsafe(Q, a, b);
// Calculate point, scalar and bytes
function schnorrGetExtPubKey(priv: PrivKey) {
let d_ = secp256k1.utils.normPrivateKeyToScalar(priv); // same method executed in fromPrivateKey
let p = Point.fromPrivateKey(d_); // P = d'⋅G; 0 < d' < n check is done inside
const scalar = p.hasEvenY() ? d_ : modN(-d_);
return { scalar: scalar, bytes: pointToBytes(p) };
}
/**
* lift_x from BIP340. Convert 32-byte x coordinate to elliptic curve point.
* @returns valid point checked for being on-curve
*/
function lift_x(x: bigint): PointType<bigint> {
if (!fe(x)) throw new Error('bad x: need 0 < x < p'); // Fail if x ≥ p.
const xx = modP(x * x);
const c = modP(xx * x + BigInt(7)); // Let c = x³ + 7 mod p.
let y = sqrtMod(c); // Let y = c^(p+1)/4 mod p.
if (y % _2n !== _0n) y = modP(-y); // Return the unique point P such that x(P) = x and
const p = new Point(x, y, _1n); // y(P) = y if y mod 2 = 0 or y(P) = p-y otherwise.
p.assertValidity();
return p;
}
/**
* Create tagged hash, convert it to bigint, reduce modulo-n.
*/
function challenge(...args: Uint8Array[]): bigint {
return modN(bytesToNumberBE(taggedHash('BIP0340/challenge', ...args)));
}
/**
* Schnorr public key is just `x` coordinate of Point as per BIP340.
*/
function schnorrGetPublicKey(privateKey: Hex): Uint8Array {
return schnorrGetExtPubKey(privateKey).bytes; // d'=int(sk). Fail if d'=0 or d'≥n. Ret bytes(d'⋅G)
}
/**
* Creates Schnorr signature as per BIP340. Verifies itself before returning anything.
* auxRand is optional and is not the sole source of k generation: bad CSPRNG won't be dangerous.
*/
function schnorrSign(
message: Hex,
privateKey: PrivKey,
auxRand: Hex = randomBytes(32)
): Uint8Array {
const m = ensureBytes('message', message);
const { bytes: px, scalar: d } = schnorrGetExtPubKey(privateKey); // checks for isWithinCurveOrder
const a = ensureBytes('auxRand', auxRand, 32); // Auxiliary random data a: a 32-byte array
const t = numTo32b(d ^ bytesToNumberBE(taggedHash('BIP0340/aux', a))); // Let t be the byte-wise xor of bytes(d) and hash/aux(a)
const rand = taggedHash('BIP0340/nonce', t, px, m); // Let rand = hash/nonce(t || bytes(P) || m)
const k_ = modN(bytesToNumberBE(rand)); // Let k' = int(rand) mod n
if (k_ === _0n) throw new Error('sign failed: k is zero'); // Fail if k' = 0.
const { bytes: rx, scalar: k } = schnorrGetExtPubKey(k_); // Let R = k'⋅G.
const e = challenge(rx, px, m); // Let e = int(hash/challenge(bytes(R) || bytes(P) || m)) mod n.
const sig = new Uint8Array(64); // Let sig = bytes(R) || bytes((k + ed) mod n).
sig.set(rx, 0);
sig.set(numTo32b(modN(k + e * d)), 32);
// If Verify(bytes(P), m, sig) (see below) returns failure, abort
if (!schnorrVerify(sig, m, px)) throw new Error('sign: Invalid signature produced');
return sig;
}
/**
* Verifies Schnorr signature.
* Will swallow errors & return false except for initial type validation of arguments.
*/
function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): boolean {
const sig = ensureBytes('signature', signature, 64);
const m = ensureBytes('message', message);
const pub = ensureBytes('publicKey', publicKey, 32);
try {
const P = lift_x(bytesToNumberBE(pub)); // P = lift_x(int(pk)); fail if that fails
const r = bytesToNumberBE(sig.subarray(0, 32)); // Let r = int(sig[0:32]); fail if r ≥ p.
if (!fe(r)) return false;
const s = bytesToNumberBE(sig.subarray(32, 64)); // Let s = int(sig[32:64]); fail if s ≥ n.
if (!ge(s)) return false;
const e = challenge(numTo32b(r), pointToBytes(P), m); // int(challenge(bytes(r)||bytes(P)||m))%n
const R = GmulAdd(P, s, modN(-e)); // R = s⋅G - e⋅P
if (!R || !R.hasEvenY() || R.toAffine().x !== r) return false; // -eP == (n-e)P
return true; // Fail if is_infinite(R) / not has_even_y(R) / x(R) ≠ r.
} catch (error) {
return false;
}
}
export const schnorr = {
getPublicKey: schnorrGetPublicKey,
sign: schnorrSign,
verify: schnorrVerify,
utils: {
randomPrivateKey: secp256k1.utils.randomPrivateKey,
lift_x,
pointToBytes,
numberToBytesBE,
bytesToNumberBE,
taggedHash,
mod,
},
};
const isoMap = htf.isogenyMap(
Fp, Fp,
[ [
// xNum // xNum
@@ -92,226 +244,26 @@ const isoMap = isogenyMap(
'0x6484aa716545ca2cf3a70c3fa8fe337e0a3d21162f0d6299a7bf8192bfd2a76f', '0x6484aa716545ca2cf3a70c3fa8fe337e0a3d21162f0d6299a7bf8192bfd2a76f',
'0x0000000000000000000000000000000000000000000000000000000000000001', // LAST 1 '0x0000000000000000000000000000000000000000000000000000000000000001', // LAST 1
], ],
].map((i) => i.map((j) => BigInt(j))) as [Fp[], Fp[], Fp[], Fp[]] ].map((i) => i.map((j) => BigInt(j))) as [bigint[], bigint[], bigint[], bigint[]]
); );
const mapSWU = mapToCurveSimpleSWU(Fp, { const mapSWU = mapToCurveSimpleSWU(Fp, {
A: BigInt('0x3f8731abdd661adca08a5558f0f5d272e953d363cb6f0e5d405447c01a444533'), A: BigInt('0x3f8731abdd661adca08a5558f0f5d272e953d363cb6f0e5d405447c01a444533'),
B: BigInt('1771'), B: BigInt('1771'),
Z: Fp.create(BigInt('-11')), Z: Fp.create(BigInt('-11')),
}); });
export const { hashToCurve, encodeToCurve } = htf.createHasher(
export const secp256k1 = createCurve( secp256k1.ProjectivePoint,
{ (scalars: bigint[]) => {
// Params: a, b
// Seem to be rigid https://bitcointalk.org/index.php?topic=289795.msg3183975#msg3183975
a: BigInt(0),
b: BigInt(7),
// Field over which we'll do calculations;
// 2n**256n - 2n**32n - 2n**9n - 2n**8n - 2n**7n - 2n**6n - 2n**4n - 1n
Fp,
// Curve order, total count of valid points in the field
n: secp256k1N,
// Base point (x, y) aka generator point
Gx: BigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'),
Gy: BigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424'),
h: BigInt(1),
// Alllow only low-S signatures by default in sign() and verify()
lowS: true,
endo: {
// Params taken from https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066
beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'),
splitScalar: (k: bigint) => {
const n = secp256k1N;
const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15');
const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3');
const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8');
const b2 = a1;
const POW_2_128 = BigInt('0x100000000000000000000000000000000'); // (2n**128n).toString(16)
const c1 = divNearest(b2 * k, n);
const c2 = divNearest(-b1 * k, n);
let k1 = mod(k - c1 * a1 - c2 * a2, n);
let k2 = mod(-c1 * b1 - c2 * b2, n);
const k1neg = k1 > POW_2_128;
const k2neg = k2 > POW_2_128;
if (k1neg) k1 = n - k1;
if (k2neg) k2 = n - k2;
if (k1 > POW_2_128 || k2 > POW_2_128) {
throw new Error('splitScalar: Endomorphism failed, k=' + k);
}
return { k1neg, k1, k2neg, k2 };
},
},
mapToCurve: (scalars: bigint[]) => {
const { x, y } = mapSWU(Fp.create(scalars[0])); const { x, y } = mapSWU(Fp.create(scalars[0]));
return isoMap(x, y); return isoMap(x, y);
}, },
htfDefaults: { {
DST: 'secp256k1_XMD:SHA-256_SSWU_RO_', DST: 'secp256k1_XMD:SHA-256_SSWU_RO_',
encodeDST: 'secp256k1_XMD:SHA-256_SSWU_NU_',
p: Fp.ORDER, p: Fp.ORDER,
m: 1, m: 1,
k: 128, k: 128,
expand: 'xmd', expand: 'xmd',
hash: sha256, hash: sha256,
}, }
},
sha256
); );
// Schnorr
const _0n = BigInt(0);
const numTo32b = secp256k1.utils._bigintToBytes;
const numTo32bStr = secp256k1.utils._bigintToString;
const normalizePrivateKey = secp256k1.utils._normalizePrivateKey;
// TODO: export?
function normalizePublicKey(publicKey: Hex | PointType<bigint>): PointType<bigint> {
if (publicKey instanceof secp256k1.Point) {
publicKey.assertValidity();
return publicKey;
} else {
const bytes = ensureBytes(publicKey);
// Schnorr is 32 bytes
if (bytes.length !== 32) throw new Error('Schnorr pubkeys must be 32 bytes');
const x = bytesToNumberBE(bytes);
if (!isValidFieldElement(x)) throw new Error('Point is not on curve');
const y2 = secp256k1.utils._weierstrassEquation(x); // y² = x³ + ax + b
let y = sqrtMod(y2); // y = y² ^ (p+1)/4
const isYOdd = (y & _1n) === _1n;
// Schnorr
if (isYOdd) y = secp256k1.CURVE.Fp.negate(y);
const point = new secp256k1.Point(x, y);
point.assertValidity();
return point;
}
}
const isWithinCurveOrder = secp256k1.utils._isWithinCurveOrder;
const isValidFieldElement = secp256k1.utils._isValidFieldElement;
const TAGS = {
challenge: 'BIP0340/challenge',
aux: 'BIP0340/aux',
nonce: 'BIP0340/nonce',
} as const;
/** An object mapping tags to their tagged hash prefix of [SHA256(tag) | SHA256(tag)] */
const TAGGED_HASH_PREFIXES: { [tag: string]: Uint8Array } = {};
export function taggedHash(tag: string, ...messages: Uint8Array[]): Uint8Array {
let tagP = TAGGED_HASH_PREFIXES[tag];
if (tagP === undefined) {
const tagH = sha256(Uint8Array.from(tag, (c) => c.charCodeAt(0)));
tagP = concatBytes(tagH, tagH);
TAGGED_HASH_PREFIXES[tag] = tagP;
}
return sha256(concatBytes(tagP, ...messages));
}
const toRawX = (point: PointType<bigint>) => point.toRawBytes(true).slice(1);
// Schnorr signatures are superior to ECDSA from above.
// Below is Schnorr-specific code as per BIP0340.
function schnorrChallengeFinalize(ch: Uint8Array): bigint {
return mod(bytesToNumberBE(ch), secp256k1.CURVE.n);
}
// Do we need this at all for Schnorr?
class SchnorrSignature {
constructor(readonly r: bigint, readonly s: bigint) {
this.assertValidity();
}
static fromHex(hex: Hex) {
const bytes = ensureBytes(hex);
const len = 32; // group length
if (bytes.length !== 2 * len)
throw new TypeError(
`SchnorrSignature.fromHex: expected ${2 * len} bytes, not ${bytes.length}`
);
const r = bytesToNumberBE(bytes.subarray(0, len));
const s = bytesToNumberBE(bytes.subarray(len, 2 * len));
return new SchnorrSignature(r, s);
}
assertValidity() {
const { r, s } = this;
if (!isValidFieldElement(r) || !isWithinCurveOrder(s)) throw new Error('Invalid signature');
}
toHex(): string {
return numTo32bStr(this.r) + numTo32bStr(this.s);
}
toRawBytes(): Uint8Array {
return hexToBytes(this.toHex());
}
}
function schnorrGetScalar(priv: bigint) {
const point = secp256k1.Point.fromPrivateKey(priv);
const scalar = point.hasEvenY() ? priv : secp256k1.CURVE.n - priv;
return { point, scalar, x: toRawX(point) };
}
/**
* Synchronously creates Schnorr signature. Improved security: verifies itself before
* producing an output.
* @param msg message (not message hash)
* @param privateKey private key
* @param auxRand random bytes that would be added to k. Bad RNG won't break it.
*/
function schnorrSign(
message: Hex,
privateKey: PrivKey,
auxRand: Hex = randomBytes(32)
): Uint8Array {
if (message == null) throw new TypeError(`sign: Expected valid message, not "${message}"`);
const m = ensureBytes(message);
// checks for isWithinCurveOrder
const { x: px, scalar: d } = schnorrGetScalar(normalizePrivateKey(privateKey));
const rand = ensureBytes(auxRand);
if (rand.length !== 32) throw new TypeError('sign: Expected 32 bytes of aux randomness');
const tag = taggedHash;
const t0h = tag(TAGS.aux, rand);
const t = numTo32b(d ^ bytesToNumberBE(t0h));
const k0h = tag(TAGS.nonce, t, px, m);
const k0 = mod(bytesToNumberBE(k0h), secp256k1.CURVE.n);
if (k0 === _0n) throw new Error('sign: Creation of signature failed. k is zero');
const { point: R, x: rx, scalar: k } = schnorrGetScalar(k0);
const e = schnorrChallengeFinalize(tag(TAGS.challenge, rx, px, m));
const sig = new SchnorrSignature(R.x, mod(k + e * d, secp256k1.CURVE.n)).toRawBytes();
if (!schnorrVerify(sig, m, px)) throw new Error('sign: Invalid signature produced');
return sig;
}
/**
* Verifies Schnorr signature synchronously.
*/
function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): boolean {
try {
const raw = signature instanceof SchnorrSignature;
const sig: SchnorrSignature = raw ? signature : SchnorrSignature.fromHex(signature);
if (raw) sig.assertValidity(); // just in case
const { r, s } = sig;
const m = ensureBytes(message);
const P = normalizePublicKey(publicKey);
const e = schnorrChallengeFinalize(taggedHash(TAGS.challenge, numTo32b(r), toRawX(P), m));
// Finalize
// R = s⋅G - e⋅P
// -eP == (n-e)P
const R = secp256k1.Point.BASE.multiplyAndAddUnsafe(
P,
normalizePrivateKey(s),
mod(-e, secp256k1.CURVE.n)
);
if (!R || !R.hasEvenY() || R.x !== r) return false;
return true;
} catch (error) {
return false;
}
}
export const schnorr = {
Signature: SchnorrSignature,
// Schnorr's pubkey is just `x` of Point (BIP340)
getPublicKey: (privateKey: PrivKey): Uint8Array =>
toRawX(secp256k1.Point.fromPrivateKey(privateKey)),
sign: schnorrSign,
verify: schnorrVerify,
};

View File

@@ -1,265 +0,0 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { keccak_256 } from '@noble/hashes/sha3';
import { sha256 } from '@noble/hashes/sha256';
import { weierstrass, ProjectivePointType } from './abstract/weierstrass.js';
import * as cutils from './abstract/utils.js';
import { Fp } from './abstract/modular.js';
import { getHash } from './_shortw_utils.js';
type ProjectivePoint = ProjectivePointType<bigint>;
// Stark-friendly elliptic curve
// https://docs.starkware.co/starkex/stark-curve.html
const CURVE_N = BigInt(
'3618502788666131213697322783095070105526743751716087489154079457884512865583'
);
const nBitLength = 252;
export const starkCurve = weierstrass({
// Params: a, b
a: BigInt(1),
b: BigInt('3141592653589793238462643383279502884197169399375105820974944592307816406665'),
// Field over which we'll do calculations; 2n**251n + 17n * 2n**192n + 1n
// There is no efficient sqrt for field (P%4==1)
Fp: Fp(BigInt('0x800000000000011000000000000000000000000000000000000000000000001')),
// Curve order, total count of valid points in the field.
n: CURVE_N,
nBitLength: nBitLength, // len(bin(N).replace('0b',''))
// Base point (x, y) aka generator point
Gx: BigInt('874739451078007766457464989774322083649278607533249481151382481072868806602'),
Gy: BigInt('152666792071518830868575557812948353041420400780739481342941381225525861407'),
h: BigInt(1),
// Default options
lowS: false,
...getHash(sha256),
truncateHash: (hash: Uint8Array, truncateOnly = false): bigint => {
// TODO: cleanup, ugly code
// Fix truncation
if (!truncateOnly) {
let hashS = bytesToNumber0x(hash).toString(16);
if (hashS.length === 63) {
hashS += '0';
hash = hexToBytes0x(hashS);
}
}
// Truncate zero bytes on left (compat with elliptic)
while (hash[0] === 0) hash = hash.subarray(1);
const byteLength = hash.length;
const delta = byteLength * 8 - nBitLength; // size of curve.n (252 bits)
let h = hash.length ? bytesToNumber0x(hash) : 0n;
if (delta > 0) h = h >> BigInt(delta);
if (!truncateOnly && h >= CURVE_N) h -= CURVE_N;
return h;
},
});
// Custom Starknet type conversion functions that can handle 0x and unpadded hex
function hexToBytes0x(hex: string): Uint8Array {
if (typeof hex !== 'string') {
throw new TypeError('hexToBytes: expected string, got ' + typeof hex);
}
hex = strip0x(hex);
if (hex.length & 1) hex = '0' + hex; // padding
if (hex.length % 2) throw new Error('hexToBytes: received invalid unpadded hex ' + hex.length);
const array = new Uint8Array(hex.length / 2);
for (let i = 0; i < array.length; i++) {
const j = i * 2;
const hexByte = hex.slice(j, j + 2);
const byte = Number.parseInt(hexByte, 16);
if (Number.isNaN(byte) || byte < 0) throw new Error('Invalid byte sequence');
array[i] = byte;
}
return array;
}
function hexToNumber0x(hex: string): bigint {
if (typeof hex !== 'string') {
throw new TypeError('hexToNumber: expected string, got ' + typeof hex);
}
// Big Endian
// TODO: strip vs no strip?
return BigInt(`0x${strip0x(hex)}`);
}
function bytesToNumber0x(bytes: Uint8Array): bigint {
return hexToNumber0x(cutils.bytesToHex(bytes));
}
function ensureBytes0x(hex: Hex): Uint8Array {
// Uint8Array.from() instead of hash.slice() because node.js Buffer
// is instance of Uint8Array, and its slice() creates **mutable** copy
return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes0x(hex);
}
function normalizePrivateKey(privKey: Hex) {
return cutils.bytesToHex(ensureBytes0x(privKey)).padStart(32 * 2, '0');
}
function getPublicKey0x(privKey: Hex, isCompressed?: boolean) {
return starkCurve.getPublicKey(normalizePrivateKey(privKey), isCompressed);
}
function getSharedSecret0x(privKeyA: Hex, pubKeyB: Hex) {
return starkCurve.getSharedSecret(normalizePrivateKey(privKeyA), pubKeyB);
}
function sign0x(msgHash: Hex, privKey: Hex, opts?: any) {
if (typeof privKey === 'string') privKey = strip0x(privKey).padStart(64, '0');
return starkCurve.sign(ensureBytes0x(msgHash), normalizePrivateKey(privKey), opts);
}
function verify0x(signature: Hex, msgHash: Hex, pubKey: Hex) {
const sig = signature instanceof Signature ? signature : ensureBytes0x(signature);
return starkCurve.verify(sig, ensureBytes0x(msgHash), ensureBytes0x(pubKey));
}
const { CURVE, Point, ProjectivePoint, Signature } = starkCurve;
export const utils = starkCurve.utils;
export {
CURVE,
Point,
Signature,
ProjectivePoint,
getPublicKey0x as getPublicKey,
getSharedSecret0x as getSharedSecret,
sign0x as sign,
verify0x as verify,
};
const stripLeadingZeros = (s: string) => s.replace(/^0+/gm, '');
export const bytesToHexEth = (uint8a: Uint8Array): string =>
`0x${stripLeadingZeros(cutils.bytesToHex(uint8a))}`;
export const strip0x = (hex: string) => hex.replace(/^0x/i, '');
export const numberToHexEth = (num: bigint | number) => `0x${num.toString(16)}`;
// We accept hex strings besides Uint8Array for simplicity
type Hex = Uint8Array | string;
// 1. seed generation
function hashKeyWithIndex(key: Uint8Array, index: number) {
let indexHex = cutils.numberToHexUnpadded(index);
if (indexHex.length & 1) indexHex = '0' + indexHex;
return bytesToNumber0x(sha256(cutils.concatBytes(key, hexToBytes0x(indexHex))));
}
export function grindKey(seed: Hex) {
const _seed = ensureBytes0x(seed);
const sha256mask = 2n ** 256n;
const Fn = Fp(CURVE.n);
const limit = sha256mask - Fn.create(sha256mask);
for (let i = 0; ; i++) {
const key = hashKeyWithIndex(_seed, i);
// key should be in [0, limit)
if (key < limit) return Fn.create(key).toString(16);
}
}
export function getStarkKey(privateKey: Hex) {
return bytesToHexEth(getPublicKey0x(privateKey, true).slice(1));
}
export function ethSigToPrivate(signature: string) {
signature = strip0x(signature.replace(/^0x/, ''));
if (signature.length !== 130) throw new Error('Wrong ethereum signature');
return grindKey(signature.substring(0, 64));
}
const MASK_31 = 2n ** 31n - 1n;
const int31 = (n: bigint) => Number(n & MASK_31);
export function getAccountPath(
layer: string,
application: string,
ethereumAddress: string,
index: number
) {
const layerNum = int31(bytesToNumber0x(sha256(layer)));
const applicationNum = int31(bytesToNumber0x(sha256(application)));
const eth = hexToNumber0x(ethereumAddress);
return `m/2645'/${layerNum}'/${applicationNum}'/${int31(eth)}'/${int31(eth >> 31n)}'/${index}`;
}
// https://docs.starkware.co/starkex/pedersen-hash-function.html
const PEDERSEN_POINTS_AFFINE = [
new Point(
2089986280348253421170679821480865132823066470938446095505822317253594081284n,
1713931329540660377023406109199410414810705867260802078187082345529207694986n
),
new Point(
996781205833008774514500082376783249102396023663454813447423147977397232763n,
1668503676786377725805489344771023921079126552019160156920634619255970485781n
),
new Point(
2251563274489750535117886426533222435294046428347329203627021249169616184184n,
1798716007562728905295480679789526322175868328062420237419143593021674992973n
),
new Point(
2138414695194151160943305727036575959195309218611738193261179310511854807447n,
113410276730064486255102093846540133784865286929052426931474106396135072156n
),
new Point(
2379962749567351885752724891227938183011949129833673362440656643086021394946n,
776496453633298175483985398648758586525933812536653089401905292063708816422n
),
];
// for (const p of PEDERSEN_POINTS) p._setWindowSize(8);
const PEDERSEN_POINTS = PEDERSEN_POINTS_AFFINE.map(ProjectivePoint.fromAffine);
function pedersenPrecompute(p1: ProjectivePoint, p2: ProjectivePoint): ProjectivePoint[] {
const out: ProjectivePoint[] = [];
let p = p1;
for (let i = 0; i < 248; i++) {
out.push(p);
p = p.double();
}
// NOTE: we cannot use wNAF here, because last 4 bits will require full 248 bits multiplication
// We can add support for this to wNAF, but it will complicate wNAF.
p = p2;
for (let i = 0; i < 4; i++) {
out.push(p);
p = p.double();
}
return out;
}
const PEDERSEN_POINTS1 = pedersenPrecompute(PEDERSEN_POINTS[1], PEDERSEN_POINTS[2]);
const PEDERSEN_POINTS2 = pedersenPrecompute(PEDERSEN_POINTS[3], PEDERSEN_POINTS[4]);
type PedersenArg = Hex | bigint | number;
function pedersenArg(arg: PedersenArg): bigint {
let value: bigint;
if (typeof arg === 'bigint') value = arg;
else if (typeof arg === 'number') {
if (!Number.isSafeInteger(arg)) throw new Error(`Invalid pedersenArg: ${arg}`);
value = BigInt(arg);
} else value = bytesToNumber0x(ensureBytes0x(arg));
// [0..Fp)
if (!(0n <= value && value < starkCurve.CURVE.Fp.ORDER))
throw new Error(`PedersenArg should be 0 <= value < CURVE.P: ${value}`);
return value;
}
function pedersenSingle(point: ProjectivePoint, value: PedersenArg, constants: ProjectivePoint[]) {
let x = pedersenArg(value);
for (let j = 0; j < 252; j++) {
const pt = constants[j];
if (pt.x === point.x) throw new Error('Same point');
if ((x & 1n) !== 0n) point = point.add(pt);
x >>= 1n;
}
return point;
}
// shift_point + x_low * P_0 + x_high * P1 + y_low * P2 + y_high * P3
export function pedersen(x: PedersenArg, y: PedersenArg) {
let point: ProjectivePoint = PEDERSEN_POINTS[0];
point = pedersenSingle(point, x, PEDERSEN_POINTS1);
point = pedersenSingle(point, y, PEDERSEN_POINTS2);
return bytesToHexEth(point.toAffine().toRawBytes(true).slice(1));
}
export function hashChain(data: PedersenArg[], fn = pedersen) {
if (!Array.isArray(data) || data.length < 1)
throw new Error('data should be array of at least 1 element');
if (data.length === 1) return numberToHexEth(pedersenArg(data[0]));
return Array.from(data)
.reverse()
.reduce((acc, i) => fn(i, acc));
}
// Same as hashChain, but computes hash even for single element and order is not revesed
export const computeHashOnElements = (data: PedersenArg[], fn = pedersen) =>
[0, ...data, data.length].reduce((x, y) => fn(x, y));
const MASK_250 = 2n ** 250n - 1n;
export const keccak = (data: Uint8Array) => bytesToNumber0x(keccak_256(data)) & MASK_250;

View File

@@ -0,0 +1,44 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { createCurve } from '../esm/_shortw_utils.js';
import { sha224, sha256 } from '@noble/hashes/sha256';
import { Field as Fp } from '../esm/abstract/modular.js';
// NIST secp192r1 aka P192
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/secg/secp192r1
export const P192 = createCurve(
{
// Params: a, b
a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffc'),
b: BigInt('0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1'),
// Field over which we'll do calculations; 2n ** 192n - 2n ** 64n - 1n
Fp: Fp(BigInt('0xfffffffffffffffffffffffffffffffeffffffffffffffff')),
// Curve order, total count of valid points in the field.
n: BigInt('0xffffffffffffffffffffffff99def836146bc9b1b4d22831'),
// Base point (x, y) aka generator point
Gx: BigInt('0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012'),
Gy: BigInt('0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811'),
h: BigInt(1),
lowS: false,
},
sha256
);
export const secp192r1 = P192;
export const P224 = createCurve(
{
// Params: a, b
a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffffffffffe'),
b: BigInt('0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4'),
// Field over which we'll do calculations;
Fp: Fp(BigInt('0xffffffffffffffffffffffffffffffff000000000000000000000001')),
// Curve order, total count of valid points in the field
n: BigInt('0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d'),
// Base point (x, y) aka generator point
Gx: BigInt('0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21'),
Gy: BigInt('0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34'),
h: BigInt(1),
lowS: false,
},
sha224
);
export const secp224r1 = P224;

103
test/_poseidon.helpers.js Normal file
View File

@@ -0,0 +1,103 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { sha256 } from '@noble/hashes/sha256';
import { utf8ToBytes } from '@noble/hashes/utils';
import { Field as Fp, validateField } from '../esm/abstract/modular.js';
import { poseidon } from '../esm/abstract/poseidon.js';
import * as u from '../esm/abstract/utils.js';
// Poseidon hash https://docs.starkware.co/starkex/stark-curve.html
export const Fp253 = Fp(
BigInt('14474011154664525231415395255581126252639794253786371766033694892385558855681')
); // 2^253 + 2^199 + 1
export const Fp251 = Fp(
BigInt('3618502788666131213697322783095070105623107215331596699973092056135872020481')
); // 2^251 + 17 * 2^192 + 1
function poseidonRoundConstant(Fp, name, idx) {
const val = Fp.fromBytes(sha256(utf8ToBytes(`${name}${idx}`)));
return Fp.create(val);
}
// NOTE: doesn't check eiginvalues and possible can create unsafe matrix. But any filtration here will break compatibility with starknet
// Please use only if you really know what you doing.
// https://eprint.iacr.org/2019/458.pdf Section 2.3 (Avoiding Insecure Matrices)
export function _poseidonMDS(Fp, name, m, attempt = 0) {
const x_values = [];
const y_values = [];
for (let i = 0; i < m; i++) {
x_values.push(poseidonRoundConstant(Fp, `${name}x`, attempt * m + i));
y_values.push(poseidonRoundConstant(Fp, `${name}y`, attempt * m + i));
}
if (new Set([...x_values, ...y_values]).size !== 2 * m)
throw new Error('X and Y values are not distinct');
return x_values.map((x) => y_values.map((y) => Fp.inv(Fp.sub(x, y))));
}
const MDS_SMALL = [
[3, 1, 1],
[1, -1, 1],
[1, 1, -2],
].map((i) => i.map(BigInt));
export function poseidonBasic(opts, mds) {
validateField(opts.Fp);
if (!Number.isSafeInteger(opts.rate) || !Number.isSafeInteger(opts.capacity))
throw new Error(`Wrong poseidon opts: ${opts}`);
const m = opts.rate + opts.capacity;
const rounds = opts.roundsFull + opts.roundsPartial;
const roundConstants = [];
for (let i = 0; i < rounds; i++) {
const row = [];
for (let j = 0; j < m; j++) row.push(poseidonRoundConstant(opts.Fp, 'Hades', m * i + j));
roundConstants.push(row);
}
const res = poseidon({
...opts,
t: m,
sboxPower: 3,
reversePartialPowIdx: true, // Why?!
mds,
roundConstants,
});
res.m = m;
res.rate = opts.rate;
res.capacity = opts.capacity;
return res;
}
export function poseidonCreate(opts, mdsAttempt = 0) {
const m = opts.rate + opts.capacity;
if (!Number.isSafeInteger(mdsAttempt)) throw new Error(`Wrong mdsAttempt=${mdsAttempt}`);
return poseidonBasic(opts, _poseidonMDS(opts.Fp, 'HadesMDS', m, mdsAttempt));
}
export const poseidonSmall = poseidonBasic(
{ Fp: Fp251, rate: 2, capacity: 1, roundsFull: 8, roundsPartial: 83 },
MDS_SMALL
);
export function poseidonHash(x, y, fn = poseidonSmall) {
return fn([x, y, 2n])[0];
}
export function poseidonHashFunc(x, y, fn = poseidonSmall) {
return u.numberToVarBytesBE(poseidonHash(u.bytesToNumberBE(x), u.bytesToNumberBE(y), fn));
}
export function poseidonHashSingle(x, fn = poseidonSmall) {
return fn([x, 0n, 1n])[0];
}
export function poseidonHashMany(values, fn = poseidonSmall) {
const { m, rate } = fn;
if (!Array.isArray(values)) throw new Error('bigint array expected in values');
const padded = Array.from(values); // copy
padded.push(1n);
while (padded.length % rate !== 0) padded.push(0n);
let state = new Array(m).fill(0n);
for (let i = 0; i < padded.length; i += rate) {
for (let j = 0; j < rate; j++) state[j] += padded[i + j];
state = fn(state);
}
return state[0];
}

View File

@@ -1,22 +1,20 @@
import { deepStrictEqual, throws } from 'assert'; import { deepStrictEqual, throws } from 'assert';
import { should, describe } from 'micro-should'; import { should, describe } from 'micro-should';
import * as fc from 'fast-check'; import * as fc from 'fast-check';
import * as mod from '../lib/esm/abstract/modular.js'; import * as mod from '../esm/abstract/modular.js';
import { bytesToHex as toHex } from '../lib/esm/abstract/utils.js'; import { bytesToHex as toHex } from '../esm/abstract/utils.js';
// Generic tests for all curves in package // Generic tests for all curves in package
import { secp192r1 } from '../lib/esm/p192.js'; import { secp192r1, secp224r1 } from './_more-curves.helpers.js';
import { secp224r1 } from '../lib/esm/p224.js'; import { secp256r1 } from '../esm/p256.js';
import { secp256r1 } from '../lib/esm/p256.js'; import { secp384r1 } from '../esm/p384.js';
import { secp384r1 } from '../lib/esm/p384.js'; import { secp521r1 } from '../esm/p521.js';
import { secp521r1 } from '../lib/esm/p521.js'; import { secp256k1 } from '../esm/secp256k1.js';
import { secp256k1 } from '../lib/esm/secp256k1.js'; import { ed25519, ed25519ctx, ed25519ph, x25519 } from '../esm/ed25519.js';
import { ed25519, ed25519ctx, ed25519ph } from '../lib/esm/ed25519.js'; import { ed448, ed448ph } from '../esm/ed448.js';
import { ed448, ed448ph } from '../lib/esm/ed448.js'; import { pallas, vesta } from '../esm/pasta.js';
import { starkCurve } from '../lib/esm/stark.js'; import { bn254 } from '../esm/bn.js';
import { pallas, vesta } from '../lib/esm/pasta.js'; import { jubjub } from '../esm/jubjub.js';
import { bn254 } from '../lib/esm/bn.js'; import { bls12_381 } from '../esm/bls12-381.js';
import { jubjub } from '../lib/esm/jubjub.js';
import { bls12_381 } from '../lib/esm/bls12-381.js';
// Fields tests // Fields tests
const FIELDS = { const FIELDS = {
@@ -25,7 +23,6 @@ const FIELDS = {
secp256r1: { Fp: [secp256r1.CURVE.Fp] }, secp256r1: { Fp: [secp256r1.CURVE.Fp] },
secp521r1: { Fp: [secp521r1.CURVE.Fp] }, secp521r1: { Fp: [secp521r1.CURVE.Fp] },
secp256k1: { Fp: [secp256k1.CURVE.Fp] }, secp256k1: { Fp: [secp256k1.CURVE.Fp] },
stark: { Fp: [starkCurve.CURVE.Fp] },
jubjub: { Fp: [jubjub.CURVE.Fp] }, jubjub: { Fp: [jubjub.CURVE.Fp] },
ed25519: { Fp: [ed25519.CURVE.Fp] }, ed25519: { Fp: [ed25519.CURVE.Fp] },
ed448: { Fp: [ed448.CURVE.Fp] }, ed448: { Fp: [ed448.CURVE.Fp] },
@@ -68,8 +65,8 @@ for (const c in FIELDS) {
fc.property(FC_BIGINT, (num) => { fc.property(FC_BIGINT, (num) => {
const a = create(num); const a = create(num);
const b = create(num); const b = create(num);
deepStrictEqual(Fp.equals(a, b), true); deepStrictEqual(Fp.eql(a, b), true);
deepStrictEqual(Fp.equals(b, a), true); deepStrictEqual(Fp.eql(b, a), true);
}) })
); );
}); });
@@ -78,8 +75,8 @@ for (const c in FIELDS) {
fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => { fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => {
const a = create(num1); const a = create(num1);
const b = create(num2); const b = create(num2);
deepStrictEqual(Fp.equals(a, b), num1 === num2); deepStrictEqual(Fp.eql(a, b), num1 === num2);
deepStrictEqual(Fp.equals(b, a), num1 === num2); deepStrictEqual(Fp.eql(b, a), num1 === num2);
}) })
); );
}); });
@@ -124,8 +121,8 @@ for (const c in FIELDS) {
fc.property(FC_BIGINT, (num1) => { fc.property(FC_BIGINT, (num1) => {
const a = create(num1); const a = create(num1);
const b = create(num1); const b = create(num1);
deepStrictEqual(Fp.sub(Fp.ZERO, a), Fp.negate(a)); deepStrictEqual(Fp.sub(Fp.ZERO, a), Fp.neg(a));
deepStrictEqual(Fp.sub(a, b), Fp.add(a, Fp.negate(b))); deepStrictEqual(Fp.sub(a, b), Fp.add(a, Fp.neg(b)));
deepStrictEqual(Fp.sub(a, b), Fp.add(a, Fp.mul(b, Fp.create(-1n)))); deepStrictEqual(Fp.sub(a, b), Fp.add(a, Fp.mul(b, Fp.create(-1n))));
}) })
); );
@@ -134,13 +131,13 @@ for (const c in FIELDS) {
fc.assert( fc.assert(
fc.property(FC_BIGINT, (num) => { fc.property(FC_BIGINT, (num) => {
const a = create(num); const a = create(num);
deepStrictEqual(Fp.negate(a), Fp.sub(Fp.ZERO, a)); deepStrictEqual(Fp.neg(a), Fp.sub(Fp.ZERO, a));
deepStrictEqual(Fp.negate(a), Fp.mul(a, Fp.create(-1n))); deepStrictEqual(Fp.neg(a), Fp.mul(a, Fp.create(-1n)));
}) })
); );
}); });
should('negate(0)', () => { should('negate(0)', () => {
deepStrictEqual(Fp.negate(Fp.ZERO), Fp.ZERO); deepStrictEqual(Fp.neg(Fp.ZERO), Fp.ZERO);
}); });
should('multiply/commutativity', () => { should('multiply/commutativity', () => {
@@ -190,7 +187,7 @@ for (const c in FIELDS) {
fc.assert( fc.assert(
fc.property(FC_BIGINT, (num) => { fc.property(FC_BIGINT, (num) => {
const a = create(num); const a = create(num);
deepStrictEqual(Fp.square(a), Fp.mul(a, a)); deepStrictEqual(Fp.sqr(a), Fp.mul(a, a));
}) })
); );
}); });
@@ -207,18 +204,18 @@ for (const c in FIELDS) {
}); });
should('square(0)', () => { should('square(0)', () => {
deepStrictEqual(Fp.square(Fp.ZERO), Fp.ZERO); deepStrictEqual(Fp.sqr(Fp.ZERO), Fp.ZERO);
deepStrictEqual(Fp.mul(Fp.ZERO, Fp.ZERO), Fp.ZERO); deepStrictEqual(Fp.mul(Fp.ZERO, Fp.ZERO), Fp.ZERO);
}); });
should('square(1)', () => { should('square(1)', () => {
deepStrictEqual(Fp.square(Fp.ONE), Fp.ONE); deepStrictEqual(Fp.sqr(Fp.ONE), Fp.ONE);
deepStrictEqual(Fp.mul(Fp.ONE, Fp.ONE), Fp.ONE); deepStrictEqual(Fp.mul(Fp.ONE, Fp.ONE), Fp.ONE);
}); });
should('square(-1)', () => { should('square(-1)', () => {
const minus1 = Fp.negate(Fp.ONE); const minus1 = Fp.neg(Fp.ONE);
deepStrictEqual(Fp.square(minus1), Fp.ONE); deepStrictEqual(Fp.sqr(minus1), Fp.ONE);
deepStrictEqual(Fp.mul(minus1, minus1), Fp.ONE); deepStrictEqual(Fp.mul(minus1, minus1), Fp.ONE);
}); });
@@ -237,8 +234,13 @@ for (const c in FIELDS) {
return; return;
} }
deepStrictEqual(isSquare(a), true); deepStrictEqual(isSquare(a), true);
deepStrictEqual(Fp.equals(Fp.square(root), a), true, 'sqrt(a)^2 == a'); deepStrictEqual(Fp.eql(Fp.sqr(root), a), true, 'sqrt(a)^2 == a');
deepStrictEqual(Fp.equals(Fp.square(Fp.negate(root)), a), true, '(-sqrt(a))^2 == a'); deepStrictEqual(Fp.eql(Fp.sqr(Fp.neg(root)), a), true, '(-sqrt(a))^2 == a');
// Returns odd/even element
deepStrictEqual(Fp.isOdd(mod.FpSqrtOdd(Fp, a)), true);
deepStrictEqual(Fp.isOdd(mod.FpSqrtEven(Fp, a)), false);
deepStrictEqual(Fp.eql(Fp.sqr(mod.FpSqrtOdd(Fp, a)), a), true);
deepStrictEqual(Fp.eql(Fp.sqr(mod.FpSqrtEven(Fp, a)), a), true);
}) })
); );
}); });
@@ -247,7 +249,7 @@ for (const c in FIELDS) {
deepStrictEqual(Fp.sqrt(Fp.ZERO), Fp.ZERO); deepStrictEqual(Fp.sqrt(Fp.ZERO), Fp.ZERO);
const sqrt1 = Fp.sqrt(Fp.ONE); const sqrt1 = Fp.sqrt(Fp.ONE);
deepStrictEqual( deepStrictEqual(
Fp.equals(sqrt1, Fp.ONE) || Fp.equals(sqrt1, Fp.negate(Fp.ONE)), Fp.eql(sqrt1, Fp.ONE) || Fp.eql(sqrt1, Fp.neg(Fp.ONE)),
true, true,
'sqrt(1) = 1 or -1' 'sqrt(1) = 1 or -1'
); );
@@ -258,9 +260,12 @@ for (const c in FIELDS) {
fc.assert( fc.assert(
fc.property(FC_BIGINT, (num) => { fc.property(FC_BIGINT, (num) => {
const a = create(num); const a = create(num);
if (Fp.equals(a, Fp.ZERO)) return; // No division by zero if (Fp.eql(a, Fp.ZERO)) return; // No division by zero
deepStrictEqual(Fp.div(a, Fp.ONE), a); deepStrictEqual(Fp.div(a, Fp.ONE), a);
deepStrictEqual(Fp.div(a, a), Fp.ONE); deepStrictEqual(Fp.div(a, a), Fp.ONE);
// FpDiv tests
deepStrictEqual(mod.FpDiv(Fp, a, Fp.ONE), a);
deepStrictEqual(mod.FpDiv(Fp, a, a), Fp.ONE);
}) })
); );
}); });
@@ -269,6 +274,7 @@ for (const c in FIELDS) {
fc.property(FC_BIGINT, (num) => { fc.property(FC_BIGINT, (num) => {
const a = create(num); const a = create(num);
deepStrictEqual(Fp.div(Fp.ZERO, a), Fp.ZERO); deepStrictEqual(Fp.div(Fp.ZERO, a), Fp.ZERO);
deepStrictEqual(mod.FpDiv(Fp, Fp.ZERO, a), Fp.ZERO);
}) })
); );
}); });
@@ -279,6 +285,10 @@ for (const c in FIELDS) {
const b = create(num2); const b = create(num2);
const c = create(num3); const c = create(num3);
deepStrictEqual(Fp.div(Fp.add(a, b), c), Fp.add(Fp.div(a, c), Fp.div(b, c))); deepStrictEqual(Fp.div(Fp.add(a, b), c), Fp.add(Fp.div(a, c), Fp.div(b, c)));
deepStrictEqual(
mod.FpDiv(Fp, Fp.add(a, b), c),
Fp.add(mod.FpDiv(Fp, a, c), mod.FpDiv(Fp, b, c))
);
}) })
); );
}); });
@@ -287,7 +297,7 @@ for (const c in FIELDS) {
fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => { fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => {
const a = create(num1); const a = create(num1);
const b = create(num2); const b = create(num2);
deepStrictEqual(Fp.div(a, b), Fp.mul(a, Fp.invert(b))); deepStrictEqual(Fp.div(a, b), Fp.mul(a, Fp.inv(b)));
}) })
); );
}); });
@@ -302,7 +312,6 @@ const CURVES = {
secp256k1, secp256k1,
ed25519, ed25519ctx, ed25519ph, ed25519, ed25519ctx, ed25519ph,
ed448, ed448ph, ed448, ed448ph,
starkCurve,
pallas, vesta, pallas, vesta,
bn254, bn254,
jubjub, jubjub,
@@ -313,12 +322,12 @@ const NUM_RUNS = 5;
const getXY = (p) => ({ x: p.x, y: p.y }); const getXY = (p) => ({ x: p.x, y: p.y });
function equal(a, b, comment) { function equal(a, b, comment) {
deepStrictEqual(a.equals(b), true, 'eq(${comment})'); deepStrictEqual(a.equals(b), true, `eq(${comment})`);
if (a.toAffine && b.toAffine) { if (a.toAffine && b.toAffine) {
deepStrictEqual(getXY(a.toAffine()), getXY(b.toAffine()), 'eqToAffine(${comment})'); deepStrictEqual(getXY(a.toAffine()), getXY(b.toAffine()), `eqToAffine(${comment})`);
} else if (!a.toAffine && !b.toAffine) { } else if (!a.toAffine && !b.toAffine) {
// Already affine // Already affine
deepStrictEqual(getXY(a), getXY(b), 'eqAffine(${comment})'); deepStrictEqual(getXY(a), getXY(b), `eqAffine(${comment})`);
} else throw new Error('Different point types'); } else throw new Error('Different point types');
} }
@@ -342,50 +351,50 @@ for (const name in CURVES) {
if (!p) continue; if (!p) continue;
const G = [p.ZERO, p.BASE]; const G = [p.ZERO, p.BASE];
for (let i = 2; i < 10; i++) G.push(G[1].multiply(i)); for (let i = 2n; i < 10n; i++) G.push(G[1].multiply(i));
const title = `${name}/${pointName}`; const title = `${name}/${pointName}`;
describe(title, () => { describe(title, () => {
describe('basic group laws', () => { describe('basic group laws', () => {
// Here we check basic group laws, to verify that points works as group // Here we check basic group laws, to verify that points works as group
should('(zero)', () => { should('zero', () => {
equal(G[0].double(), G[0], '(0*G).double() = 0'); equal(G[0].double(), G[0], '(0*G).double() = 0');
equal(G[0].add(G[0]), G[0], '0*G + 0*G = 0'); equal(G[0].add(G[0]), G[0], '0*G + 0*G = 0');
equal(G[0].subtract(G[0]), G[0], '0*G - 0*G = 0'); equal(G[0].subtract(G[0]), G[0], '0*G - 0*G = 0');
equal(G[0].negate(), G[0], '-0 = 0'); equal(G[0].negate(), G[0], '-0 = 0');
for (let i = 0; i < G.length; i++) { for (let i = 0; i < G.length; i++) {
const p = G[i]; const p = G[i];
equal(p, p.add(G[0]), '${i}*G + 0 = ${i}*G'); equal(p, p.add(G[0]), `${i}*G + 0 = ${i}*G`);
equal(G[0].multiply(i + 1), G[0], '${i + 1}*0 = 0'); equal(G[0].multiply(BigInt(i + 1)), G[0], `${i + 1}*0 = 0`);
} }
}); });
should('(one)', () => { should('one', () => {
equal(G[1].double(), G[2], '(1*G).double() = 2*G'); equal(G[1].double(), G[2], '(1*G).double() = 2*G');
equal(G[1].subtract(G[1]), G[0], '1*G - 1*G = 0'); equal(G[1].subtract(G[1]), G[0], '1*G - 1*G = 0');
equal(G[1].add(G[1]), G[2], '1*G + 1*G = 2*G'); equal(G[1].add(G[1]), G[2], '1*G + 1*G = 2*G');
}); });
should('(sanity tests)', () => { should('sanity tests', () => {
equal(G[2].double(), G[4], '(2*G).double() = 4*G'); equal(G[2].double(), G[4], '(2*G).double() = 4*G');
equal(G[2].add(G[2]), G[4], '2*G + 2*G = 4*G'); equal(G[2].add(G[2]), G[4], '2*G + 2*G = 4*G');
equal(G[7].add(G[3].negate()), G[4], '7*G - 3*G = 4*G'); equal(G[7].add(G[3].negate()), G[4], '7*G - 3*G = 4*G');
}); });
should('(addition commutativity)', () => { should('add commutativity', () => {
equal(G[4].add(G[3]), G[3].add(G[4]), '4*G + 3*G = 3*G + 4*G'); equal(G[4].add(G[3]), G[3].add(G[4]), '4*G + 3*G = 3*G + 4*G');
equal(G[4].add(G[3]), G[3].add(G[2]).add(G[2]), '4*G + 3*G = 3*G + 2*G + 2*G'); equal(G[4].add(G[3]), G[3].add(G[2]).add(G[2]), '4*G + 3*G = 3*G + 2*G + 2*G');
}); });
should('(double)', () => { should('double', () => {
equal(G[3].double(), G[6], '(3*G).double() = 6*G'); equal(G[3].double(), G[6], '(3*G).double() = 6*G');
}); });
should('(multiply)', () => { should('multiply', () => {
equal(G[2].multiply(3), G[6], '(2*G).multiply(3) = 6*G'); equal(G[2].multiply(3n), G[6], '(2*G).multiply(3) = 6*G');
}); });
should('(same point addition)', () => { should('add same-point', () => {
equal(G[3].add(G[3]), G[6], '3*G + 3*G = 6*G'); equal(G[3].add(G[3]), G[6], '3*G + 3*G = 6*G');
}); });
should('(same point (negative) addition)', () => { should('add same-point negative', () => {
equal(G[3].add(G[3].negate()), G[0], '3*G + (- 3*G) = 0*G'); equal(G[3].add(G[3].negate()), G[0], '3*G + (- 3*G) = 0*G');
equal(G[3].subtract(G[3]), G[0], '3*G - 3*G = 0*G'); equal(G[3].subtract(G[3]), G[0], '3*G - 3*G = 0*G');
}); });
should('(curve order)', () => { should('mul by curve order', () => {
equal(G[1].multiply(CURVE_ORDER - 1n).add(G[1]), G[0], '(N-1)*G + G = 0'); equal(G[1].multiply(CURVE_ORDER - 1n).add(G[1]), G[0], '(N-1)*G + G = 0');
equal(G[1].multiply(CURVE_ORDER - 1n).add(G[2]), G[1], '(N-1)*G + 2*G = 1*G'); equal(G[1].multiply(CURVE_ORDER - 1n).add(G[2]), G[1], '(N-1)*G + 2*G = 1*G');
equal(G[1].multiply(CURVE_ORDER - 2n).add(G[2]), G[0], '(N-2)*G + 2*G = 0'); equal(G[1].multiply(CURVE_ORDER - 2n).add(G[2]), G[0], '(N-2)*G + 2*G = 0');
@@ -393,7 +402,7 @@ for (const name in CURVES) {
const carry = CURVE_ORDER % 2n === 1n ? G[1] : G[0]; const carry = CURVE_ORDER % 2n === 1n ? G[1] : G[0];
equal(G[1].multiply(half).double().add(carry), G[0], '((N/2) * G).double() = 0'); equal(G[1].multiply(half).double().add(carry), G[0], '((N/2) * G).double() = 0');
}); });
should('(inversion)', () => { should('inversion', () => {
const a = 1234n; const a = 1234n;
const b = 5678n; const b = 5678n;
const c = a * b; const c = a * b;
@@ -401,7 +410,7 @@ for (const name in CURVES) {
const inv = mod.invert(b, CURVE_ORDER); const inv = mod.invert(b, CURVE_ORDER);
equal(G[1].multiply(c).multiply(inv), G[1].multiply(a), 'c*G * (1/b)*G = a*G'); equal(G[1].multiply(c).multiply(inv), G[1].multiply(a), 'c*G * (1/b)*G = a*G');
}); });
should('(multiply, rand)', () => should('multiply, rand', () =>
fc.assert( fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, (a, b) => { fc.property(FC_BIGINT, FC_BIGINT, (a, b) => {
const c = mod.mod(a + b, CURVE_ORDER); const c = mod.mod(a + b, CURVE_ORDER);
@@ -415,7 +424,7 @@ for (const name in CURVES) {
{ numRuns: NUM_RUNS } { numRuns: NUM_RUNS }
) )
); );
should('(multiply2, rand)', () => should('multiply2, rand', () =>
fc.assert( fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, (a, b) => { fc.property(FC_BIGINT, FC_BIGINT, (a, b) => {
const c = mod.mod(a * b, CURVE_ORDER); const c = mod.mod(a * b, CURVE_ORDER);
@@ -436,9 +445,16 @@ for (const name in CURVES) {
throws(() => G[1][op](0n), '0n'); throws(() => G[1][op](0n), '0n');
G[1][op](G[2]); G[1][op](G[2]);
throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER'); throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER');
throws(() => G[1][op](-123n), '-123n');
throws(() => G[1][op](123), '123');
throws(() => G[1][op](123.456), '123.456'); throws(() => G[1][op](123.456), '123.456');
throws(() => G[1][op](true), 'true'); throws(() => G[1][op](true), 'true');
throws(() => G[1][op](false), 'false');
throws(() => G[1][op](null), 'null');
throws(() => G[1][op](undefined), 'undefined');
throws(() => G[1][op]('1'), "'1'"); throws(() => G[1][op]('1'), "'1'");
throws(() => G[1][op]({ x: 1n, y: 1n }), '{ x: 1n, y: 1n }');
throws(() => G[1][op]({ x: 1n, y: 1n, z: 1n }), '{ x: 1n, y: 1n, z: 1n }');
throws( throws(
() => G[1][op]({ x: 1n, y: 1n, z: 1n, t: 1n }), () => G[1][op]({ x: 1n, y: 1n, z: 1n, t: 1n }),
'{ x: 1n, y: 1n, z: 1n, t: 1n }' '{ x: 1n, y: 1n, z: 1n, t: 1n }'
@@ -447,8 +463,8 @@ for (const name in CURVES) {
throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])'); throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])');
throws(() => G[1][op](new Uint8Array([1])), 'ui8a([1])'); throws(() => G[1][op](new Uint8Array([1])), 'ui8a([1])');
throws(() => G[1][op](new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])'); throws(() => G[1][op](new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])');
if (G[1].toAffine) throws(() => G[1][op](C.Point.BASE), 'Point ${op} ${pointName}'); // if (G[1].toAffine) throws(() => G[1][op](C.Point.BASE), `Point ${op} ${pointName}`);
throws(() => G[1][op](o.BASE), '${op}/other curve point'); throws(() => G[1][op](o.BASE), `${op}/other curve point`);
}); });
}); });
} }
@@ -468,7 +484,7 @@ for (const name in CURVES) {
throws(() => G[1].equals(new Uint8Array([0])), 'ui8a([0])'); throws(() => G[1].equals(new Uint8Array([0])), 'ui8a([0])');
throws(() => G[1].equals(new Uint8Array([1])), 'ui8a([1])'); throws(() => G[1].equals(new Uint8Array([1])), 'ui8a([1])');
throws(() => G[1].equals(new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])'); throws(() => G[1].equals(new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])');
if (G[1].toAffine) throws(() => G[1].equals(C.Point.BASE), 'Point.equals(${pointName})'); // if (G[1].toAffine) throws(() => G[1].equals(C.Point.BASE), 'Point.equals(${pointName})');
throws(() => G[1].equals(o.BASE), 'other curve point'); throws(() => G[1].equals(o.BASE), 'other curve point');
}); });
@@ -497,25 +513,39 @@ for (const name in CURVES) {
}); });
} }
// Complex point (Extended/Jacobian/Projective?) // Complex point (Extended/Jacobian/Projective?)
if (p.BASE.toAffine) { // if (p.BASE.toAffine && C.Point) {
should('toAffine()', () => { // should('toAffine()', () => {
equal(p.ZERO.toAffine(), C.Point.ZERO, '0 = 0'); // equal(p.ZERO.toAffine(), C.Point.ZERO, '0 = 0');
equal(p.BASE.toAffine(), C.Point.BASE, '1 = 1'); // equal(p.BASE.toAffine(), C.Point.BASE, '1 = 1');
}); // });
} // }
if (p.fromAffine) { // if (p.fromAffine && C.Point) {
should('fromAffine()', () => { // should('fromAffine()', () => {
equal(p.ZERO, p.fromAffine(C.Point.ZERO), '0 = 0'); // equal(p.ZERO, p.fromAffine(C.Point.ZERO), '0 = 0');
equal(p.BASE, p.fromAffine(C.Point.BASE), '1 = 1'); // equal(p.BASE, p.fromAffine(C.Point.BASE), '1 = 1');
}); // });
} // }
// toHex/fromHex (if available) // toHex/fromHex (if available)
if (p.fromHex && p.BASE.toHex) { if (p.fromHex && p.BASE.toHex) {
should('fromHex(toHex()) roundtrip', () => { should('fromHex(toHex()) roundtrip', () => {
fc.assert( fc.assert(
fc.property(FC_BIGINT, (x) => { fc.property(FC_BIGINT, (x) => {
const hex = p.BASE.multiply(x).toHex(); const point = p.BASE.multiply(x);
const hex = point.toHex();
const bytes = point.toRawBytes();
deepStrictEqual(p.fromHex(hex).toHex(), hex); deepStrictEqual(p.fromHex(hex).toHex(), hex);
deepStrictEqual(p.fromHex(bytes).toHex(), hex);
})
);
});
should('fromHex(toHex(compressed=true)) roundtrip', () => {
fc.assert(
fc.property(FC_BIGINT, (x) => {
const point = p.BASE.multiply(x);
const hex = point.toHex(true);
const bytes = point.toRawBytes(true);
deepStrictEqual(p.fromHex(hex).toHex(true), hex);
deepStrictEqual(p.fromHex(bytes).toHex(true), hex);
}) })
); );
}); });
@@ -523,21 +553,28 @@ for (const name in CURVES) {
}); });
} }
describe(name, () => { describe(name, () => {
if (['bn254', 'pallas', 'vesta'].includes(name)) return;
// Generic complex things (getPublicKey/sign/verify/getSharedSecret) // Generic complex things (getPublicKey/sign/verify/getSharedSecret)
should('getPublicKey type check', () => { should('.getPublicKey() type check', () => {
throws(() => C.getPublicKey(0), '0'); throws(() => C.getPublicKey(0), '0');
throws(() => C.getPublicKey(0n), '0n'); throws(() => C.getPublicKey(0n), '0n');
throws(() => C.getPublicKey(false), 'false'); throws(() => C.getPublicKey(-123n), '-123n');
throws(() => C.getPublicKey(123), '123');
throws(() => C.getPublicKey(123.456), '123.456'); throws(() => C.getPublicKey(123.456), '123.456');
throws(() => C.getPublicKey(true), 'true'); throws(() => C.getPublicKey(true), 'true');
throws(() => C.getPublicKey(false), 'false');
throws(() => C.getPublicKey(null), 'null');
throws(() => C.getPublicKey(undefined), 'undefined');
throws(() => C.getPublicKey(''), "''"); throws(() => C.getPublicKey(''), "''");
// NOTE: passes because of disabled hex padding checks for starknet, maybe enable? // NOTE: passes because of disabled hex padding checks for starknet, maybe enable?
// throws(() => C.getPublicKey('1'), "'1'"); // throws(() => C.getPublicKey('1'), "'1'");
throws(() => C.getPublicKey('key'), "'key'"); throws(() => C.getPublicKey('key'), "'key'");
throws(() => C.getPublicKey({}));
throws(() => C.getPublicKey(new Uint8Array([]))); throws(() => C.getPublicKey(new Uint8Array([])));
throws(() => C.getPublicKey(new Uint8Array([0]))); throws(() => C.getPublicKey(new Uint8Array([0])));
throws(() => C.getPublicKey(new Uint8Array([1]))); throws(() => C.getPublicKey(new Uint8Array([1])));
throws(() => C.getPublicKey(new Uint8Array(4096).fill(1))); throws(() => C.getPublicKey(new Uint8Array(4096).fill(1)));
throws(() => C.getPublicKey(Array(32).fill(1)));
}); });
should('.verify() should verify random signatures', () => should('.verify() should verify random signatures', () =>
fc.assert( fc.assert(
@@ -548,25 +585,96 @@ for (const name in CURVES) {
deepStrictEqual( deepStrictEqual(
C.verify(sig, msg, pub), C.verify(sig, msg, pub),
true, true,
'priv=${toHex(priv)},pub=${toHex(pub)},msg=${msg}' `priv=${toHex(priv)},pub=${toHex(pub)},msg=${msg}`
); );
}), }),
{ numRuns: NUM_RUNS } { numRuns: NUM_RUNS }
) )
); );
should('.verify() should verify empty signatures', () => {
const msg = new Uint8Array([]);
const priv = C.utils.randomPrivateKey();
const pub = C.getPublicKey(priv);
const sig = C.sign(msg, priv);
deepStrictEqual(
C.verify(sig, msg, pub),
true,
'priv=${toHex(priv)},pub=${toHex(pub)},msg=${msg}'
);
});
should('.sign() edge cases', () => { should('.sign() edge cases', () => {
throws(() => C.sign()); throws(() => C.sign());
throws(() => C.sign('')); throws(() => C.sign(''));
throws(() => C.sign('', ''));
throws(() => C.sign(new Uint8Array(), new Uint8Array()));
}); });
should('.verify() should not verify signature with wrong hash', () => { describe('verify()', () => {
const MSG = '01'.repeat(32); const msg = '01'.repeat(32);
const PRIV_KEY = 0x2n; should('true for proper signatures', () => {
const WRONG_MSG = '11'.repeat(32); const priv = C.utils.randomPrivateKey();
const signature = C.sign(MSG, PRIV_KEY); const sig = C.sign(msg, priv);
const publicKey = C.getPublicKey(PRIV_KEY); const pub = C.getPublicKey(priv);
deepStrictEqual(C.verify(signature, WRONG_MSG, publicKey), false); deepStrictEqual(C.verify(sig, msg, pub), true);
}); });
should('false for wrong messages', () => {
const priv = C.utils.randomPrivateKey();
const sig = C.sign(msg, priv);
const pub = C.getPublicKey(priv);
deepStrictEqual(C.verify(sig, '11'.repeat(32), pub), false);
});
should('false for wrong keys', () => {
const priv = C.utils.randomPrivateKey();
const sig = C.sign(msg, priv);
deepStrictEqual(C.verify(sig, msg, C.getPublicKey(C.utils.randomPrivateKey())), false);
});
});
if (C.Signature) {
should('Signature serialization roundtrip', () =>
fc.assert(
fc.property(fc.hexaString({ minLength: 64, maxLength: 64 }), (msg) => {
const priv = C.utils.randomPrivateKey();
const sig = C.sign(msg, priv);
const sigRS = (sig) => ({ s: sig.s, r: sig.r });
// Compact
deepStrictEqual(sigRS(C.Signature.fromCompact(sig.toCompactHex())), sigRS(sig));
deepStrictEqual(sigRS(C.Signature.fromCompact(sig.toCompactRawBytes())), sigRS(sig));
// DER
deepStrictEqual(sigRS(C.Signature.fromDER(sig.toDERHex())), sigRS(sig));
deepStrictEqual(sigRS(C.Signature.fromDER(sig.toDERRawBytes())), sigRS(sig));
}),
{ numRuns: NUM_RUNS }
)
);
should('Signature.addRecoveryBit/Signature.recoveryPublicKey', () =>
fc.assert(
fc.property(fc.hexaString({ minLength: 64, maxLength: 64 }), (msg) => {
const priv = C.utils.randomPrivateKey();
const pub = C.getPublicKey(priv);
const sig = C.sign(msg, priv);
deepStrictEqual(sig.recoverPublicKey(msg).toRawBytes(), pub);
const sig2 = C.Signature.fromCompact(sig.toCompactHex());
throws(() => sig2.recoverPublicKey(msg));
const sig3 = sig2.addRecoveryBit(sig.recovery);
deepStrictEqual(sig3.recoverPublicKey(msg).toRawBytes(), pub);
}),
{ numRuns: NUM_RUNS }
)
);
should('Signature.normalizeS', () =>
fc.assert(
fc.property(fc.hexaString({ minLength: 64, maxLength: 64 }), (msg) => {
const priv = C.utils.randomPrivateKey();
const pub = C.getPublicKey(priv);
const sig = C.sign(msg, priv);
const sig2 = sig.normalizeS();
deepStrictEqual(sig2.hasHighS(), false);
}),
{ numRuns: NUM_RUNS }
)
);
}
// NOTE: fails for ed, because of empty message. Since we convert it to scalar, // NOTE: fails for ed, because of empty message. Since we convert it to scalar,
// need to check what other implementations do. Empty message != new Uint8Array([0]), but what scalar should be in that case? // need to check what other implementations do. Empty message != new Uint8Array([0]), but what scalar should be in that case?
// should('should not verify signature with wrong message', () => { // should('should not verify signature with wrong message', () => {
@@ -618,10 +726,20 @@ should('secp224k1 sqrt bug', () => {
23621584063597419797792593680131996961517196803742576047493035507225n 23621584063597419797792593680131996961517196803742576047493035507225n
); );
deepStrictEqual( deepStrictEqual(
Fp.negate(sqrtMinus1), Fp.neg(sqrtMinus1),
3338362603553219996874421406887633712040719456283732096017030791656n 3338362603553219996874421406887633712040719456283732096017030791656n
); );
deepStrictEqual(Fp.square(sqrtMinus1), Fp.create(-1n)); deepStrictEqual(Fp.sqr(sqrtMinus1), Fp.create(-1n));
});
should('bigInt private keys', () => {
// Doesn't support bigints anymore
throws(() => ed25519.sign('', 123n));
throws(() => ed25519.getPublicKey(123n));
throws(() => x25519.getPublicKey(123n));
// Weierstrass still supports
secp256k1.getPublicKey(123n);
secp256k1.sign('', 123n);
}); });
// ESM is broken. // ESM is broken.

File diff suppressed because it is too large Load Diff

303
test/ed25519-addons.test.js Normal file
View File

@@ -0,0 +1,303 @@
import { sha512 } from '@noble/hashes/sha512';
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
import { deepStrictEqual, strictEqual, throws } from 'assert';
import { describe, should } from 'micro-should';
import { bytesToNumberLE, numberToBytesLE } from '../esm/abstract/utils.js';
import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' };
import { ed25519ctx, ed25519ph, RistrettoPoint, x25519 } from '../esm/ed25519.js';
// const ed = ed25519;
const hex = bytesToHex;
// const Point = ed.ExtendedPoint;
const VECTORS_RFC8032_CTX = [
{
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
message: 'f726936d19c800494e3fdaff20b276a8',
context: '666f6f',
signature:
'55a4cc2f70a54e04288c5f4cd1e45a7b' +
'b520b36292911876cada7323198dd87a' +
'8b36950b95130022907a7fb7c4e9b2d5' +
'f6cca685a587b4b21f4b888e4e7edb0d',
},
{
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
message: 'f726936d19c800494e3fdaff20b276a8',
context: '626172',
signature:
'fc60d5872fc46b3aa69f8b5b4351d580' +
'8f92bcc044606db097abab6dbcb1aee3' +
'216c48e8b3b66431b5b186d1d28f8ee1' +
'5a5ca2df6668346291c2043d4eb3e90d',
},
{
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
message: '508e9e6882b979fea900f62adceaca35',
context: '666f6f',
signature:
'8b70c1cc8310e1de20ac53ce28ae6e72' +
'07f33c3295e03bb5c0732a1d20dc6490' +
'8922a8b052cf99b7c4fe107a5abb5b2c' +
'4085ae75890d02df26269d8945f84b0b',
},
{
secretKey: 'ab9c2853ce297ddab85c993b3ae14bcad39b2c682beabc27d6d4eb20711d6560',
publicKey: '0f1d1274943b91415889152e893d80e93275a1fc0b65fd71b4b0dda10ad7d772',
message: 'f726936d19c800494e3fdaff20b276a8',
context: '666f6f',
signature:
'21655b5f1aa965996b3f97b3c849eafb' +
'a922a0a62992f73b3d1b73106a84ad85' +
'e9b86a7b6005ea868337ff2d20a7f5fb' +
'd4cd10b0be49a68da2b2e0dc0ad8960f',
},
];
describe('RFC8032ctx', () => {
for (let i = 0; i < VECTORS_RFC8032_CTX.length; i++) {
const v = VECTORS_RFC8032_CTX[i];
should(`${i}`, () => {
deepStrictEqual(hex(ed25519ctx.getPublicKey(v.secretKey)), v.publicKey);
deepStrictEqual(hex(ed25519ctx.sign(v.message, v.secretKey, v.context)), v.signature);
deepStrictEqual(ed25519ctx.verify(v.signature, v.message, v.publicKey, v.context), true);
});
}
});
const VECTORS_RFC8032_PH = [
{
secretKey: '833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42',
publicKey: 'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf',
message: '616263',
signature:
'98a70222f0b8121aa9d30f813d683f80' +
'9e462b469c7ff87639499bb94e6dae41' +
'31f85042463c2a355a2003d062adf5aa' +
'a10b8c61e636062aaad11c2a26083406',
},
];
describe('RFC8032ph', () => {
for (let i = 0; i < VECTORS_RFC8032_PH.length; i++) {
const v = VECTORS_RFC8032_PH[i];
should(`${i}`, () => {
deepStrictEqual(hex(ed25519ph.getPublicKey(v.secretKey)), v.publicKey);
deepStrictEqual(hex(ed25519ph.sign(v.message, v.secretKey)), v.signature);
deepStrictEqual(ed25519ph.verify(v.signature, v.message, v.publicKey), true);
});
}
});
// x25519
should('X25519 base point', () => {
const { y } = ed25519ph.ExtendedPoint.BASE;
const { Fp } = ed25519ph.CURVE;
const u = Fp.create((y + 1n) * Fp.inv(1n - y));
deepStrictEqual(numberToBytesLE(u, 32), x25519.GuBytes);
});
describe('RFC7748', () => {
const rfc7748Mul = [
{
scalar: 'a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4',
u: 'e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c',
outputU: 'c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552',
},
{
scalar: '4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d',
u: 'e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493',
outputU: '95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957',
},
];
for (let i = 0; i < rfc7748Mul.length; i++) {
const v = rfc7748Mul[i];
should(`scalarMult (${i})`, () => {
deepStrictEqual(hex(x25519.scalarMult(v.scalar, v.u)), v.outputU);
});
}
const rfc7748Iter = [
{ scalar: '422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079', iters: 1 },
{ scalar: '684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51', iters: 1000 },
// { scalar: '7c3911e0ab2586fd864497297e575e6f3bc601c0883c30df5f4dd2d24f665424', iters: 1000000 },
];
for (let i = 0; i < rfc7748Iter.length; i++) {
const { scalar, iters } = rfc7748Iter[i];
should(`scalarMult iteration (${i})`, () => {
let k = x25519.GuBytes;
for (let i = 0, u = k; i < iters; i++) [k, u] = [x25519.scalarMult(k, u), k];
deepStrictEqual(hex(k), scalar);
});
}
should('getSharedKey', () => {
const alicePrivate = '77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a';
const alicePublic = '8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a';
const bobPrivate = '5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb';
const bobPublic = 'de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f';
const shared = '4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742';
deepStrictEqual(alicePublic, hex(x25519.getPublicKey(alicePrivate)));
deepStrictEqual(bobPublic, hex(x25519.getPublicKey(bobPrivate)));
deepStrictEqual(hex(x25519.scalarMult(alicePrivate, bobPublic)), shared);
deepStrictEqual(hex(x25519.scalarMult(bobPrivate, alicePublic)), shared);
});
});
describe('Wycheproof', () => {
const group = x25519vectors.testGroups[0];
should(`X25519`, () => {
for (let i = 0; i < group.tests.length; i++) {
const v = group.tests[i];
const comment = `(${i}, ${v.result}) ${v.comment}`;
if (v.result === 'valid' || v.result === 'acceptable') {
try {
const shared = hex(x25519.scalarMult(v.private, v.public));
deepStrictEqual(shared, v.shared, comment);
} catch (e) {
// We are more strict
if (e.message.includes('Expected valid scalar')) return;
if (e.message.includes('Invalid private or public key received')) return;
throw e;
}
} else if (v.result === 'invalid') {
let failed = false;
try {
x25519.scalarMult(v.private, v.public);
} catch (error) {
failed = true;
}
deepStrictEqual(failed, true, comment);
} else throw new Error('unknown test result');
}
});
});
function utf8ToBytes(str) {
if (typeof str !== 'string') {
throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
}
return new TextEncoder().encode(str);
}
describe('ristretto255', () => {
should('follow the byte encodings of small multiples', () => {
const encodingsOfSmallMultiples = [
// This is the identity point
'0000000000000000000000000000000000000000000000000000000000000000',
// This is the basepoint
'e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76',
// These are small multiples of the basepoint
'6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919',
'94741f5d5d52755ece4f23f044ee27d5d1ea1e2bd196b462166b16152a9d0259',
'da80862773358b466ffadfe0b3293ab3d9fd53c5ea6c955358f568322daf6a57',
'e882b131016b52c1d3337080187cf768423efccbb517bb495ab812c4160ff44e',
'f64746d3c92b13050ed8d80236a7f0007c3b3f962f5ba793d19a601ebb1df403',
'44f53520926ec81fbd5a387845beb7df85a96a24ece18738bdcfa6a7822a176d',
'903293d8f2287ebe10e2374dc1a53e0bc887e592699f02d077d5263cdd55601c',
'02622ace8f7303a31cafc63f8fc48fdc16e1c8c8d234b2f0d6685282a9076031',
'20706fd788b2720a1ed2a5dad4952b01f413bcf0e7564de8cdc816689e2db95f',
'bce83f8ba5dd2fa572864c24ba1810f9522bc6004afe95877ac73241cafdab42',
'e4549ee16b9aa03099ca208c67adafcafa4c3f3e4e5303de6026e3ca8ff84460',
'aa52e000df2e16f55fb1032fc33bc42742dad6bd5a8fc0be0167436c5948501f',
'46376b80f409b29dc2b5f6f0c52591990896e5716f41477cd30085ab7f10301e',
'e0c418f7c8d9c4cdd7395b93ea124f3ad99021bb681dfc3302a9d99a2e53e64e',
];
let B = RistrettoPoint.BASE;
let P = RistrettoPoint.ZERO;
for (const encoded of encodingsOfSmallMultiples) {
deepStrictEqual(P.toHex(), encoded);
deepStrictEqual(RistrettoPoint.fromHex(encoded).toHex(), encoded);
P = P.add(B);
}
});
should('not convert bad bytes encoding', () => {
const badEncodings = [
// These are all bad because they're non-canonical field encodings.
'00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
'f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
'edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
// These are all bad because they're negative field elements.
'0100000000000000000000000000000000000000000000000000000000000000',
'01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
'ed57ffd8c914fb201471d1c3d245ce3c746fcbe63a3679d51b6a516ebebe0e20',
'c34c4e1826e5d403b78e246e88aa051c36ccf0aafebffe137d148a2bf9104562',
'c940e5a4404157cfb1628b108db051a8d439e1a421394ec4ebccb9ec92a8ac78',
'47cfc5497c53dc8e61c91d17fd626ffb1c49e2bca94eed052281b510b1117a24',
'f1c6165d33367351b0da8f6e4511010c68174a03b6581212c71c0e1d026c3c72',
'87260f7a2f12495118360f02c26a470f450dadf34a413d21042b43b9d93e1309',
// These are all bad because they give a nonsquare x².
'26948d35ca62e643e26a83177332e6b6afeb9d08e4268b650f1f5bbd8d81d371',
'4eac077a713c57b4f4397629a4145982c661f48044dd3f96427d40b147d9742f',
'de6a7b00deadc788eb6b6c8d20c0ae96c2f2019078fa604fee5b87d6e989ad7b',
'bcab477be20861e01e4a0e295284146a510150d9817763caf1a6f4b422d67042',
'2a292df7e32cababbd9de088d1d1abec9fc0440f637ed2fba145094dc14bea08',
'f4a9e534fc0d216c44b218fa0c42d99635a0127ee2e53c712f70609649fdff22',
'8268436f8c4126196cf64b3c7ddbda90746a378625f9813dd9b8457077256731',
'2810e5cbc2cc4d4eece54f61c6f69758e289aa7ab440b3cbeaa21995c2f4232b',
// These are all bad because they give a negative xy value.
'3eb858e78f5a7254d8c9731174a94f76755fd3941c0ac93735c07ba14579630e',
'a45fdc55c76448c049a1ab33f17023edfb2be3581e9c7aade8a6125215e04220',
'd483fe813c6ba647ebbfd3ec41adca1c6130c2beeee9d9bf065c8d151c5f396e',
'8a2e1d30050198c65a54483123960ccc38aef6848e1ec8f5f780e8523769ba32',
'32888462f8b486c68ad7dd9610be5192bbeaf3b443951ac1a8118419d9fa097b',
'227142501b9d4355ccba290404bde41575b037693cef1f438c47f8fbf35d1165',
'5c37cc491da847cfeb9281d407efc41e15144c876e0170b499a96a22ed31e01e',
'445425117cb8c90edcbc7c1cc0e74f747f2c1efa5630a967c64f287792a48a4b',
// This is s = -1, which causes y = 0.
'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
];
for (const badBytes of badEncodings) {
const b = hexToBytes(badBytes);
throws(() => RistrettoPoint.fromHex(b), badBytes);
}
});
should('create right points from uniform hash', () => {
const labels = [
'Ristretto is traditionally a short shot of espresso coffee',
'made with the normal amount of ground coffee but extracted with',
'about half the amount of water in the same amount of time',
'by using a finer grind.',
'This produces a concentrated shot of coffee per volume.',
'Just pulling a normal shot short will produce a weaker shot',
'and is not a Ristretto as some believe.',
];
const encodedHashToPoints = [
'3066f82a1a747d45120d1740f14358531a8f04bbffe6a819f86dfe50f44a0a46',
'f26e5b6f7d362d2d2a94c5d0e7602cb4773c95a2e5c31a64f133189fa76ed61b',
'006ccd2a9e6867e6a2c5cea83d3302cc9de128dd2a9a57dd8ee7b9d7ffe02826',
'f8f0c87cf237953c5890aec3998169005dae3eca1fbb04548c635953c817f92a',
'ae81e7dedf20a497e10c304a765c1767a42d6e06029758d2d7e8ef7cc4c41179',
'e2705652ff9f5e44d3e841bf1c251cf7dddb77d140870d1ab2ed64f1a9ce8628',
'80bd07262511cdde4863f8a7434cef696750681cb9510eea557088f76d9e5065',
];
for (let i = 0; i < labels.length; i++) {
const hash = sha512(utf8ToBytes(labels[i]));
const point = RistrettoPoint.hashToCurve(hash);
deepStrictEqual(point.toHex(), encodedHashToPoints[i]);
}
});
should('have proper equality testing', () => {
const MAX_255B = BigInt('0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
const bytes255ToNumberLE = (bytes) =>
ed25519ctx.CURVE.Fp.create(bytesToNumberLE(bytes) & MAX_255B);
const priv = new Uint8Array([
198, 101, 65, 165, 93, 120, 37, 238, 16, 133, 10, 35, 253, 243, 161, 246, 229, 135, 12, 137,
202, 114, 222, 139, 146, 123, 4, 125, 152, 173, 1, 7,
]);
const pub = RistrettoPoint.BASE.multiply(bytes255ToNumberLE(priv));
deepStrictEqual(pub.equals(RistrettoPoint.ZERO), false);
});
});
// ESM is broken.
import url from 'url';
import { assert } from 'console';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run();
}

1
test/ed25519.helpers.js Normal file
View File

@@ -0,0 +1 @@
export { ed25519, ED25519_TORSION_SUBGROUP } from '../esm/ed25519.js';

View File

@@ -1,25 +1,16 @@
import { deepEqual, deepStrictEqual, strictEqual, throws } from 'assert'; import { deepStrictEqual, strictEqual, throws } from 'assert';
import { describe, should } from 'micro-should';
import * as fc from 'fast-check';
import {
ed25519,
ed25519ctx,
ed25519ph,
x25519,
RistrettoPoint,
ED25519_TORSION_SUBGROUP,
} from '../lib/esm/ed25519.js';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils'; import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
import { numberToBytesLE } from '../lib/esm/abstract/utils.js'; import * as fc from 'fast-check';
import { sha512 } from '@noble/hashes/sha512'; import { describe, should } from 'micro-should';
import { ed25519, ED25519_TORSION_SUBGROUP } from './ed25519.helpers.js';
import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' }; import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' };
import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' }; import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
describe('ed25519', () => { describe('ed25519', () => {
const ed = ed25519; const ed = ed25519;
const hex = bytesToHex; const hex = bytesToHex;
const Point = ed.ExtendedPoint;
function to32Bytes(numOrStr) { function to32Bytes(numOrStr) {
let hex = typeof numOrStr === 'string' ? numOrStr : numOrStr.toString(16); let hex = typeof numOrStr === 'string' ? numOrStr : numOrStr.toString(16);
@@ -28,7 +19,7 @@ describe('ed25519', () => {
function utf8ToBytes(str) { function utf8ToBytes(str) {
if (typeof str !== 'string') { if (typeof str !== 'string') {
throw new TypeError(`utf8ToBytes expected string, got ${typeof str}`); throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
} }
return new TextEncoder().encode(str); return new TextEncoder().encode(str);
} }
@@ -78,6 +69,7 @@ describe('ed25519', () => {
); );
}); });
const privKey = to32Bytes('a665a45920422f9d417e4867ef'); const privKey = to32Bytes('a665a45920422f9d417e4867ef');
const wrongPriv = to32Bytes('a675a45920422f9d417e4867ef');
const msg = hexToBytes('874f9960c5d2b7a9b5fad383e1ba44719ebb743a'); const msg = hexToBytes('874f9960c5d2b7a9b5fad383e1ba44719ebb743a');
const wrongMsg = hexToBytes('589d8c7f1da0a24bc07b7381ad48b1cfc211af1c'); const wrongMsg = hexToBytes('589d8c7f1da0a24bc07b7381ad48b1cfc211af1c');
describe('basic methods', () => { describe('basic methods', () => {
@@ -86,16 +78,6 @@ describe('ed25519', () => {
const signature = ed.sign(msg, privKey); const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, msg, publicKey), true); deepStrictEqual(ed.verify(signature, msg, publicKey), true);
}); });
should('not verify signature with wrong public key', () => {
const publicKey = ed.getPublicKey(12);
const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, msg, publicKey), false);
});
should('not verify signature with wrong hash', () => {
const publicKey = ed.getPublicKey(privKey);
const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, wrongMsg, publicKey), false);
});
}); });
describe('sync methods', () => { describe('sync methods', () => {
should('sign and verify', () => { should('sign and verify', () => {
@@ -104,7 +86,7 @@ describe('ed25519', () => {
deepStrictEqual(ed.verify(signature, msg, publicKey), true); deepStrictEqual(ed.verify(signature, msg, publicKey), true);
}); });
should('not verify signature with wrong public key', () => { should('not verify signature with wrong public key', () => {
const publicKey = ed.getPublicKey(12); const publicKey = ed.getPublicKey(wrongPriv);
const signature = ed.sign(msg, privKey); const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, msg, publicKey), false); deepStrictEqual(ed.verify(signature, msg, publicKey), false);
}); });
@@ -114,56 +96,46 @@ describe('ed25519', () => {
deepStrictEqual(ed.verify(signature, wrongMsg, publicKey), false); deepStrictEqual(ed.verify(signature, wrongMsg, publicKey), false);
}); });
}); });
describe('BASE_POINT.multiply()', () => {
// https://xmr.llcoins.net/addresstests.html // https://xmr.llcoins.net/addresstests.html
should( should('create right publicKey without SHA-512 hashing TEST 1', () => {
'ed25519/BASE_POINT.multiply()/should create right publicKey without SHA-512 hashing TEST 1',
() => {
const publicKey = const publicKey =
ed.Point.BASE.multiply(0x90af56259a4b6bfbc4337980d5d75fbe3c074630368ff3804d33028e5dbfa77n); Point.BASE.multiply(0x90af56259a4b6bfbc4337980d5d75fbe3c074630368ff3804d33028e5dbfa77n);
deepStrictEqual( deepStrictEqual(
publicKey.toHex(), publicKey.toHex(),
'0f3b913371411b27e646b537e888f685bf929ea7aab93c950ed84433f064480d' '0f3b913371411b27e646b537e888f685bf929ea7aab93c950ed84433f064480d'
); );
} });
); should('create right publicKey without SHA-512 hashing TEST 2', () => {
should(
'ed25519/BASE_POINT.multiply()/should create right publicKey without SHA-512 hashing TEST 2',
() => {
const publicKey = const publicKey =
ed.Point.BASE.multiply(0x364e8711a60780382a5d57b061c126f039940f28a9e91fe039d4d3094d8b88n); Point.BASE.multiply(0x364e8711a60780382a5d57b061c126f039940f28a9e91fe039d4d3094d8b88n);
deepStrictEqual( deepStrictEqual(
publicKey.toHex(), publicKey.toHex(),
'ad545340b58610f0cd62f17d55af1ab11ecde9c084d5476865ddb4dbda015349' 'ad545340b58610f0cd62f17d55af1ab11ecde9c084d5476865ddb4dbda015349'
); );
} });
); should('create right publicKey without SHA-512 hashing TEST 3', () => {
should(
'ed25519/BASE_POINT.multiply()/should create right publicKey without SHA-512 hashing TEST 3',
() => {
const publicKey = const publicKey =
ed.Point.BASE.multiply(0xb9bf90ff3abec042752cac3a07a62f0c16cfb9d32a3fc2305d676ec2d86e941n); Point.BASE.multiply(0xb9bf90ff3abec042752cac3a07a62f0c16cfb9d32a3fc2305d676ec2d86e941n);
deepStrictEqual( deepStrictEqual(
publicKey.toHex(), publicKey.toHex(),
'e097c4415fe85724d522b2e449e8fd78dd40d20097bdc9ae36fe8ec6fe12cb8c' 'e097c4415fe85724d522b2e449e8fd78dd40d20097bdc9ae36fe8ec6fe12cb8c'
); );
} });
); should('create right publicKey without SHA-512 hashing TEST 4', () => {
should(
'ed25519/BASE_POINT.multiply()/should create right publicKey without SHA-512 hashing TEST 4',
() => {
const publicKey = const publicKey =
ed.Point.BASE.multiply(0x69d896f02d79524c9878e080308180e2859d07f9f54454e0800e8db0847a46en); Point.BASE.multiply(0x69d896f02d79524c9878e080308180e2859d07f9f54454e0800e8db0847a46en);
deepStrictEqual( deepStrictEqual(
publicKey.toHex(), publicKey.toHex(),
'f12cb7c43b59971395926f278ce7c2eaded9444fbce62ca717564cb508a0db1d' 'f12cb7c43b59971395926f278ce7c2eaded9444fbce62ca717564cb508a0db1d'
); );
} });
); should('throw Point#multiply on TEST 5', () => {
should('ed25519/BASE_POINT.multiply()/should throw Point#multiply on TEST 5', () => {
for (const num of [0n, 0, -1n, -1, 1.1]) { for (const num of [0n, 0, -1n, -1, 1.1]) {
throws(() => ed.Point.BASE.multiply(num)); throws(() => Point.BASE.multiply(num));
} }
}); });
});
// https://ed25519.cr.yp.to/python/sign.py // https://ed25519.cr.yp.to/python/sign.py
// https://ed25519.cr.yp.to/python/sign.input // https://ed25519.cr.yp.to/python/sign.input
@@ -184,7 +156,7 @@ describe('ed25519', () => {
// Calculate // Calculate
const pub = ed.getPublicKey(to32Bytes(priv)); const pub = ed.getPublicKey(to32Bytes(priv));
deepStrictEqual(hex(pub), expectedPub); deepStrictEqual(hex(pub), expectedPub);
deepStrictEqual(pub, ed.Point.fromHex(pub).toRawBytes()); deepStrictEqual(pub, Point.fromHex(pub).toRawBytes());
const signature = hex(ed.sign(msg, priv)); const signature = hex(ed.sign(msg, priv));
// console.log('vector', i); // console.log('vector', i);
@@ -310,104 +282,6 @@ describe('ed25519', () => {
// // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY); // // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY);
// // expect(await ristretto25519.verify(signature, WRONG_MESSAGE, publicKey)).toBe(false); // // expect(await ristretto25519.verify(signature, WRONG_MESSAGE, publicKey)).toBe(false);
// // }); // // });
should('ristretto255/should follow the byte encodings of small multiples', () => {
const encodingsOfSmallMultiples = [
// This is the identity point
'0000000000000000000000000000000000000000000000000000000000000000',
// This is the basepoint
'e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76',
// These are small multiples of the basepoint
'6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919',
'94741f5d5d52755ece4f23f044ee27d5d1ea1e2bd196b462166b16152a9d0259',
'da80862773358b466ffadfe0b3293ab3d9fd53c5ea6c955358f568322daf6a57',
'e882b131016b52c1d3337080187cf768423efccbb517bb495ab812c4160ff44e',
'f64746d3c92b13050ed8d80236a7f0007c3b3f962f5ba793d19a601ebb1df403',
'44f53520926ec81fbd5a387845beb7df85a96a24ece18738bdcfa6a7822a176d',
'903293d8f2287ebe10e2374dc1a53e0bc887e592699f02d077d5263cdd55601c',
'02622ace8f7303a31cafc63f8fc48fdc16e1c8c8d234b2f0d6685282a9076031',
'20706fd788b2720a1ed2a5dad4952b01f413bcf0e7564de8cdc816689e2db95f',
'bce83f8ba5dd2fa572864c24ba1810f9522bc6004afe95877ac73241cafdab42',
'e4549ee16b9aa03099ca208c67adafcafa4c3f3e4e5303de6026e3ca8ff84460',
'aa52e000df2e16f55fb1032fc33bc42742dad6bd5a8fc0be0167436c5948501f',
'46376b80f409b29dc2b5f6f0c52591990896e5716f41477cd30085ab7f10301e',
'e0c418f7c8d9c4cdd7395b93ea124f3ad99021bb681dfc3302a9d99a2e53e64e',
];
let B = RistrettoPoint.BASE;
let P = RistrettoPoint.ZERO;
for (const encoded of encodingsOfSmallMultiples) {
deepStrictEqual(P.toHex(), encoded);
deepStrictEqual(RistrettoPoint.fromHex(encoded).toHex(), encoded);
P = P.add(B);
}
});
should('ristretto255/should not convert bad bytes encoding', () => {
const badEncodings = [
// These are all bad because they're non-canonical field encodings.
'00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
'f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
'edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
// These are all bad because they're negative field elements.
'0100000000000000000000000000000000000000000000000000000000000000',
'01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
'ed57ffd8c914fb201471d1c3d245ce3c746fcbe63a3679d51b6a516ebebe0e20',
'c34c4e1826e5d403b78e246e88aa051c36ccf0aafebffe137d148a2bf9104562',
'c940e5a4404157cfb1628b108db051a8d439e1a421394ec4ebccb9ec92a8ac78',
'47cfc5497c53dc8e61c91d17fd626ffb1c49e2bca94eed052281b510b1117a24',
'f1c6165d33367351b0da8f6e4511010c68174a03b6581212c71c0e1d026c3c72',
'87260f7a2f12495118360f02c26a470f450dadf34a413d21042b43b9d93e1309',
// These are all bad because they give a nonsquare x².
'26948d35ca62e643e26a83177332e6b6afeb9d08e4268b650f1f5bbd8d81d371',
'4eac077a713c57b4f4397629a4145982c661f48044dd3f96427d40b147d9742f',
'de6a7b00deadc788eb6b6c8d20c0ae96c2f2019078fa604fee5b87d6e989ad7b',
'bcab477be20861e01e4a0e295284146a510150d9817763caf1a6f4b422d67042',
'2a292df7e32cababbd9de088d1d1abec9fc0440f637ed2fba145094dc14bea08',
'f4a9e534fc0d216c44b218fa0c42d99635a0127ee2e53c712f70609649fdff22',
'8268436f8c4126196cf64b3c7ddbda90746a378625f9813dd9b8457077256731',
'2810e5cbc2cc4d4eece54f61c6f69758e289aa7ab440b3cbeaa21995c2f4232b',
// These are all bad because they give a negative xy value.
'3eb858e78f5a7254d8c9731174a94f76755fd3941c0ac93735c07ba14579630e',
'a45fdc55c76448c049a1ab33f17023edfb2be3581e9c7aade8a6125215e04220',
'd483fe813c6ba647ebbfd3ec41adca1c6130c2beeee9d9bf065c8d151c5f396e',
'8a2e1d30050198c65a54483123960ccc38aef6848e1ec8f5f780e8523769ba32',
'32888462f8b486c68ad7dd9610be5192bbeaf3b443951ac1a8118419d9fa097b',
'227142501b9d4355ccba290404bde41575b037693cef1f438c47f8fbf35d1165',
'5c37cc491da847cfeb9281d407efc41e15144c876e0170b499a96a22ed31e01e',
'445425117cb8c90edcbc7c1cc0e74f747f2c1efa5630a967c64f287792a48a4b',
// This is s = -1, which causes y = 0.
'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
];
for (const badBytes of badEncodings) {
const b = hexToBytes(badBytes);
throws(() => RistrettoPoint.fromHex(b), badBytes);
}
});
should('ristretto255/should create right points from uniform hash', async () => {
const labels = [
'Ristretto is traditionally a short shot of espresso coffee',
'made with the normal amount of ground coffee but extracted with',
'about half the amount of water in the same amount of time',
'by using a finer grind.',
'This produces a concentrated shot of coffee per volume.',
'Just pulling a normal shot short will produce a weaker shot',
'and is not a Ristretto as some believe.',
];
const encodedHashToPoints = [
'3066f82a1a747d45120d1740f14358531a8f04bbffe6a819f86dfe50f44a0a46',
'f26e5b6f7d362d2d2a94c5d0e7602cb4773c95a2e5c31a64f133189fa76ed61b',
'006ccd2a9e6867e6a2c5cea83d3302cc9de128dd2a9a57dd8ee7b9d7ffe02826',
'f8f0c87cf237953c5890aec3998169005dae3eca1fbb04548c635953c817f92a',
'ae81e7dedf20a497e10c304a765c1767a42d6e06029758d2d7e8ef7cc4c41179',
'e2705652ff9f5e44d3e841bf1c251cf7dddb77d140870d1ab2ed64f1a9ce8628',
'80bd07262511cdde4863f8a7434cef696750681cb9510eea557088f76d9e5065',
];
for (let i = 0; i < labels.length; i++) {
const hash = sha512(utf8ToBytes(labels[i]));
const point = RistrettoPoint.hashToCurve(hash);
deepStrictEqual(point.toHex(), encodedHashToPoints[i]);
}
});
should('input immutability: sign/verify are immutable', () => { should('input immutability: sign/verify are immutable', () => {
const privateKey = ed.utils.randomPrivateKey(); const privateKey = ed.utils.randomPrivateKey();
@@ -440,58 +314,14 @@ describe('ed25519', () => {
} catch (e) { } catch (e) {
noble = false; noble = false;
} }
deepStrictEqual(noble, v.valid_zip215); deepStrictEqual(noble, v.valid_zip215, JSON.stringify(v));
} }
}); });
should('ZIP-215 compliance tests/disallows sig.s >= CURVE.n', () => { should('ZIP-215 compliance tests/disallows sig.s >= CURVE.n', () => {
const sig = new ed.Signature(ed.Point.BASE, 1n); // sig.R = BASE, sig.s = N+1
sig.s = ed.CURVE.n + 1n; const sig =
throws(() => ed.verify(sig, 'deadbeef', ed.Point.BASE)); '5866666666666666666666666666666666666666666666666666666666666666eed3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010';
}); throws(() => ed.verify(sig, 'deadbeef', Point.BASE));
const rfc7748Mul = [
{
scalar: 'a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4',
u: 'e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c',
outputU: 'c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552',
},
{
scalar: '4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d',
u: 'e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493',
outputU: '95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957',
},
];
for (let i = 0; i < rfc7748Mul.length; i++) {
const v = rfc7748Mul[i];
should(`RFC7748: scalarMult (${i})`, () => {
deepStrictEqual(hex(x25519.scalarMult(v.scalar, v.u)), v.outputU);
});
}
const rfc7748Iter = [
{ scalar: '422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079', iters: 1 },
{ scalar: '684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51', iters: 1000 },
// { scalar: '7c3911e0ab2586fd864497297e575e6f3bc601c0883c30df5f4dd2d24f665424', iters: 1000000 },
];
for (let i = 0; i < rfc7748Iter.length; i++) {
const { scalar, iters } = rfc7748Iter[i];
should(`RFC7748: scalarMult iteration (${i})`, () => {
let k = x25519.Gu;
for (let i = 0, u = k; i < iters; i++) [k, u] = [x25519.scalarMult(k, u), k];
deepStrictEqual(hex(k), scalar);
});
}
should('RFC7748 getSharedKey', () => {
const alicePrivate = '77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a';
const alicePublic = '8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a';
const bobPrivate = '5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb';
const bobPublic = 'de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f';
const shared = '4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742';
deepStrictEqual(alicePublic, hex(x25519.getPublicKey(alicePrivate)));
deepStrictEqual(bobPublic, hex(x25519.getPublicKey(bobPrivate)));
deepStrictEqual(hex(x25519.scalarMult(alicePrivate, bobPublic)), shared);
deepStrictEqual(hex(x25519.scalarMult(bobPrivate, alicePublic)), shared);
}); });
// should('X25519/getSharedSecret() should be commutative', () => { // should('X25519/getSharedSecret() should be commutative', () => {
@@ -511,40 +341,11 @@ describe('ed25519', () => {
// should('X25519: should convert base point to montgomery using fromPoint', () => { // should('X25519: should convert base point to montgomery using fromPoint', () => {
// deepStrictEqual( // deepStrictEqual(
// hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)), // hex(ed.montgomeryCurve.UfromPoint(Point.BASE)),
// ed.montgomeryCurve.BASE_POINT_U // ed.montgomeryCurve.BASE_POINT_U
// ); // );
// }); // });
{
const group = x25519vectors.testGroups[0];
should(`Wycheproof/X25519`, () => {
for (let i = 0; i < group.tests.length; i++) {
const v = group.tests[i];
const comment = `(${i}, ${v.result}) ${v.comment}`;
if (v.result === 'valid' || v.result === 'acceptable') {
try {
const shared = hex(x25519.scalarMult(v.private, v.public));
deepStrictEqual(shared, v.shared, comment);
} catch (e) {
// We are more strict
if (e.message.includes('Expected valid scalar')) return;
if (e.message.includes('Invalid private or public key received')) return;
throw e;
}
} else if (v.result === 'invalid') {
let failed = false;
try {
x25519.scalarMult(v.private, v.public);
} catch (error) {
failed = true;
}
deepStrictEqual(failed, true, comment);
} else throw new Error('unknown test result');
}
});
}
should(`Wycheproof/ED25519`, () => { should(`Wycheproof/ED25519`, () => {
for (let g = 0; g < ed25519vectors.testGroups.length; g++) { for (let g = 0; g < ed25519vectors.testGroups.length; g++) {
const group = ed25519vectors.testGroups[g]; const group = ed25519vectors.testGroups[g];
@@ -576,95 +377,10 @@ describe('ed25519', () => {
deepStrictEqual(ed.verify(signature, message, publicKey), true); deepStrictEqual(ed.verify(signature, message, publicKey), true);
}); });
const VECTORS_RFC8032_CTX = [
{
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
message: 'f726936d19c800494e3fdaff20b276a8',
context: '666f6f',
signature:
'55a4cc2f70a54e04288c5f4cd1e45a7b' +
'b520b36292911876cada7323198dd87a' +
'8b36950b95130022907a7fb7c4e9b2d5' +
'f6cca685a587b4b21f4b888e4e7edb0d',
},
{
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
message: 'f726936d19c800494e3fdaff20b276a8',
context: '626172',
signature:
'fc60d5872fc46b3aa69f8b5b4351d580' +
'8f92bcc044606db097abab6dbcb1aee3' +
'216c48e8b3b66431b5b186d1d28f8ee1' +
'5a5ca2df6668346291c2043d4eb3e90d',
},
{
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
message: '508e9e6882b979fea900f62adceaca35',
context: '666f6f',
signature:
'8b70c1cc8310e1de20ac53ce28ae6e72' +
'07f33c3295e03bb5c0732a1d20dc6490' +
'8922a8b052cf99b7c4fe107a5abb5b2c' +
'4085ae75890d02df26269d8945f84b0b',
},
{
secretKey: 'ab9c2853ce297ddab85c993b3ae14bcad39b2c682beabc27d6d4eb20711d6560',
publicKey: '0f1d1274943b91415889152e893d80e93275a1fc0b65fd71b4b0dda10ad7d772',
message: 'f726936d19c800494e3fdaff20b276a8',
context: '666f6f',
signature:
'21655b5f1aa965996b3f97b3c849eafb' +
'a922a0a62992f73b3d1b73106a84ad85' +
'e9b86a7b6005ea868337ff2d20a7f5fb' +
'd4cd10b0be49a68da2b2e0dc0ad8960f',
},
];
for (let i = 0; i < VECTORS_RFC8032_CTX.length; i++) {
const v = VECTORS_RFC8032_CTX[i];
should(`RFC8032ctx/${i}`, () => {
deepStrictEqual(hex(ed25519ctx.getPublicKey(v.secretKey)), v.publicKey);
deepStrictEqual(hex(ed25519ctx.sign(v.message, v.secretKey, v.context)), v.signature);
deepStrictEqual(ed25519ctx.verify(v.signature, v.message, v.publicKey, v.context), true);
});
}
const VECTORS_RFC8032_PH = [
{
secretKey: '833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42',
publicKey: 'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf',
message: '616263',
signature:
'98a70222f0b8121aa9d30f813d683f80' +
'9e462b469c7ff87639499bb94e6dae41' +
'31f85042463c2a355a2003d062adf5aa' +
'a10b8c61e636062aaad11c2a26083406',
},
];
for (let i = 0; i < VECTORS_RFC8032_PH.length; i++) {
const v = VECTORS_RFC8032_PH[i];
should(`RFC8032ph/${i}`, () => {
deepStrictEqual(hex(ed25519ph.getPublicKey(v.secretKey)), v.publicKey);
deepStrictEqual(hex(ed25519ph.sign(v.message, v.secretKey)), v.signature);
deepStrictEqual(ed25519ph.verify(v.signature, v.message, v.publicKey), true);
});
}
should('X25519 base point', () => {
const { y } = ed25519.Point.BASE;
const { Fp } = ed25519.CURVE;
const u = Fp.create((y + 1n) * Fp.invert(1n - y));
deepStrictEqual(hex(numberToBytesLE(u, 32)), x25519.Gu);
});
should('isTorsionFree()', () => { should('isTorsionFree()', () => {
const orig = ed.utils.getExtendedPublicKey(ed.utils.randomPrivateKey()).point; const orig = ed.utils.getExtendedPublicKey(ed.utils.randomPrivateKey()).point;
for (const hex of ED25519_TORSION_SUBGROUP.slice(1)) { for (const hex of ED25519_TORSION_SUBGROUP.slice(1)) {
const dirty = orig.add(ed.Point.fromHex(hex)); const dirty = orig.add(Point.fromHex(hex));
const cleared = dirty.clearCofactor(); const cleared = dirty.clearCofactor();
strictEqual(orig.isTorsionFree(), true, `orig must be torsionFree: ${hex}`); strictEqual(orig.isTorsionFree(), true, `orig must be torsionFree: ${hex}`);
strictEqual(dirty.isTorsionFree(), false, `dirty must not be torsionFree: ${hex}`); strictEqual(dirty.isTorsionFree(), false, `dirty must not be torsionFree: ${hex}`);
@@ -673,6 +389,15 @@ describe('ed25519', () => {
}); });
}); });
should('ed25519 bug', () => {
const t = 81718630521762619991978402609047527194981150691135404693881672112315521837062n;
const point = ed25519.ExtendedPoint.fromAffine({ x: t, y: t });
throws(() => point.assertValidity());
// Otherwise (without assertValidity):
// const point2 = point.double();
// point2.toAffine(); // crash!
});
// ESM is broken. // ESM is broken.
import url from 'url'; import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {

View File

@@ -1,9 +1,9 @@
import { deepStrictEqual, throws } from 'assert'; import { deepStrictEqual, throws } from 'assert';
import { describe, should } from 'micro-should'; import { describe, should } from 'micro-should';
import * as fc from 'fast-check'; import * as fc from 'fast-check';
import { ed448, ed448ph, x448 } from '../lib/esm/ed448.js'; import { ed448, ed448ph, x448 } from '../esm/ed448.js';
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils'; import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
import { numberToBytesLE } from '../lib/esm/abstract/utils.js'; import { numberToBytesLE } from '../esm/abstract/utils.js';
import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' }; import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' };
import { default as x448vectors } from './wycheproof/x448_test.json' assert { type: 'json' }; import { default as x448vectors } from './wycheproof/x448_test.json' assert { type: 'json' };
@@ -11,9 +11,10 @@ describe('ed448', () => {
const ed = ed448; const ed = ed448;
const hex = bytesToHex; const hex = bytesToHex;
ed.utils.precompute(4); ed.utils.precompute(4);
const Point = ed.ExtendedPoint;
should(`Basic`, () => { should(`Basic`, () => {
const G1 = ed.Point.BASE; const G1 = Point.BASE.toAffine();
deepStrictEqual( deepStrictEqual(
G1.x, G1.x,
224580040295924300187604334099896036246789641632564134246125461686950415467406032909029192869357953282578032075146446173674602635247710n 224580040295924300187604334099896036246789641632564134246125461686950415467406032909029192869357953282578032075146446173674602635247710n
@@ -22,7 +23,7 @@ describe('ed448', () => {
G1.y, G1.y,
298819210078481492676017930443930673437544040154080242095928241372331506189835876003536878655418784733982303233503462500531545062832660n 298819210078481492676017930443930673437544040154080242095928241372331506189835876003536878655418784733982303233503462500531545062832660n
); );
const G2 = ed.Point.BASE.multiply(2n); const G2 = Point.BASE.multiply(2n).toAffine();
deepStrictEqual( deepStrictEqual(
G2.x, G2.x,
484559149530404593699549205258669689569094240458212040187660132787056912146709081364401144455726350866276831544947397859048262938744149n 484559149530404593699549205258669689569094240458212040187660132787056912146709081364401144455726350866276831544947397859048262938744149n
@@ -31,7 +32,7 @@ describe('ed448', () => {
G2.y, G2.y,
494088759867433727674302672526735089350544552303727723746126484473087719117037293890093462157703888342865036477787453078312060500281069n 494088759867433727674302672526735089350544552303727723746126484473087719117037293890093462157703888342865036477787453078312060500281069n
); );
const G3 = ed.Point.BASE.multiply(3n); const G3 = Point.BASE.multiply(3n).toAffine();
deepStrictEqual( deepStrictEqual(
G3.x, G3.x,
23839778817283171003887799738662344287085130522697782688245073320169861206004018274567429238677677920280078599146891901463786155880335n 23839778817283171003887799738662344287085130522697782688245073320169861206004018274567429238677677920280078599146891901463786155880335n
@@ -43,12 +44,12 @@ describe('ed448', () => {
}); });
should('Basic/decompress', () => { should('Basic/decompress', () => {
const G1 = ed.Point.BASE; const G1 = Point.BASE;
const G2 = ed.Point.BASE.multiply(2n); const G2 = Point.BASE.multiply(2n);
const G3 = ed.Point.BASE.multiply(3n); const G3 = Point.BASE.multiply(3n);
const points = [G1, G2, G3]; const points = [G1, G2, G3];
const getXY = (p) => ({ x: p.x, y: p.y }); const getXY = (p) => p.toAffine();
for (const p of points) deepStrictEqual(getXY(ed.Point.fromHex(p.toHex())), getXY(p)); for (const p of points) deepStrictEqual(getXY(Point.fromHex(p.toHex())), getXY(p));
}); });
const VECTORS_RFC8032 = [ const VECTORS_RFC8032 = [
@@ -315,16 +316,18 @@ describe('ed448', () => {
}, },
]; ];
describe('RFC8032', () => {
for (let i = 0; i < VECTORS_RFC8032.length; i++) { for (let i = 0; i < VECTORS_RFC8032.length; i++) {
const v = VECTORS_RFC8032[i]; const v = VECTORS_RFC8032[i];
should(`RFC8032/${i}`, () => { should(`${i}`, () => {
deepStrictEqual(hex(ed.getPublicKey(v.secretKey)), v.publicKey); deepStrictEqual(hex(ed.getPublicKey(v.secretKey)), v.publicKey);
deepStrictEqual(hex(ed.sign(v.message, v.secretKey)), v.signature); deepStrictEqual(hex(ed.sign(v.message, v.secretKey)), v.signature);
deepStrictEqual(ed.verify(v.signature, v.message, v.publicKey), true); deepStrictEqual(ed.verify(v.signature, v.message, v.publicKey), true);
}); });
} }
});
should('not accept >57byte private keys', async () => { should('not accept >57byte private keys', () => {
const invalidPriv = const invalidPriv =
100000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800073278156000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000n; 100000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800073278156000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000n;
throws(() => ed.getPublicKey(invalidPriv)); throws(() => ed.getPublicKey(invalidPriv));
@@ -382,7 +385,24 @@ describe('ed448', () => {
deepStrictEqual(ed.verify(signature, msg, publicKey), true); deepStrictEqual(ed.verify(signature, msg, publicKey), true);
}); });
should('not verify signature with wrong public key', () => { should('not verify signature with wrong public key', () => {
const publicKey = ed.getPublicKey(12); const publicKey = ed.getPublicKey(ed.utils.randomPrivateKey());
const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, msg, publicKey), false);
});
should('not verify signature with wrong hash', () => {
const publicKey = ed.getPublicKey(privKey);
const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, wrongMsg, publicKey), false);
});
});
describe('sync methods', () => {
should('sign and verify', () => {
const publicKey = ed.getPublicKey(privKey);
const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, msg, publicKey), true);
});
should('not verify signature with wrong public key', () => {
const publicKey = ed.getPublicKey(ed.utils.randomPrivateKey());
const signature = ed.sign(msg, privKey); const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, msg, publicKey), false); deepStrictEqual(ed.verify(signature, msg, publicKey), false);
}); });
@@ -393,27 +413,9 @@ describe('ed448', () => {
}); });
}); });
describe('sync methods', () => {
should('sign and verify', () => {
const publicKey = ed.getPublicKey(privKey);
const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, msg, publicKey), true);
});
should('not verify signature with wrong public key', async () => {
const publicKey = ed.getPublicKey(12);
const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, msg, publicKey), false);
});
should('not verify signature with wrong hash', async () => {
const publicKey = ed.getPublicKey(privKey);
const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, wrongMsg, publicKey), false);
});
});
should('BASE_POINT.multiply() throws in Point#multiply on TEST 5', () => { should('BASE_POINT.multiply() throws in Point#multiply on TEST 5', () => {
for (const num of [0n, 0, -1n, -1, 1.1]) { for (const num of [0n, 0, -1n, -1, 1.1]) {
throws(() => ed.Point.BASE.multiply(num)); throws(() => ed.ExtendedPoint.BASE.multiply(num));
} }
}); });
@@ -437,14 +439,14 @@ describe('ed448', () => {
} }
}); });
{ describe('wycheproof', () => {
for (let g = 0; g < ed448vectors.testGroups.length; g++) { for (let g = 0; g < ed448vectors.testGroups.length; g++) {
const group = ed448vectors.testGroups[g]; const group = ed448vectors.testGroups[g];
const key = group.key; const key = group.key;
should(`Wycheproof/ED448(${g}, public)`, () => { should(`ED448(${g}, public)`, () => {
deepStrictEqual(hex(ed.getPublicKey(key.sk)), key.pk); deepStrictEqual(hex(ed.getPublicKey(key.sk)), key.pk);
}); });
should(`Wycheproof/ED448`, () => { should(`ED448`, () => {
for (let i = 0; i < group.tests.length; i++) { for (let i = 0; i < group.tests.length; i++) {
const v = group.tests[i]; const v = group.tests[i];
const index = `${g}/${i} ${v.comment}`; const index = `${g}/${i} ${v.comment}`;
@@ -463,7 +465,7 @@ describe('ed448', () => {
} }
}); });
} }
} });
// ECDH // ECDH
const rfc7748Mul = [ const rfc7748Mul = [
@@ -482,12 +484,14 @@ describe('ed448', () => {
'884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d', '884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d',
}, },
]; ];
describe('RFC7748', () => {
for (let i = 0; i < rfc7748Mul.length; i++) { for (let i = 0; i < rfc7748Mul.length; i++) {
const v = rfc7748Mul[i]; const v = rfc7748Mul[i];
should(`RFC7748: scalarMult (${i})`, () => { should(`scalarMult (${i})`, () => {
deepStrictEqual(hex(x448.scalarMult(v.scalar, v.u)), v.outputU); deepStrictEqual(hex(x448.scalarMult(v.scalar, v.u)), v.outputU);
}); });
} }
});
const rfc7748Iter = [ const rfc7748Iter = [
{ {
@@ -505,7 +509,7 @@ describe('ed448', () => {
for (let i = 0; i < rfc7748Iter.length; i++) { for (let i = 0; i < rfc7748Iter.length; i++) {
const { scalar, iters } = rfc7748Iter[i]; const { scalar, iters } = rfc7748Iter[i];
should(`RFC7748: scalarMult iteration (${i})`, () => { should(`RFC7748: scalarMult iteration (${i})`, () => {
let k = x448.Gu; let k = x448.GuBytes;
for (let i = 0, u = k; i < iters; i++) [k, u] = [x448.scalarMult(k, u), k]; for (let i = 0, u = k; i < iters; i++) [k, u] = [x448.scalarMult(k, u), k];
deepStrictEqual(hex(k), scalar); deepStrictEqual(hex(k), scalar);
}); });
@@ -528,9 +532,9 @@ describe('ed448', () => {
deepStrictEqual(hex(x448.scalarMult(bobPrivate, alicePublic)), shared); deepStrictEqual(hex(x448.scalarMult(bobPrivate, alicePublic)), shared);
}); });
{ describe('wycheproof', () => {
const group = x448vectors.testGroups[0]; const group = x448vectors.testGroups[0];
should(`Wycheproof/X448`, () => { should(`X448`, () => {
for (let i = 0; i < group.tests.length; i++) { for (let i = 0; i < group.tests.length; i++) {
const v = group.tests[i]; const v = group.tests[i];
const index = `(${i}, ${v.result}) ${v.comment}`; const index = `(${i}, ${v.result}) ${v.comment}`;
@@ -556,11 +560,11 @@ describe('ed448', () => {
} else throw new Error('unknown test result'); } else throw new Error('unknown test result');
} }
}); });
} });
// should('X448: should convert base point to montgomery using fromPoint', () => { // should('X448: should convert base point to montgomery using fromPoint', () => {
// deepStrictEqual( // deepStrictEqual(
// hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)), // hex(ed.montgomeryCurve.UfromPoint(Point.BASE)),
// ed.montgomeryCurve.BASE_POINT_U // ed.montgomeryCurve.BASE_POINT_U
// ); // );
// }); // });
@@ -655,12 +659,12 @@ describe('ed448', () => {
} }
should('X448 base point', () => { should('X448 base point', () => {
const { x, y } = ed448.Point.BASE; const { x, y } = Point.BASE;
const { Fp } = ed448.CURVE; const { Fp } = ed448.CURVE;
// const invX = Fp.invert(x * x); // x² // const invX = Fp.invert(x * x); // x²
const u = Fp.div(Fp.create(y * y), Fp.create(x * x)); // (y²/x²) const u = Fp.div(Fp.create(y * y), Fp.create(x * x)); // (y²/x²)
// const u = Fp.create(y * y * invX); // const u = Fp.create(y * y * invX);
deepStrictEqual(hex(numberToBytesLE(u, 56)), x448.Gu); deepStrictEqual(numberToBytesLE(u, 56), x448.GuBytes);
}); });
}); });

View File

@@ -5,18 +5,15 @@ import { bytesToHex } from '@noble/hashes/utils';
import { sha256 } from '@noble/hashes/sha256'; import { sha256 } from '@noble/hashes/sha256';
import { sha512 } from '@noble/hashes/sha512'; import { sha512 } from '@noble/hashes/sha512';
import { shake128, shake256 } from '@noble/hashes/sha3'; import { shake128, shake256 } from '@noble/hashes/sha3';
import { secp256r1 } from '../lib/esm/p256.js'; import * as secp256r1 from '../esm/p256.js';
import { secp384r1 } from '../lib/esm/p384.js'; import * as secp384r1 from '../esm/p384.js';
import { secp521r1 } from '../lib/esm/p521.js'; import * as secp521r1 from '../esm/p521.js';
import { ed25519 } from '../lib/esm/ed25519.js'; import * as ed25519 from '../esm/ed25519.js';
import { ed448 } from '../lib/esm/ed448.js'; import * as ed448 from '../esm/ed448.js';
import { secp256k1 } from '../lib/esm/secp256k1.js'; import * as secp256k1 from '../esm/secp256k1.js';
import { bls12_381 } from '../lib/esm/bls12-381.js'; import { bls12_381 } from '../esm/bls12-381.js';
import { import { expand_message_xmd, expand_message_xof } from '../esm/abstract/hash-to-curve.js';
stringToBytes, import { utf8ToBytes } from '../esm/abstract/utils.js';
expand_message_xmd,
expand_message_xof,
} from '../lib/esm/abstract/hash-to-curve.js';
// XMD // XMD
import { default as xmd_sha256_38 } from './hash-to-curve/expand_message_xmd_SHA256_38.json' assert { type: 'json' }; import { default as xmd_sha256_38 } from './hash-to-curve/expand_message_xmd_SHA256_38.json' assert { type: 'json' };
import { default as xmd_sha256_256 } from './hash-to-curve/expand_message_xmd_SHA256_256.json' assert { type: 'json' }; import { default as xmd_sha256_256 } from './hash-to-curve/expand_message_xmd_SHA256_256.json' assert { type: 'json' };
@@ -56,9 +53,9 @@ function testExpandXMD(hash, vectors) {
const t = vectors.tests[i]; const t = vectors.tests[i];
should(`${vectors.hash}/${vectors.DST.length}/${i}`, () => { should(`${vectors.hash}/${vectors.DST.length}/${i}`, () => {
const p = expand_message_xmd( const p = expand_message_xmd(
stringToBytes(t.msg), utf8ToBytes(t.msg),
stringToBytes(vectors.DST), utf8ToBytes(vectors.DST),
t.len_in_bytes, Number.parseInt(t.len_in_bytes),
hash hash
); );
deepStrictEqual(bytesToHex(p), t.uniform_bytes); deepStrictEqual(bytesToHex(p), t.uniform_bytes);
@@ -79,9 +76,9 @@ function testExpandXOF(hash, vectors) {
const t = vectors.tests[i]; const t = vectors.tests[i];
should(`${i}`, () => { should(`${i}`, () => {
const p = expand_message_xof( const p = expand_message_xof(
stringToBytes(t.msg), utf8ToBytes(t.msg),
stringToBytes(vectors.DST), utf8ToBytes(vectors.DST),
+t.len_in_bytes, Number.parseInt(t.len_in_bytes),
vectors.k, vectors.k,
hash hash
); );
@@ -111,9 +108,11 @@ function testCurve(curve, ro, nu) {
for (let i = 0; i < ro.vectors.length; i++) { for (let i = 0; i < ro.vectors.length; i++) {
const t = ro.vectors[i]; const t = ro.vectors[i];
should(`(${i})`, () => { should(`(${i})`, () => {
const p = curve.Point.hashToCurve(stringToBytes(t.msg), { const p = curve
.hashToCurve(utf8ToBytes(t.msg), {
DST: ro.dst, DST: ro.dst,
}); })
.toAffine();
deepStrictEqual(p.x, stringToFp(t.P.x), 'Px'); deepStrictEqual(p.x, stringToFp(t.P.x), 'Px');
deepStrictEqual(p.y, stringToFp(t.P.y), 'Py'); deepStrictEqual(p.y, stringToFp(t.P.y), 'Py');
}); });
@@ -123,9 +122,11 @@ function testCurve(curve, ro, nu) {
for (let i = 0; i < nu.vectors.length; i++) { for (let i = 0; i < nu.vectors.length; i++) {
const t = nu.vectors[i]; const t = nu.vectors[i];
should(`(${i})`, () => { should(`(${i})`, () => {
const p = curve.Point.encodeToCurve(stringToBytes(t.msg), { const p = curve
.encodeToCurve(utf8ToBytes(t.msg), {
DST: nu.dst, DST: nu.dst,
}); })
.toAffine();
deepStrictEqual(p.x, stringToFp(t.P.x), 'Px'); deepStrictEqual(p.x, stringToFp(t.P.x), 'Px');
deepStrictEqual(p.y, stringToFp(t.P.y), 'Py'); deepStrictEqual(p.y, stringToFp(t.P.y), 'Py');
}); });
@@ -136,7 +137,6 @@ function testCurve(curve, ro, nu) {
testCurve(secp256r1, p256_ro, p256_nu); testCurve(secp256r1, p256_ro, p256_nu);
testCurve(secp384r1, p384_ro, p384_nu); testCurve(secp384r1, p384_ro, p384_nu);
testCurve(secp521r1, p521_ro, p521_nu); testCurve(secp521r1, p521_ro, p521_nu);
// TODO: remove same tests from bls12
testCurve(bls12_381.G1, g1_ro, g1_nu); testCurve(bls12_381.G1, g1_ro, g1_nu);
testCurve(bls12_381.G2, g2_ro, g2_nu); testCurve(bls12_381.G2, g2_ro, g2_nu);
testCurve(secp256k1, secp256k1_ro, secp256k1_nu); testCurve(secp256k1, secp256k1_ro, secp256k1_nu);

View File

@@ -6,9 +6,10 @@ import './nist.test.js';
import './ed448.test.js'; import './ed448.test.js';
import './ed25519.test.js'; import './ed25519.test.js';
import './secp256k1.test.js'; import './secp256k1.test.js';
import './stark/stark.test.js'; import './secp256k1-schnorr.test.js';
import './jubjub.test.js'; import './jubjub.test.js';
import './bls12-381.test.js';
import './hash-to-curve.test.js'; import './hash-to-curve.test.js';
import './poseidon.test.js';
import './bls12-381.test.js';
should.run(); should.run();

View File

@@ -1,15 +1,15 @@
import { jubjub, findGroupHash } from '../lib/esm/jubjub.js'; import { jubjub, findGroupHash } from '../esm/jubjub.js';
import { describe, should } from 'micro-should'; import { describe, should } from 'micro-should';
import { deepStrictEqual, throws } from 'assert'; import { deepStrictEqual, throws } from 'assert';
import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; const Point = jubjub.ExtendedPoint;
const G_SPEND = new jubjub.ExtendedPoint( const G_SPEND = new Point(
0x055f1f24f0f0512287e51c3c5a0a6903fc0baf8711de9eafd7c0e66f69d8d2dbn, 0x055f1f24f0f0512287e51c3c5a0a6903fc0baf8711de9eafd7c0e66f69d8d2dbn,
0x566178b2505fdd52132a5007d80a04652842e78ffb376897588f406278214ed7n, 0x566178b2505fdd52132a5007d80a04652842e78ffb376897588f406278214ed7n,
0x0141fafa1f11088a3b2007c14d652375888f3b37838ba6bdffae096741ceddfen, 0x0141fafa1f11088a3b2007c14d652375888f3b37838ba6bdffae096741ceddfen,
0x12eada93c0b7d595f5f04f5ebfb4b7d033ef2884136475cab5e41ce17db5be9cn 0x12eada93c0b7d595f5f04f5ebfb4b7d033ef2884136475cab5e41ce17db5be9cn
); );
const G_PROOF = new jubjub.ExtendedPoint( const G_PROOF = new Point(
0x0174d54ce9fad258a2f8a86a1deabf15c7a2b51106b0fbcd9d29020f78936f71n, 0x0174d54ce9fad258a2f8a86a1deabf15c7a2b51106b0fbcd9d29020f78936f71n,
0x16871d6d877dcd222e4ec3bccb3f37cb1865a2d37dd3a5dcbc032a69b62b4445n, 0x16871d6d877dcd222e4ec3bccb3f37cb1865a2d37dd3a5dcbc032a69b62b4445n,
0x57a3cd31e496d82bd4aa78bd5ecd751cfb76d54a5d3f4560866379f9fc11c9b3n, 0x57a3cd31e496d82bd4aa78bd5ecd751cfb76d54a5d3f4560866379f9fc11c9b3n,
@@ -22,7 +22,7 @@ describe('jubjub', () => {
should('toHex/fromHex', () => { should('toHex/fromHex', () => {
// More than field // More than field
throws(() => throws(() =>
jubjub.Point.fromHex( Point.fromHex(
new Uint8Array([ new Uint8Array([
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
@@ -31,14 +31,14 @@ describe('jubjub', () => {
); );
// Multiplicative generator (sqrt == null), not on curve. // Multiplicative generator (sqrt == null), not on curve.
throws(() => throws(() =>
jubjub.Point.fromHex( Point.fromHex(
new Uint8Array([ new Uint8Array([
7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,
]) ])
) )
); );
const tmp = jubjub.Point.fromHex( const tmp = Point.fromHex(
new Uint8Array([ new Uint8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0,
@@ -47,14 +47,14 @@ describe('jubjub', () => {
deepStrictEqual(tmp.x, 0x8d51ccce760304d0ec030002760300000001000000000000n); deepStrictEqual(tmp.x, 0x8d51ccce760304d0ec030002760300000001000000000000n);
deepStrictEqual(tmp.y, 0n); deepStrictEqual(tmp.y, 0n);
const S = G_SPEND.toAffine().toRawBytes(); const S = G_SPEND.toRawBytes();
const S2 = G_SPEND.double().toAffine().toRawBytes(); const S2 = G_SPEND.double().toRawBytes();
const P = G_PROOF.toAffine().toRawBytes(); const P = G_PROOF.toRawBytes();
const P2 = G_PROOF.double().toAffine().toRawBytes(); const P2 = G_PROOF.double().toRawBytes();
const S_exp = jubjub.Point.fromHex(S); const S_exp = Point.fromHex(S);
const S2_exp = jubjub.Point.fromHex(S2); const S2_exp = Point.fromHex(S2);
const P_exp = jubjub.Point.fromHex(P); const P_exp = Point.fromHex(P);
const P2_exp = jubjub.Point.fromHex(P2); const P2_exp = Point.fromHex(P2);
deepStrictEqual(getXY(G_SPEND.toAffine()), getXY(S_exp)); deepStrictEqual(getXY(G_SPEND.toAffine()), getXY(S_exp));
deepStrictEqual(getXY(G_SPEND.double().toAffine()), getXY(S2_exp)); deepStrictEqual(getXY(G_SPEND.double().toAffine()), getXY(S2_exp));
deepStrictEqual(getXY(G_PROOF.toAffine()), getXY(P_exp)); deepStrictEqual(getXY(G_PROOF.toAffine()), getXY(P_exp));

View File

@@ -1,16 +1,50 @@
import { deepStrictEqual, throws } from 'assert'; import { deepStrictEqual } from 'assert';
import { describe, should } from 'micro-should'; import { describe, should } from 'micro-should';
import { secp192r1, P192 } from '../lib/esm/p192.js'; import { secp192r1, secp224r1, P192, P224 } from './_more-curves.helpers.js';
import { secp224r1, P224 } from '../lib/esm/p224.js'; import { secp256r1, P256 } from '../esm/p256.js';
import { secp256r1, P256 } from '../lib/esm/p256.js'; import { secp384r1, P384 } from '../esm/p384.js';
import { secp384r1, P384 } from '../lib/esm/p384.js'; import { secp521r1, P521 } from '../esm/p521.js';
import { secp521r1, P521 } from '../lib/esm/p521.js'; import { secp256k1 } from '../esm/secp256k1.js';
import { secp256k1 } from '../lib/esm/secp256k1.js'; import { hexToBytes, bytesToHex } from '../esm/abstract/utils.js';
import { hexToBytes, bytesToHex } from '../lib/esm/abstract/utils.js';
import { default as ecdsa } from './wycheproof/ecdsa_test.json' assert { type: 'json' }; import { default as ecdsa } from './wycheproof/ecdsa_test.json' assert { type: 'json' };
import { default as ecdh } from './wycheproof/ecdh_test.json' assert { type: 'json' }; import { default as ecdh } from './wycheproof/ecdh_test.json' assert { type: 'json' };
import { default as rfc6979 } from './fixtures/rfc6979.json' assert { type: 'json' }; import { default as rfc6979 } from './fixtures/rfc6979.json' assert { type: 'json' };
import { default as ecdh_secp224r1_test } from './wycheproof/ecdh_secp224r1_test.json' assert { type: 'json' };
import { default as ecdh_secp256r1_test } from './wycheproof/ecdh_secp256r1_test.json' assert { type: 'json' };
import { default as ecdh_secp256k1_test } from './wycheproof/ecdh_secp256k1_test.json' assert { type: 'json' };
import { default as ecdh_secp384r1_test } from './wycheproof/ecdh_secp384r1_test.json' assert { type: 'json' };
import { default as ecdh_secp521r1_test } from './wycheproof/ecdh_secp521r1_test.json' assert { type: 'json' };
// Tests with custom hashes
import { default as secp224r1_sha224_test } from './wycheproof/ecdsa_secp224r1_sha224_test.json' assert { type: 'json' };
import { default as secp224r1_sha256_test } from './wycheproof/ecdsa_secp224r1_sha256_test.json' assert { type: 'json' };
import { default as secp224r1_sha3_224_test } from './wycheproof/ecdsa_secp224r1_sha3_224_test.json' assert { type: 'json' };
import { default as secp224r1_sha3_256_test } from './wycheproof/ecdsa_secp224r1_sha3_256_test.json' assert { type: 'json' };
import { default as secp224r1_sha3_512_test } from './wycheproof/ecdsa_secp224r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp224r1_sha512_test } from './wycheproof/ecdsa_secp224r1_sha512_test.json' assert { type: 'json' };
import { default as secp256k1_sha256_test } from './wycheproof/ecdsa_secp256k1_sha256_test.json' assert { type: 'json' };
import { default as secp256k1_sha3_256_test } from './wycheproof/ecdsa_secp256k1_sha3_256_test.json' assert { type: 'json' };
import { default as secp256k1_sha3_512_test } from './wycheproof/ecdsa_secp256k1_sha3_512_test.json' assert { type: 'json' };
import { default as secp256k1_sha512_test } from './wycheproof/ecdsa_secp256k1_sha512_test.json' assert { type: 'json' };
import { default as secp256r1_sha256_test } from './wycheproof/ecdsa_secp256r1_sha256_test.json' assert { type: 'json' };
import { default as secp256r1_sha3_256_test } from './wycheproof/ecdsa_secp256r1_sha3_256_test.json' assert { type: 'json' };
import { default as secp256r1_sha3_512_test } from './wycheproof/ecdsa_secp256r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp256r1_sha512_test } from './wycheproof/ecdsa_secp256r1_sha512_test.json' assert { type: 'json' };
import { default as secp384r1_sha384_test } from './wycheproof/ecdsa_secp384r1_sha384_test.json' assert { type: 'json' };
import { default as secp384r1_sha3_384_test } from './wycheproof/ecdsa_secp384r1_sha3_384_test.json' assert { type: 'json' };
import { default as secp384r1_sha3_512_test } from './wycheproof/ecdsa_secp384r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp384r1_sha512_test } from './wycheproof/ecdsa_secp384r1_sha512_test.json' assert { type: 'json' };
import { default as secp521r1_sha3_512_test } from './wycheproof/ecdsa_secp521r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp521r1_sha512_test } from './wycheproof/ecdsa_secp521r1_sha512_test.json' assert { type: 'json' };
import { sha3_224, sha3_256, sha3_384, sha3_512 } from '@noble/hashes/sha3';
import { sha512, sha384 } from '@noble/hashes/sha512';
import { sha224, sha256 } from '@noble/hashes/sha256';
const hex = bytesToHex; const hex = bytesToHex;
// prettier-ignore // prettier-ignore
@@ -23,7 +57,8 @@ const NIST = {
secp256k1, secp256k1,
}; };
should('Curve Fields', () => { describe('NIST curves', () => {});
should('fields', () => {
const vectors = { const vectors = {
secp192r1: 0xfffffffffffffffffffffffffffffffeffffffffffffffffn, secp192r1: 0xfffffffffffffffffffffffffffffffeffffffffffffffffn,
secp224r1: 0xffffffffffffffffffffffffffffffff000000000000000000000001n, secp224r1: 0xffffffffffffffffffffffffffffffff000000000000000000000001n,
@@ -37,59 +72,21 @@ should('Curve Fields', () => {
for (const n in vectors) deepStrictEqual(NIST[n].CURVE.Fp.ORDER, vectors[n]); for (const n in vectors) deepStrictEqual(NIST[n].CURVE.Fp.ORDER, vectors[n]);
}); });
should('wychenproof ECDSA vectors', () => { describe('wycheproof ECDH', () => {
for (const group of ecdsa.testGroups) {
// Tested in secp256k1.test.js
if (group.key.curve === 'secp256k1') continue;
let CURVE = NIST[group.key.curve];
if (!CURVE) continue;
if (group.key.curve === 'secp224r1' && group.sha !== 'SHA-224') {
if (group.sha === 'SHA-256') CURVE = CURVE.create(sha256);
}
const pubKey = CURVE.Point.fromHex(group.key.uncompressed);
deepStrictEqual(pubKey.x, BigInt(`0x${group.key.wx}`));
deepStrictEqual(pubKey.y, BigInt(`0x${group.key.wy}`));
for (const test of group.tests) {
if (['Hash weaker than DL-group'].includes(test.comment)) {
continue;
}
const m = CURVE.CURVE.hash(hexToBytes(test.msg));
if (test.result === 'valid' || test.result === 'acceptable') {
try {
CURVE.Signature.fromDER(test.sig);
} catch (e) {
// Some test has invalid signature which we don't accept
if (e.message.includes('Invalid signature: incorrect length')) continue;
throw e;
}
const verified = CURVE.verify(test.sig, m, pubKey);
deepStrictEqual(verified, true, 'valid');
} else if (test.result === 'invalid') {
let failed = false;
try {
failed = !CURVE.verify(test.sig, m, pubKey);
} catch (error) {
failed = true;
}
deepStrictEqual(failed, true, 'invalid');
} else throw new Error('unknown test result');
}
}
});
should('wychenproof ECDH vectors', () => {
for (const group of ecdh.testGroups) { for (const group of ecdh.testGroups) {
// // Tested in secp256k1.test.js // // Tested in secp256k1.test.js
// if (group.key.curve === 'secp256k1') continue; // if (group.key.curve === 'secp256k1') continue;
// We don't have SHA-224 // We don't have SHA-224
const CURVE = NIST[group.curve]; const CURVE = NIST[group.curve];
if (!CURVE) continue; if (!CURVE) continue;
should(group.curve, () => {
for (const test of group.tests) { for (const test of group.tests) {
if (test.result === 'valid' || test.result === 'acceptable') { if (test.result === 'valid' || test.result === 'acceptable') {
try { try {
const pub = CURVE.Point.fromHex(test.public); const pub = CURVE.ProjectivePoint.fromHex(test.public);
} catch (e) { } catch (e) {
if (e.message.includes('Point.fromHex: received invalid point.')) continue; // Our strict validation filter doesn't let weird-length DER vectors
if (e.message.startsWith('Point of length')) continue;
throw e; throw e;
} }
const shared = CURVE.getSharedSecret(test.private, test.public); const shared = CURVE.getSharedSecret(test.private, test.public);
@@ -104,14 +101,8 @@ should('wychenproof ECDH vectors', () => {
deepStrictEqual(failed, true, 'invalid'); deepStrictEqual(failed, true, 'invalid');
} else throw new Error('unknown test result'); } else throw new Error('unknown test result');
} }
}
}); });
}
import { default as ecdh_secp224r1_test } from './wycheproof/ecdh_secp224r1_test.json' assert { type: 'json' };
import { default as ecdh_secp256r1_test } from './wycheproof/ecdh_secp256r1_test.json' assert { type: 'json' };
import { default as ecdh_secp256k1_test } from './wycheproof/ecdh_secp256k1_test.json' assert { type: 'json' };
import { default as ecdh_secp384r1_test } from './wycheproof/ecdh_secp384r1_test.json' assert { type: 'json' };
import { default as ecdh_secp521r1_test } from './wycheproof/ecdh_secp521r1_test.json' assert { type: 'json' };
// More per curve tests // More per curve tests
const WYCHEPROOF_ECDH = { const WYCHEPROOF_ECDH = {
@@ -143,13 +134,14 @@ for (const name in WYCHEPROOF_ECDH) {
const test = tests[i]; const test = tests[i];
for (let j = 0; j < test.testGroups.length; j++) { for (let j = 0; j < test.testGroups.length; j++) {
const group = test.testGroups[j]; const group = test.testGroups[j];
should(`Wycheproof/ECDH ${name} (${i}/${j})`, () => { should(`additional ${name} (${i}/${j})`, () => {
for (const test of group.tests) { for (const test of group.tests) {
if (test.result === 'valid' || test.result === 'acceptable') { if (test.result === 'valid' || test.result === 'acceptable') {
try { try {
const pub = curve.Point.fromHex(test.public); const pub = curve.ProjectivePoint.fromHex(test.public);
} catch (e) { } catch (e) {
if (e.message.includes('Point.fromHex: received invalid point.')) continue; // Our strict validation filter doesn't let weird-length DER vectors
if (e.message.includes('Point of length')) continue;
throw e; throw e;
} }
const shared = curve.getSharedSecret(test.private, test.public); const shared = curve.getSharedSecret(test.private, test.public);
@@ -168,36 +160,7 @@ for (const name in WYCHEPROOF_ECDH) {
} }
} }
} }
});
// Tests with custom hashes
import { default as secp224r1_sha224_test } from './wycheproof/ecdsa_secp224r1_sha224_test.json' assert { type: 'json' };
import { default as secp224r1_sha256_test } from './wycheproof/ecdsa_secp224r1_sha256_test.json' assert { type: 'json' };
import { default as secp224r1_sha3_224_test } from './wycheproof/ecdsa_secp224r1_sha3_224_test.json' assert { type: 'json' };
import { default as secp224r1_sha3_256_test } from './wycheproof/ecdsa_secp224r1_sha3_256_test.json' assert { type: 'json' };
import { default as secp224r1_sha3_512_test } from './wycheproof/ecdsa_secp224r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp224r1_sha512_test } from './wycheproof/ecdsa_secp224r1_sha512_test.json' assert { type: 'json' };
import { default as secp256k1_sha256_test } from './wycheproof/ecdsa_secp256k1_sha256_test.json' assert { type: 'json' };
import { default as secp256k1_sha3_256_test } from './wycheproof/ecdsa_secp256k1_sha3_256_test.json' assert { type: 'json' };
import { default as secp256k1_sha3_512_test } from './wycheproof/ecdsa_secp256k1_sha3_512_test.json' assert { type: 'json' };
import { default as secp256k1_sha512_test } from './wycheproof/ecdsa_secp256k1_sha512_test.json' assert { type: 'json' };
import { default as secp256r1_sha256_test } from './wycheproof/ecdsa_secp256r1_sha256_test.json' assert { type: 'json' };
import { default as secp256r1_sha3_256_test } from './wycheproof/ecdsa_secp256r1_sha3_256_test.json' assert { type: 'json' };
import { default as secp256r1_sha3_512_test } from './wycheproof/ecdsa_secp256r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp256r1_sha512_test } from './wycheproof/ecdsa_secp256r1_sha512_test.json' assert { type: 'json' };
import { default as secp384r1_sha384_test } from './wycheproof/ecdsa_secp384r1_sha384_test.json' assert { type: 'json' };
import { default as secp384r1_sha3_384_test } from './wycheproof/ecdsa_secp384r1_sha3_384_test.json' assert { type: 'json' };
import { default as secp384r1_sha3_512_test } from './wycheproof/ecdsa_secp384r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp384r1_sha512_test } from './wycheproof/ecdsa_secp384r1_sha512_test.json' assert { type: 'json' };
import { default as secp521r1_sha3_512_test } from './wycheproof/ecdsa_secp521r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp521r1_sha512_test } from './wycheproof/ecdsa_secp521r1_sha512_test.json' assert { type: 'json' };
import { sha3_224, sha3_256, sha3_384, sha3_512 } from '@noble/hashes/sha3';
import { sha512, sha384 } from '@noble/hashes/sha512';
import { sha224, sha256 } from '@noble/hashes/sha256';
const WYCHEPROOF_ECDSA = { const WYCHEPROOF_ECDSA = {
P224: { P224: {
@@ -232,7 +195,6 @@ const WYCHEPROOF_ECDSA = {
secp256k1: { secp256k1: {
curve: secp256k1, curve: secp256k1,
hashes: { hashes: {
// TODO: debug why fails, can be bug
sha256: { sha256: {
hash: sha256, hash: sha256,
tests: [secp256k1_sha256_test], tests: [secp256k1_sha256_test],
@@ -309,31 +271,33 @@ const WYCHEPROOF_ECDSA = {
}; };
function runWycheproof(name, CURVE, group, index) { function runWycheproof(name, CURVE, group, index) {
const pubKey = CURVE.Point.fromHex(group.key.uncompressed); const pubKey = CURVE.ProjectivePoint.fromHex(group.key.uncompressed);
deepStrictEqual(pubKey.x, BigInt(`0x${group.key.wx}`)); deepStrictEqual(pubKey.x, BigInt(`0x${group.key.wx}`));
deepStrictEqual(pubKey.y, BigInt(`0x${group.key.wy}`)); deepStrictEqual(pubKey.y, BigInt(`0x${group.key.wy}`));
const pubR = pubKey.toRawBytes();
for (const test of group.tests) { for (const test of group.tests) {
const m = CURVE.CURVE.hash(hexToBytes(test.msg)); const m = CURVE.CURVE.hash(hexToBytes(test.msg));
const { sig } = test;
if (test.result === 'valid' || test.result === 'acceptable') { if (test.result === 'valid' || test.result === 'acceptable') {
try { try {
CURVE.Signature.fromDER(test.sig); CURVE.Signature.fromDER(sig);
} catch (e) { } catch (e) {
// Some tests has invalid signature which we don't accept // Some tests has invalid signature which we don't accept
if (e.message.includes('Invalid signature: incorrect length')) continue; if (e.message.includes('Invalid signature: incorrect length')) continue;
throw e; throw e;
} }
const verified = CURVE.verify(test.sig, m, pubKey); const verified = CURVE.verify(sig, m, pubR);
if (name === 'secp256k1') { if (name === 'secp256k1') {
// lowS: true for secp256k1 // lowS: true for secp256k1
deepStrictEqual(verified, !CURVE.Signature.fromDER(test.sig).hasHighS(), `${index}: valid`); deepStrictEqual(verified, !CURVE.Signature.fromDER(sig).hasHighS(), `${index}: valid`);
} else { } else {
deepStrictEqual(verified, true, `${index}: valid`); deepStrictEqual(verified, true, `${index}: valid`);
} }
} else if (test.result === 'invalid') { } else if (test.result === 'invalid') {
let failed = false; let failed = false;
try { try {
failed = !CURVE.verify(test.sig, m, pubKey); failed = !CURVE.verify(sig, m, pubR);
} catch (error) { } catch (error) {
failed = true; failed = true;
} }
@@ -342,9 +306,49 @@ function runWycheproof(name, CURVE, group, index) {
} }
} }
describe('wycheproof ECDSA', () => {
should('generic', () => {
for (const group of ecdsa.testGroups) {
// Tested in secp256k1.test.js
if (group.key.curve === 'secp256k1') continue;
let CURVE = NIST[group.key.curve];
if (!CURVE) continue;
if (group.key.curve === 'secp224r1' && group.sha !== 'SHA-224') {
if (group.sha === 'SHA-256') CURVE = CURVE.create(sha256);
}
const pubKey = CURVE.ProjectivePoint.fromHex(group.key.uncompressed);
deepStrictEqual(pubKey.x, BigInt(`0x${group.key.wx}`));
deepStrictEqual(pubKey.y, BigInt(`0x${group.key.wy}`));
for (const test of group.tests) {
if (['Hash weaker than DL-group'].includes(test.comment)) {
continue;
}
const m = CURVE.CURVE.hash(hexToBytes(test.msg));
if (test.result === 'valid' || test.result === 'acceptable') {
try {
CURVE.Signature.fromDER(test.sig);
} catch (e) {
// Some test has invalid signature which we don't accept
if (e.message.includes('Invalid signature: incorrect length')) continue;
throw e;
}
const verified = CURVE.verify(test.sig, m, pubKey.toHex());
deepStrictEqual(verified, true, 'valid');
} else if (test.result === 'invalid') {
let failed = false;
try {
failed = !CURVE.verify(test.sig, m, pubKey.toHex());
} catch (error) {
failed = true;
}
deepStrictEqual(failed, true, 'invalid');
} else throw new Error('unknown test result');
}
}
});
for (const name in WYCHEPROOF_ECDSA) { for (const name in WYCHEPROOF_ECDSA) {
const { curve, hashes } = WYCHEPROOF_ECDSA[name]; const { curve, hashes } = WYCHEPROOF_ECDSA[name];
describe('Wycheproof/WYCHEPROOF_ECDSA', () => { describe(name, () => {
for (const hName in hashes) { for (const hName in hashes) {
const { hash, tests } = hashes[hName]; const { hash, tests } = hashes[hName];
const CURVE = curve.create(hash); const CURVE = curve.create(hash);
@@ -360,14 +364,16 @@ for (const name in WYCHEPROOF_ECDSA) {
} }
}); });
} }
});
const hexToBigint = (hex) => BigInt(`0x${hex}`); const hexToBigint = (hex) => BigInt(`0x${hex}`);
should('RFC6979', () => { describe('RFC6979', () => {
for (const v of rfc6979) { for (const v of rfc6979) {
should(v.curve, () => {
const curve = NIST[v.curve]; const curve = NIST[v.curve];
deepStrictEqual(curve.CURVE.n, hexToBigint(v.q)); deepStrictEqual(curve.CURVE.n, hexToBigint(v.q));
const pubKey = curve.getPublicKey(v.private); const pubKey = curve.getPublicKey(v.private);
const pubPoint = curve.Point.fromHex(pubKey); const pubPoint = curve.ProjectivePoint.fromHex(pubKey);
deepStrictEqual(pubPoint.x, hexToBigint(v.Ux)); deepStrictEqual(pubPoint.x, hexToBigint(v.Ux));
deepStrictEqual(pubPoint.y, hexToBigint(v.Uy)); deepStrictEqual(pubPoint.y, hexToBigint(v.Uy));
for (const c of v.cases) { for (const c of v.cases) {
@@ -378,6 +384,7 @@ should('RFC6979', () => {
deepStrictEqual(curve.verify(sigObj.toDERRawBytes(), h, pubKey), true, 'verify(1)'); deepStrictEqual(curve.verify(sigObj.toDERRawBytes(), h, pubKey), true, 'verify(1)');
deepStrictEqual(curve.verify(sigObj, h, pubKey), true, 'verify(2)'); deepStrictEqual(curve.verify(sigObj, h, pubKey), true, 'verify(2)');
} }
});
} }
}); });

377
test/poseidon.test.js Normal file
View File

@@ -0,0 +1,377 @@
import { deepStrictEqual, throws } from 'assert';
import { should, describe } from 'micro-should';
import * as poseidon from '../esm/abstract/poseidon.js';
import * as stark from './_poseidon.helpers.js';
import * as mod from '../esm/abstract/modular.js';
import { default as pvectors } from './vectors/poseidon.json' assert { type: 'json' };
const { st1, st2, st3, st4 } = pvectors;
describe('Stark', () => {
should('poseidonMdsMatrixUnsafe', () => {
const matrix = [
[
2778560475384578201077246683568670693743746494974613838537993780462451025202n,
1175299404131241652930097281601393692628174430208909163156444576599667748918n,
459930634481240293374476654621049426021644833445120509139335338093973616187n,
],
[
2699370377471722242958186781613316939129713429759631049128040020458992590651n,
1488831960940040807419416081499284128899207850625157044437836107358246188803n,
3405112981980800875534081635548548562399171531483475155039499736396630179833n,
],
[
1860070716810022053527433635909648527418980081585070357136946388030401399342n,
2606527819893847364468965441606872534271438365089422719512470850627617054272n,
2715867691630559973784374069384091521307896505826088878858115800121387149186n,
],
];
deepStrictEqual(stark._poseidonMDS(stark.Fp251, 'HadesMDS', 3, 0), matrix);
});
should('HadesPermutation', () => {
deepStrictEqual(
stark.poseidonSmall([
4379311784651118086770398084575492314150568148003994287303975907890254409956n,
5329163686893598957822497554130545759427567507701132391649270915797304266381n,
1081797873147645298856697595691862435558345225505029083672323747888463248125n,
]),
[
1342232677189718451682683203787286758407058155581807117466384919996430343159n,
380853961496438693334706417244065195303131974442781224856980145160981376662n,
1919212703304954644851339421413808305076993030243665926017858381407659820613n,
]
);
});
should('HadesPermutation (custom)', () => {
const h = stark.poseidonCreate({
Fp: stark.Fp251,
rate: 2,
capacity: 1,
roundsFull: 8,
roundsPartial: 83,
});
deepStrictEqual(
h([
4379311784651118086770398084575492314150568148003994287303975907890254409956n,
5329163686893598957822497554130545759427567507701132391649270915797304266381n,
1081797873147645298856697595691862435558345225505029083672323747888463248125n,
]),
[
2864461397224564530993577865807718592436235694918699912757414692654057505365n,
1576206983934669422583425346343473837630736957734769961428118554039862202613n,
1607006208879950753054674913136990521997740361932184292107790666308092455675n,
]
);
});
should('HadesPermutation (custom, Fp253)', () => {
const h = stark.poseidonCreate({
Fp: stark.Fp253,
rate: 2,
capacity: 1,
roundsFull: 8,
roundsPartial: 83,
});
deepStrictEqual(
h([
4379311784651118086770398084575492314150568148003994287303975907890254409956n,
5329163686893598957822497554130545759427567507701132391649270915797304266381n,
1081797873147645298856697595691862435558345225505029083672323747888463248125n,
]),
[
11142411210283675631592374649001218595612035205233832049083369488791454026844n,
98304838055259883374145304326851527594402230455144399354815642835291000581n,
8643534790068701259242695637167859384191499281344739826454631748110172472997n,
]
);
});
should('PoseidonHash', () => {
deepStrictEqual(
stark.poseidonHash(
4379311784651118086770398084575492314150568148003994287303975907890254409956n,
5329163686893598957822497554130545759427567507701132391649270915797304266381n
),
2457757238178986673695038558497063891521456354791980183317105434323761563347n
);
});
should('PoseidonHash (custom)', () => {
const h = stark.poseidonCreate({
Fp: stark.Fp251,
rate: 2,
capacity: 1,
roundsFull: 8,
roundsPartial: 83,
});
deepStrictEqual(
stark.poseidonHash(
4379311784651118086770398084575492314150568148003994287303975907890254409956n,
5329163686893598957822497554130545759427567507701132391649270915797304266381n,
h
),
654164301216498483748450956182386165976155551413834652546305861430119544536n
);
});
should('PoseidonHash (custom, Fp253)', () => {
const h = stark.poseidonCreate({
Fp: stark.Fp253,
rate: 2,
capacity: 1,
roundsFull: 8,
roundsPartial: 83,
});
deepStrictEqual(
stark.poseidonHash(
4379311784651118086770398084575492314150568148003994287303975907890254409956n,
5329163686893598957822497554130545759427567507701132391649270915797304266381n,
h
),
9557424461253897982213839283192966960594440725760392861778010931094267239786n
);
});
});
// Official vectors: https://extgit.iaik.tugraz.at/krypto/hadeshash/-/blob/master/code/test_vectors.txt
should('poseidonperm_x5_255_3', () => {
const Fp = mod.Field(
BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001')
);
const mds = [
[
0x3d955d6c02fe4d7cb500e12f2b55eff668a7b4386bd27413766713c93f2acfcdn,
0x3798866f4e6058035dcf8addb2cf1771fac234bcc8fc05d6676e77e797f224bfn,
0x2c51456a7bf2467eac813649f3f25ea896eac27c5da020dae54a6e640278fda2n,
],
[
0x20088ca07bbcd7490a0218ebc0ecb31d0ea34840e2dc2d33a1a5adfecff83b43n,
0x1d04ba0915e7807c968ea4b1cb2d610c7f9a16b4033f02ebacbb948c86a988c3n,
0x5387ccd5729d7acbd09d96714d1d18bbd0eeaefb2ddee3d2ef573c9c7f953307n,
],
[
0x1e208f585a72558534281562cad89659b428ec61433293a8d7f0f0e38a6726acn,
0x0455ebf862f0b60f69698e97d36e8aafd4d107cae2b61be1858b23a3363642e0n,
0x569e2c206119e89455852059f707370e2c1fc9721f6c50991cedbbf782daef54n,
],
];
const t = 3;
const roundConstants = poseidon.splitConstants(st1.map(BigInt), t);
const poseidon_x5_255_3 = poseidon.poseidon({
Fp,
t,
roundsFull: 8,
roundsPartial: 57,
mds,
roundConstants,
});
deepStrictEqual(
poseidon_x5_255_3([
0x0000000000000000000000000000000000000000000000000000000000000000n,
0x0000000000000000000000000000000000000000000000000000000000000001n,
0x0000000000000000000000000000000000000000000000000000000000000002n,
]),
[
0x28ce19420fc246a05553ad1e8c98f5c9d67166be2c18e9e4cb4b4e317dd2a78an,
0x51f3e312c95343a896cfd8945ea82ba956c1118ce9b9859b6ea56637b4b1ddc4n,
0x3b2b69139b235626a0bfb56c9527ae66a7bf486ad8c11c14d1da0c69bbe0f79an,
]
);
});
should('poseidonperm_x5_255_5', () => {
const Fp = mod.Field(0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001n);
const t = 5;
const mds = [
[
0x354423b163d1078b0dd645be56316e34a9b98e52dcf9f469be44b108be46c107n,
0x44778737e8bc1154aca1cd92054a1e5b83808403705f7d54da88bbd1920e1053n,
0x5872eefb5ab6b2946556524168a2aebb69afd513a2fff91e50167b1f6e4055e0n,
0x43dff85b25129835819bc8c95819f1a34136f6114e900cd3656e1b9e0e13f86an,
0x07803d2ffe72940596803f244ac090a9cf2d3616546520bc360c7eed0b81cbf8n,
],
[
0x45d6bc4b818e2b9a53e0e2c0a08f70c34167fd8128e05ac800651ddfee0932d1n,
0x08317abbb9e5046b22dfb79e64c8184855107c1d95dddd2b63ca10dddea9ff1an,
0x1bb80eba77c5dcffafb55ccba4ae39ac8f94a054f2a0ee3006b362f709d5e470n,
0x038e75bdcf8be7fd3a1e844c4de7333531bbd5a8d2c3779627df88e7480e7c5cn,
0x2dd797a699e620ea6b31b91ba3fad4a82f40cffb3e8a30c0b7a546ff69a9002bn,
],
[
0x4b906f9ee339b196e958e3541b555b4b53e540a113b2f1cabba627be16eb5608n,
0x605f0c707b82ef287f46431f9241fe4acf0b7ddb151803cbcf1e7bbd27c3e974n,
0x100c514bf38f6ff10df1c83bb428397789cfff7bb0b1280f52343861e8c8737en,
0x2d40ce8af8a252f5611701c3d6b1e517161d0549ef27f443570c81fcdfe3706bn,
0x3e6418bdf0313f59afc5f40b4450e56881110ea9a0532e8092efb06a12a8b0f1n,
],
[
0x71788bf7f6c0cebae5627c5629d012d5fba52428d1f25cdaa0a7434e70e014d0n,
0x55cc73296f7e7d26d10b9339721d7983ca06145675255025ab00b34342557db7n,
0x0f043b29be2def73a6c6ec92168ea4b47bc9f434a5e6b5d48677670a7ca4d285n,
0x62ccc9cdfed859a610f103d74ea04dec0f6874a9b36f3b4e9b47fd73368d45b4n,
0x55fb349dd6200b34eaba53a67e74f47d08e473da139dc47e44df50a26423d2d1n,
],
[
0x45bfbe5ed2f4a01c13b15f20bba00ff577b1154a81b3f318a6aff86369a66735n,
0x6a008906685587af05dce9ad2c65ea1d42b1ec32609597bd00c01f58443329efn,
0x004feebd0dbdb9b71176a1d43c9eb495e16419382cdf7864e4bce7b37440cd58n,
0x09f080180ce23a5aef3a07e60b28ffeb2cf1771aefbc565c2a3059b39ed82f43n,
0x2f7126ddc54648ab6d02493dbe9907f29f4ef3967ad8cd609f0d9467e1694607n,
],
];
const roundConstants = poseidon.splitConstants(st2.map(BigInt), t);
const poseidon_x5_255_5 = poseidon.poseidon({
Fp,
t,
roundsFull: 8,
roundsPartial: 60,
mds,
roundConstants,
});
deepStrictEqual(
poseidon_x5_255_5([
0x0000000000000000000000000000000000000000000000000000000000000000n,
0x0000000000000000000000000000000000000000000000000000000000000001n,
0x0000000000000000000000000000000000000000000000000000000000000002n,
0x0000000000000000000000000000000000000000000000000000000000000003n,
0x0000000000000000000000000000000000000000000000000000000000000004n,
]),
[
0x2a918b9c9f9bd7bb509331c81e297b5707f6fc7393dcee1b13901a0b22202e18n,
0x65ebf8671739eeb11fb217f2d5c5bf4a0c3f210e3f3cd3b08b5db75675d797f7n,
0x2cc176fc26bc70737a696a9dfd1b636ce360ee76926d182390cdb7459cf585cen,
0x4dc4e29d283afd2a491fe6aef122b9a968e74eff05341f3cc23fda1781dcb566n,
0x03ff622da276830b9451b88b85e6184fd6ae15c8ab3ee25a5667be8592cce3b1n,
]
);
});
should('poseidonperm_x5_254_3', () => {
const Fp = mod.Field(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001n);
const t = 3;
const mds = [
[
0x109b7f411ba0e4c9b2b70caf5c36a7b194be7c11ad24378bfedb68592ba8118bn,
0x16ed41e13bb9c0c66ae119424fddbcbc9314dc9fdbdeea55d6c64543dc4903e0n,
0x2b90bba00fca0589f617e7dcbfe82e0df706ab640ceb247b791a93b74e36736dn,
],
[
0x2969f27eed31a480b9c36c764379dbca2cc8fdd1415c3dded62940bcde0bd771n,
0x2e2419f9ec02ec394c9871c832963dc1b89d743c8c7b964029b2311687b1fe23n,
0x101071f0032379b697315876690f053d148d4e109f5fb065c8aacc55a0f89bfan,
],
[
0x143021ec686a3f330d5f9e654638065ce6cd79e28c5b3753326244ee65a1b1a7n,
0x176cc029695ad02582a70eff08a6fd99d057e12e58e7d7b6b16cdfabc8ee2911n,
0x19a3fc0a56702bf417ba7fee3802593fa644470307043f7773279cd71d25d5e0n,
],
];
const roundConstants = poseidon.splitConstants(st3.map(BigInt), t);
const poseidon_x5_254_3 = poseidon.poseidon({
Fp,
t,
roundsFull: 8,
roundsPartial: 57,
mds,
roundConstants,
});
deepStrictEqual(
poseidon_x5_254_3([
0x0000000000000000000000000000000000000000000000000000000000000000n,
0x0000000000000000000000000000000000000000000000000000000000000001n,
0x0000000000000000000000000000000000000000000000000000000000000002n,
]),
[
0x115cc0f5e7d690413df64c6b9662e9cf2a3617f2743245519e19607a4417189an,
0x0fca49b798923ab0239de1c9e7a4a9a2210312b6a2f616d18b5a87f9b628ae29n,
0x0e7ae82e40091e63cbd4f16a6d16310b3729d4b6e138fcf54110e2867045a30cn,
]
);
});
should('poseidonperm_x5_254_5', () => {
const Fp = mod.Field(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001n);
const t = 5;
const mds = [
[
0x251e7fdf99591080080b0af133b9e4369f22e57ace3cd7f64fc6fdbcf38d7da1n,
0x25fb50b65acf4fb047cbd3b1c17d97c7fe26ea9ca238d6e348550486e91c7765n,
0x293d617d7da72102355f39ebf62f91b06deb5325f367a4556ea1e31ed5767833n,
0x104d0295ab00c85e960111ac25da474366599e575a9b7edf6145f14ba6d3c1c4n,
0x0aaa35e2c84baf117dea3e336cd96a39792b3813954fe9bf3ed5b90f2f69c977n,
],
[
0x2a70b9f1d4bbccdbc03e17c1d1dcdb02052903dc6609ea6969f661b2eb74c839n,
0x281154651c921e746315a9934f1b8a1bba9f92ad8ef4b979115b8e2e991ccd7an,
0x28c2be2f8264f95f0b53c732134efa338ccd8fdb9ee2b45fb86a894f7db36c37n,
0x21888041e6febd546d427c890b1883bb9b626d8cb4dc18dcc4ec8fa75e530a13n,
0x14ddb5fada0171db80195b9592d8cf2be810930e3ea4574a350d65e2cbff4941n,
],
[
0x2f69a7198e1fbcc7dea43265306a37ed55b91bff652ad69aa4fa8478970d401dn,
0x001c1edd62645b73ad931ab80e37bbb267ba312b34140e716d6a3747594d3052n,
0x15b98ce93e47bc64ce2f2c96c69663c439c40c603049466fa7f9a4b228bfc32bn,
0x12c7e2adfa524e5958f65be2fbac809fcba8458b28e44d9265051de33163cf9cn,
0x2efc2b90d688134849018222e7b8922eaf67ce79816ef468531ec2de53bbd167n,
],
[
0x0c3f050a6bf5af151981e55e3e1a29a13c3ffa4550bd2514f1afd6c5f721f830n,
0x0dec54e6dbf75205fa75ba7992bd34f08b2efe2ecd424a73eda7784320a1a36en,
0x1c482a25a729f5df20225815034b196098364a11f4d988fb7cc75cf32d8136fan,
0x2625ce48a7b39a4252732624e4ab94360812ac2fc9a14a5fb8b607ae9fd8514an,
0x07f017a7ebd56dd086f7cd4fd710c509ed7ef8e300b9a8bb9fb9f28af710251fn,
],
[
0x2a20e3a4a0e57d92f97c9d6186c6c3ea7c5e55c20146259be2f78c2ccc2e3595n,
0x1049f8210566b51faafb1e9a5d63c0ee701673aed820d9c4403b01feb727a549n,
0x02ecac687ef5b4b568002bd9d1b96b4bef357a69e3e86b5561b9299b82d69c8en,
0x2d3a1aea2e6d44466808f88c9ba903d3bdcb6b58ba40441ed4ebcf11bbe1e37bn,
0x14074bb14c982c81c9ad171e4f35fe49b39c4a7a72dbb6d9c98d803bfed65e64n,
],
];
const roundConstants = poseidon.splitConstants(st4.map(BigInt), t);
const poseidon_x5_254_5 = poseidon.poseidon({
Fp,
t,
roundsFull: 8,
roundsPartial: 60,
mds,
roundConstants,
});
deepStrictEqual(
poseidon_x5_254_5([
0x0000000000000000000000000000000000000000000000000000000000000000n,
0x0000000000000000000000000000000000000000000000000000000000000001n,
0x0000000000000000000000000000000000000000000000000000000000000002n,
0x0000000000000000000000000000000000000000000000000000000000000003n,
0x0000000000000000000000000000000000000000000000000000000000000004n,
]),
[
0x299c867db6c1fdd79dcefa40e4510b9837e60ebb1ce0663dbaa525df65250465n,
0x1148aaef609aa338b27dafd89bb98862d8bb2b429aceac47d86206154ffe053dn,
0x24febb87fed7462e23f6665ff9a0111f4044c38ee1672c1ac6b0637d34f24907n,
0x0eb08f6d809668a981c186beaf6110060707059576406b248e5d9cf6e78b3d3en,
0x07748bc6877c9b82c8b98666ee9d0626ec7f5be4205f79ee8528ef1c4a376fc7n,
]
);
});
// Startadperm is unsupported, since it is non prime field
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run();
}

View File

@@ -0,0 +1,34 @@
import { deepStrictEqual, throws } from 'assert';
import { readFileSync } from 'fs';
import { should, describe } from 'micro-should';
import { bytesToHex as hex } from '@noble/hashes/utils';
import { schnorr } from '../esm/secp256k1.js';
const schCsv = readFileSync('./test/vectors/schnorr.csv', 'utf-8');
describe('schnorr.sign()', () => {
// index,secret key,public key,aux_rand,message,signature,verification result,comment
const vectors = schCsv
.split('\n')
.map((line) => line.split(','))
.slice(1, -1);
for (let vec of vectors) {
const [index, sec, pub, rnd, msg, expSig, passes, comment] = vec;
should(`${comment || 'vector ' + index}`, () => {
if (sec) {
deepStrictEqual(hex(schnorr.getPublicKey(sec)), pub.toLowerCase());
const sig = schnorr.sign(msg, sec, rnd);
deepStrictEqual(hex(sig), expSig.toLowerCase());
deepStrictEqual(schnorr.verify(sig, msg, pub), true);
} else {
const passed = schnorr.verify(expSig, msg, pub);
deepStrictEqual(passed, passes === 'TRUE');
}
});
}
});
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run();
}

14
test/secp256k1.helpers.js Normal file
View File

@@ -0,0 +1,14 @@
// @ts-ignore
export { secp256k1 as secp } from '../esm/secp256k1.js';
import { secp256k1 as _secp } from '../esm/secp256k1.js';
export { bytesToNumberBE, numberToBytesBE } from '../esm/abstract/utils.js';
export { mod } from '../esm/abstract/modular.js';
export const sigFromDER = (der) => {
return _secp.Signature.fromDER(der);
};
export const sigToDER = (sig) => sig.toDERHex();
export const selectHash = (secp) => secp.CURVE.hash;
export const normVerifySig = (s) => _secp.Signature.fromDER(s);
// export const bytesToNumberBE = secp256k1.utils.bytesToNumberBE;
// export const numberToBytesBE = secp256k1.utils.numberToBytesBE;
// export const mod = mod_;

View File

@@ -1,20 +1,21 @@
import { hexToBytes, bytesToHex as hex } from '@noble/hashes/utils';
import { deepStrictEqual, throws } from 'assert';
import * as fc from 'fast-check'; import * as fc from 'fast-check';
import { secp256k1, schnorr } from '../lib/esm/secp256k1.js';
import { Fp } from '../lib/esm/abstract/modular.js';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { should, describe } from 'micro-should';
// prettier-ignore
import {
secp, sigFromDER, sigToDER, selectHash, normVerifySig, mod, bytesToNumberBE, numberToBytesBE
} from './secp256k1.helpers.js';
import { default as ecdsa } from './vectors/ecdsa.json' assert { type: 'json' }; import { default as ecdsa } from './vectors/ecdsa.json' assert { type: 'json' };
import { default as ecdh } from './vectors/ecdh.json' assert { type: 'json' }; import { default as ecdh } from './vectors/ecdh.json' assert { type: 'json' };
import { default as privates } from './vectors/privates.json' assert { type: 'json' }; import { default as privates } from './vectors/privates.json' assert { type: 'json' };
import { default as points } from './vectors/points.json' assert { type: 'json' }; import { default as points } from './vectors/points.json' assert { type: 'json' };
import { default as wp } from './vectors/wychenproof.json' assert { type: 'json' }; import { default as wp } from './vectors/wychenproof.json' assert { type: 'json' };
import { should, describe } from 'micro-should';
import { deepStrictEqual, throws } from 'assert';
import { hexToBytes, bytesToHex } from '@noble/hashes/utils';
const hex = bytesToHex; const Point = secp.ProjectivePoint;
const secp = secp256k1;
const privatesTxt = readFileSync('./test/vectors/privates-2.txt', 'utf-8'); const privatesTxt = readFileSync('./test/vectors/privates-2.txt', 'utf-8');
const schCsv = readFileSync('./test/vectors/schnorr.csv', 'utf-8');
const FC_BIGINT = fc.bigInt(1n + 1n, secp.CURVE.n - 1n); const FC_BIGINT = fc.bigInt(1n + 1n, secp.CURVE.n - 1n);
// prettier-ignore // prettier-ignore
@@ -24,7 +25,7 @@ const toBEHex = (n) => n.toString(16).padStart(64, '0');
function hexToNumber(hex) { function hexToNumber(hex) {
if (typeof hex !== 'string') { if (typeof hex !== 'string') {
throw new TypeError('hexToNumber: expected string, got ' + typeof hex); throw new Error('hexToNumber: expected string, got ' + typeof hex);
} }
// Big Endian // Big Endian
return BigInt(`0x${hex}`); return BigInt(`0x${hex}`);
@@ -37,15 +38,15 @@ describe('secp256k1', () => {
.filter((line) => line) .filter((line) => line)
.map((line) => line.split(':')); .map((line) => line.split(':'));
for (let [priv, x, y] of data) { for (let [priv, x, y] of data) {
const point = secp.Point.fromPrivateKey(BigInt(priv)); const point = Point.fromPrivateKey(BigInt(priv));
deepStrictEqual(toBEHex(point.x), x); deepStrictEqual(toBEHex(point.x), x);
deepStrictEqual(toBEHex(point.y), y); deepStrictEqual(toBEHex(point.y), y);
const point2 = secp.Point.fromHex(secp.getPublicKey(toBEHex(BigInt(priv)))); const point2 = Point.fromHex(secp.getPublicKey(toBEHex(BigInt(priv))));
deepStrictEqual(toBEHex(point2.x), x); deepStrictEqual(toBEHex(point2.x), x);
deepStrictEqual(toBEHex(point2.y), y); deepStrictEqual(toBEHex(point2.y), y);
const point3 = secp.Point.fromHex(secp.getPublicKey(hexToBytes(toBEHex(BigInt(priv))))); const point3 = Point.fromHex(secp.getPublicKey(hexToBytes(toBEHex(BigInt(priv)))));
deepStrictEqual(toBEHex(point3.x), x); deepStrictEqual(toBEHex(point3.x), x);
deepStrictEqual(toBEHex(point3.y), y); deepStrictEqual(toBEHex(point3.y), y);
} }
@@ -62,71 +63,72 @@ describe('secp256k1', () => {
.filter((line) => line) .filter((line) => line)
.map((line) => line.split(':')); .map((line) => line.split(':'));
for (let [priv, x, y] of data) { for (let [priv, x, y] of data) {
const point = secp.Point.fromPrivateKey(BigInt(priv)); const point = Point.fromPrivateKey(BigInt(priv));
deepStrictEqual(toBEHex(point.x), x); deepStrictEqual(toBEHex(point.x), x);
deepStrictEqual(toBEHex(point.y), y); deepStrictEqual(toBEHex(point.y), y);
const point2 = secp.Point.fromHex(secp.getPublicKey(toBEHex(BigInt(priv)))); const point2 = Point.fromHex(secp.getPublicKey(toBEHex(BigInt(priv))));
deepStrictEqual(toBEHex(point2.x), x); deepStrictEqual(toBEHex(point2.x), x);
deepStrictEqual(toBEHex(point2.y), y); deepStrictEqual(toBEHex(point2.y), y);
const point3 = secp.Point.fromHex(secp.getPublicKey(hexToBytes(toBEHex(BigInt(priv))))); const point3 = Point.fromHex(secp.getPublicKey(hexToBytes(toBEHex(BigInt(priv)))));
deepStrictEqual(toBEHex(point3.x), x); deepStrictEqual(toBEHex(point3.x), x);
deepStrictEqual(toBEHex(point3.y), y); deepStrictEqual(toBEHex(point3.y), y);
} }
}); });
should('Point.isValidPoint()', () => { describe('Point', () => {
should('fromHex() assertValidity', () => {
for (const vector of points.valid.isPoint) { for (const vector of points.valid.isPoint) {
const { P, expected } = vector; const { P, expected } = vector;
if (expected) { if (expected) {
secp.Point.fromHex(P); Point.fromHex(P);
} else { } else {
throws(() => secp.Point.fromHex(P)); throws(() => Point.fromHex(P));
} }
} }
}); });
should('Point.fromPrivateKey()', () => { should('.fromPrivateKey()', () => {
for (const vector of points.valid.pointFromScalar) { for (const vector of points.valid.pointFromScalar) {
const { d, expected } = vector; const { d, expected } = vector;
let p = secp.Point.fromPrivateKey(d); let p = Point.fromPrivateKey(d);
deepStrictEqual(p.toHex(true), expected); deepStrictEqual(p.toHex(true), expected);
} }
}); });
should('Point#toHex(compressed)', () => { should('#toHex(compressed)', () => {
for (const vector of points.valid.pointCompress) { for (const vector of points.valid.pointCompress) {
const { P, compress, expected } = vector; const { P, compress, expected } = vector;
let p = secp.Point.fromHex(P); let p = Point.fromHex(P);
deepStrictEqual(p.toHex(compress), expected); deepStrictEqual(p.toHex(compress), expected);
} }
}); });
should('Point#toHex() roundtrip (failed case)', () => { should('#toHex() roundtrip (failed case)', () => {
const point1 = const point1 =
secp.Point.fromPrivateKey( Point.fromPrivateKey(
88572218780422190464634044548753414301110513745532121983949500266768436236425n 88572218780422190464634044548753414301110513745532121983949500266768436236425n
); );
// const hex = point1.toHex(true); // const hex = point1.toHex(true);
// deepStrictEqual(secp.Point.fromHex(hex).toHex(true), hex); // deepStrictEqual(Point.fromHex(hex).toHex(true), hex);
}); });
should('Point#toHex() roundtrip', () => { should('#toHex() roundtrip', () => {
fc.assert( fc.assert(
fc.property(FC_BIGINT, (x) => { fc.property(FC_BIGINT, (x) => {
const point1 = secp.Point.fromPrivateKey(x); const point1 = Point.fromPrivateKey(x);
const hex = point1.toHex(true); const hex = point1.toHex(true);
deepStrictEqual(secp.Point.fromHex(hex).toHex(true), hex); deepStrictEqual(Point.fromHex(hex).toHex(true), hex);
}) })
); );
}); });
should('Point#add(other)', () => { should('#add(other)', () => {
for (const vector of points.valid.pointAdd) { for (const vector of points.valid.pointAdd) {
const { P, Q, expected } = vector; const { P, Q, expected } = vector;
let p = secp.Point.fromHex(P); let p = Point.fromHex(P);
let q = secp.Point.fromHex(Q); let q = Point.fromHex(Q);
if (expected) { if (expected) {
deepStrictEqual(p.add(q).toHex(true), expected); deepStrictEqual(p.add(q).toHex(true), expected);
} else { } else {
@@ -137,12 +139,12 @@ describe('secp256k1', () => {
} }
}); });
should('Point#multiply(privateKey)', () => { should('#multiply(privateKey)', () => {
for (const vector of points.valid.pointMultiply) { for (const vector of points.valid.pointMultiply) {
const { P, d, expected } = vector; const { P, d, expected } = vector;
const p = secp.Point.fromHex(P); const p = Point.fromHex(P);
if (expected) { if (expected) {
deepStrictEqual(p.multiply(hexToNumber(d)).toHex(true), expected); deepStrictEqual(p.multiply(hexToNumber(d)).toHex(true), expected, P);
} else { } else {
throws(() => { throws(() => {
p.multiply(hexToNumber(d)).toHex(true); p.multiply(hexToNumber(d)).toHex(true);
@@ -154,15 +156,16 @@ describe('secp256k1', () => {
const { P, d } = vector; const { P, d } = vector;
if (hexToNumber(d) < secp.CURVE.n) { if (hexToNumber(d) < secp.CURVE.n) {
throws(() => { throws(() => {
const p = secp.Point.fromHex(P); const p = Point.fromHex(P);
p.multiply(hexToNumber(d)).toHex(true); p.multiply(hexToNumber(d)).toHex(true);
}); });
} }
} }
for (const num of [0n, 0, -1n, -1, 1.1]) { for (const num of [0n, 0, -1n, -1, 1.1]) {
throws(() => secp.Point.BASE.multiply(num)); throws(() => Point.BASE.multiply(num));
} }
}); });
});
// multiply() should equal multiplyUnsafe() // multiply() should equal multiplyUnsafe()
// should('ProjectivePoint#multiplyUnsafe', () => { // should('ProjectivePoint#multiplyUnsafe', () => {
@@ -175,8 +178,8 @@ describe('secp256k1', () => {
// console.log(p0.multiply(z)); // console.log(p0.multiply(z));
// console.log(secp.ProjectivePoint.normalizeZ([p0.multiplyUnsafe(z)])[0]) // console.log(secp.ProjectivePoint.normalizeZ([p0.multiplyUnsafe(z)])[0])
// }); // });
describe('Signature', () => {
should('Signature.fromCompactHex() roundtrip', () => { should('.fromCompactHex() roundtrip', () => {
fc.assert( fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, (r, s) => { fc.property(FC_BIGINT, FC_BIGINT, (r, s) => {
const sig = new secp.Signature(r, s); const sig = new secp.Signature(r, s);
@@ -185,16 +188,18 @@ describe('secp256k1', () => {
); );
}); });
should('Signature.fromDERHex() roundtrip', () => { should('.fromDERHex() roundtrip', () => {
fc.assert( fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, (r, s) => { fc.property(FC_BIGINT, FC_BIGINT, (r, s) => {
const sig = new secp.Signature(r, s); const sig = new secp.Signature(r, s);
deepStrictEqual(secp.Signature.fromDER(sig.toDERHex()), sig); deepStrictEqual(sigFromDER(sigToDER(sig)), sig);
}) })
); );
}); });
});
should('sign()/should create deterministic signatures with RFC 6979', () => { describe('sign()', () => {
should('create deterministic signatures with RFC 6979', () => {
for (const vector of ecdsa.valid) { for (const vector of ecdsa.valid) {
let usig = secp.sign(vector.m, vector.d); let usig = secp.sign(vector.m, vector.d);
let sig = usig.toCompactHex(); let sig = usig.toCompactHex();
@@ -204,21 +209,18 @@ describe('secp256k1', () => {
} }
}); });
should( should('not create invalid deterministic signatures with RFC 6979', () => {
'secp256k1.sign()/should not create invalid deterministic signatures with RFC 6979',
() => {
for (const vector of ecdsa.invalid.sign) { for (const vector of ecdsa.invalid.sign) {
throws(() => secp.sign(vector.m, vector.d)); throws(() => secp.sign(vector.m, vector.d));
} }
} });
);
should('sign()/edge cases', () => { should('edge cases', () => {
throws(() => secp.sign()); throws(() => secp.sign());
throws(() => secp.sign('')); throws(() => secp.sign(''));
}); });
should('sign()/should create correct DER encoding against libsecp256k1', () => { should('create correct DER encoding against libsecp256k1', () => {
const CASES = [ const CASES = [
[ [
'd1a9dc8ed4e46a6a3e5e594615ca351d7d7ef44df1e4c94c1802f3592183794b', 'd1a9dc8ed4e46a6a3e5e594615ca351d7d7ef44df1e4c94c1802f3592183794b',
@@ -233,15 +235,17 @@ describe('secp256k1', () => {
'3045022100d18990bba7832bb283e3ecf8700b67beb39acc73f4200ed1c331247c46edccc602202e5c8bbfe47ae159512c583b30a3fa86575cddc62527a03de7756517ae4c6c73', '3045022100d18990bba7832bb283e3ecf8700b67beb39acc73f4200ed1c331247c46edccc602202e5c8bbfe47ae159512c583b30a3fa86575cddc62527a03de7756517ae4c6c73',
], ],
]; ];
const privKey = hexToBytes('0101010101010101010101010101010101010101010101010101010101010101'); const privKey = hexToBytes(
'0101010101010101010101010101010101010101010101010101010101010101'
);
for (const [msg, exp] of CASES) { for (const [msg, exp] of CASES) {
const res = secp.sign(msg, privKey, { extraEntropy: undefined }); const res = secp.sign(msg, privKey, { extraEntropy: undefined });
deepStrictEqual(res.toDERHex(), exp); deepStrictEqual(sigToDER(res), exp);
const rs = secp.Signature.fromDER(res.toDERHex()).toCompactHex(); const rs = sigFromDER(sigToDER(res)).toCompactHex();
deepStrictEqual(secp.Signature.fromCompact(rs).toDERHex(), exp); deepStrictEqual(sigToDER(secp.Signature.fromCompact(rs)), exp);
} }
}); });
should('sign()/sign ecdsa extraData', () => { should('handle {extraData} option', () => {
const ent1 = '0000000000000000000000000000000000000000000000000000000000000000'; const ent1 = '0000000000000000000000000000000000000000000000000000000000000000';
const ent2 = '0000000000000000000000000000000000000000000000000000000000000001'; const ent2 = '0000000000000000000000000000000000000000000000000000000000000001';
const ent3 = '6e723d3fd94ed5d2b6bdd4f123364b0f3ca52af829988a63f8afe91d29db1c33'; const ent3 = '6e723d3fd94ed5d2b6bdd4f123364b0f3ca52af829988a63f8afe91d29db1c33';
@@ -261,34 +265,36 @@ describe('secp256k1', () => {
deepStrictEqual(sign(ent5), e.extraEntropyMax); deepStrictEqual(sign(ent5), e.extraEntropyMax);
} }
}); });
});
should('verify()/should verify signature', () => { describe('verify()', () => {
should('verify signature', () => {
const MSG = '01'.repeat(32); const MSG = '01'.repeat(32);
const PRIV_KEY = 0x2n; const PRIV_KEY = 0x2n;
const signature = secp.sign(MSG, PRIV_KEY); const signature = secp.sign(MSG, PRIV_KEY);
const publicKey = secp.getPublicKey(PRIV_KEY); const publicKey = secp.getPublicKey(PRIV_KEY);
deepStrictEqual(publicKey.length, 65); deepStrictEqual(publicKey.length, 33);
deepStrictEqual(secp.verify(signature, MSG, publicKey), true); deepStrictEqual(secp.verify(signature, MSG, publicKey), true);
}); });
should('verify()/should not verify signature with wrong public key', () => { should(' not verify signature with wrong public key', () => {
const MSG = '01'.repeat(32); const MSG = '01'.repeat(32);
const PRIV_KEY = 0x2n; const PRIV_KEY = '01'.repeat(32);
const WRONG_PRIV_KEY = 0x22n; const WRONG_PRIV_KEY = '02'.repeat(32);
const signature = secp.sign(MSG, PRIV_KEY); const signature = secp.sign(MSG, PRIV_KEY);
const publicKey = secp.Point.fromPrivateKey(WRONG_PRIV_KEY).toHex(); const publicKey = Point.fromPrivateKey(WRONG_PRIV_KEY).toHex();
deepStrictEqual(publicKey.length, 130); deepStrictEqual(publicKey.length, 66);
deepStrictEqual(secp.verify(signature, MSG, publicKey), false); deepStrictEqual(secp.verify(signature, MSG, publicKey), false);
}); });
should('verify()/should not verify signature with wrong hash', () => { should('not verify signature with wrong hash', () => {
const MSG = '01'.repeat(32); const MSG = '01'.repeat(32);
const PRIV_KEY = 0x2n; const PRIV_KEY = 0x2n;
const WRONG_MSG = '11'.repeat(32); const WRONG_MSG = '11'.repeat(32);
const signature = secp.sign(MSG, PRIV_KEY); const signature = secp.sign(MSG, PRIV_KEY);
const publicKey = secp.getPublicKey(PRIV_KEY); const publicKey = secp.getPublicKey(PRIV_KEY);
deepStrictEqual(publicKey.length, 65); deepStrictEqual(publicKey.length, 33);
deepStrictEqual(secp.verify(signature, WRONG_MSG, publicKey), false); deepStrictEqual(secp.verify(signature, WRONG_MSG, publicKey), false);
}); });
should('verify()/should verify random signatures', () => should('verify random signatures', () =>
fc.assert( fc.assert(
fc.property(FC_BIGINT, fc.hexaString({ minLength: 64, maxLength: 64 }), (privKey, msg) => { fc.property(FC_BIGINT, fc.hexaString({ minLength: 64, maxLength: 64 }), (privKey, msg) => {
const pub = secp.getPublicKey(privKey); const pub = secp.getPublicKey(privKey);
@@ -297,7 +303,7 @@ describe('secp256k1', () => {
}) })
) )
); );
should('verify()/should not verify signature with invalid r/s', () => { should('not verify signature with invalid r/s', () => {
const msg = new Uint8Array([ const msg = new Uint8Array([
0xbb, 0x5a, 0x52, 0xf4, 0x2f, 0x9c, 0x92, 0x61, 0xed, 0x43, 0x61, 0xf5, 0x94, 0x22, 0xa1, 0xbb, 0x5a, 0x52, 0xf4, 0x2f, 0x9c, 0x92, 0x61, 0xed, 0x43, 0x61, 0xf5, 0x94, 0x22, 0xa1,
0xe3, 0x00, 0x36, 0xe7, 0xc3, 0x2b, 0x27, 0x0c, 0x88, 0x07, 0xa4, 0x19, 0xfe, 0xca, 0x60, 0xe3, 0x00, 0x36, 0xe7, 0xc3, 0x2b, 0x27, 0x0c, 0x88, 0x07, 0xa4, 0x19, 0xfe, 0xca, 0x60,
@@ -308,7 +314,7 @@ describe('secp256k1', () => {
const r = 1n; const r = 1n;
const s = 115792089237316195423570985008687907852837564279074904382605163141518162728904n; const s = 115792089237316195423570985008687907852837564279074904382605163141518162728904n;
const pub = new secp.Point(x, y); const pub = new Point(x, y, 1n).toRawBytes();
const signature = new secp.Signature(2n, 2n); const signature = new secp.Signature(2n, 2n);
signature.r = r; signature.r = r;
signature.s = s; signature.s = s;
@@ -317,60 +323,38 @@ describe('secp256k1', () => {
// Verifies, but it shouldn't, because signature S > curve order // Verifies, but it shouldn't, because signature S > curve order
deepStrictEqual(verified, false); deepStrictEqual(verified, false);
}); });
should('verify()/should not verify msg = curve order', () => { should('not verify msg = curve order', () => {
const msg = 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'; const msg = 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141';
const x = 55066263022277343669578718895168534326250603453777594175500187360389116729240n; const x = 55066263022277343669578718895168534326250603453777594175500187360389116729240n;
const y = 32670510020758816978083085130507043184471273380659243275938904335757337482424n; const y = 32670510020758816978083085130507043184471273380659243275938904335757337482424n;
const r = 104546003225722045112039007203142344920046999340768276760147352389092131869133n; const r = 104546003225722045112039007203142344920046999340768276760147352389092131869133n;
const s = 96900796730960181123786672629079577025401317267213807243199432755332205217369n; const s = 96900796730960181123786672629079577025401317267213807243199432755332205217369n;
const pub = new secp.Point(x, y); const pub = new Point(x, y, 1n).toRawBytes();
const sig = new secp.Signature(r, s); const sig = new secp.Signature(r, s);
deepStrictEqual(secp.verify(sig, msg, pub), false); deepStrictEqual(secp.verify(sig, msg, pub), false);
}); });
should('verify()/should verify non-strict msg bb5a...', () => { should('verify non-strict msg bb5a...', () => {
const msg = 'bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023'; const msg = 'bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023';
const x = 3252872872578928810725465493269682203671229454553002637820453004368632726370n; const x = 3252872872578928810725465493269682203671229454553002637820453004368632726370n;
const y = 17482644437196207387910659778872952193236850502325156318830589868678978890912n; const y = 17482644437196207387910659778872952193236850502325156318830589868678978890912n;
const r = 432420386565659656852420866390673177323n; const r = 432420386565659656852420866390673177323n;
const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n; const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n;
const pub = new secp.Point(x, y); const pub = new Point(x, y, 1n).toRawBytes();
const sig = new secp.Signature(r, s); const sig = new secp.Signature(r, s);
deepStrictEqual(secp.verify(sig, msg, pub, { strict: false }), true); deepStrictEqual(secp.verify(sig, msg, pub, { lowS: false }), true);
}); });
should( should('not verify invalid deterministic signatures with RFC 6979', () => {
'secp256k1.verify()/should not verify invalid deterministic signatures with RFC 6979',
() => {
for (const vector of ecdsa.invalid.verify) { for (const vector of ecdsa.invalid.verify) {
const res = secp.verify(vector.signature, vector.m, vector.Q); const res = secp.verify(vector.signature, vector.m, vector.Q);
deepStrictEqual(res, false); deepStrictEqual(res, false);
} }
}
);
// index,secret key,public key,aux_rand,message,signature,verification result,comment
const vectors = schCsv
.split('\n')
.map((line) => line.split(','))
.slice(1, -1);
for (let vec of vectors) {
const [index, sec, pub, rnd, msg, expSig, passes, comment] = vec;
should(`sign with Schnorr scheme vector ${index}`, () => {
if (sec) {
deepStrictEqual(hex(schnorr.getPublicKey(sec)), pub.toLowerCase());
const sig = schnorr.sign(msg, sec, rnd);
deepStrictEqual(hex(sig), expSig.toLowerCase());
deepStrictEqual(schnorr.verify(sig, msg, pub), true);
} else {
const passed = schnorr.verify(expSig, msg, pub);
deepStrictEqual(passed, passes === 'TRUE');
}
}); });
} });
describe('recoverPublicKey()', () => {
should('recoverPublicKey()/should recover public key from recovery bit', () => { should('recover public key from recovery bit', () => {
const message = '00000000000000000000000000000000000000000000000000000000deadbeef'; const message = '00000000000000000000000000000000000000000000000000000000deadbeef';
const privateKey = 123456789n; const privateKey = 123456789n;
const publicKey = secp.Point.fromHex(secp.getPublicKey(privateKey)).toHex(false); const publicKey = Point.fromHex(secp.getPublicKey(privateKey)).toHex(false);
const sig = secp.sign(message, privateKey); const sig = secp.sign(message, privateKey);
const recoveredPubkey = sig.recoverPublicKey(message); const recoveredPubkey = sig.recoverPublicKey(message);
// const recoveredPubkey = secp.recoverPublicKey(message, signature, recovery); // const recoveredPubkey = secp.recoverPublicKey(message, signature, recovery);
@@ -378,36 +362,38 @@ describe('secp256k1', () => {
deepStrictEqual(recoveredPubkey.toHex(false), publicKey); deepStrictEqual(recoveredPubkey.toHex(false), publicKey);
deepStrictEqual(secp.verify(sig, message, publicKey), true); deepStrictEqual(secp.verify(sig, message, publicKey), true);
}); });
should('recoverPublicKey()/should not recover zero points', () => { should('not recover zero points', () => {
const msgHash = '6b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9'; const msgHash = '6b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9';
const sig = const sig =
'79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817986b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9'; '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817986b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9';
const recovery = 0; const recovery = 0;
throws(() => secp.recoverPublicKey(msgHash, sig, recovery)); throws(() => secp.recoverPublicKey(msgHash, sig, recovery));
}); });
should('recoverPublicKey()/should handle all-zeros msghash', () => { should('handle all-zeros msghash', () => {
const privKey = secp.utils.randomPrivateKey(); const privKey = secp.utils.randomPrivateKey();
const pub = secp.getPublicKey(privKey); const pub = secp.getPublicKey(privKey);
const zeros = '0000000000000000000000000000000000000000000000000000000000000000'; const zeros = '0000000000000000000000000000000000000000000000000000000000000000';
const sig = secp.sign(zeros, privKey, { recovered: true }); const sig = secp.sign(zeros, privKey);
const recoveredKey = sig.recoverPublicKey(zeros); const recoveredKey = sig.recoverPublicKey(zeros);
deepStrictEqual(recoveredKey.toRawBytes(), pub); deepStrictEqual(recoveredKey.toRawBytes(), pub);
}); });
should('recoverPublicKey()/should handle RFC 6979 vectors', () => { should('handle RFC 6979 vectors', () => {
for (const vector of ecdsa.valid) { for (const vector of ecdsa.valid) {
let usig = secp.sign(vector.m, vector.d); let usig = secp.sign(vector.m, vector.d);
let sig = usig.toDERHex(); let sig = sigToDER(usig);
const vpub = secp.getPublicKey(vector.d); const vpub = secp.getPublicKey(vector.d);
const recovered = usig.recoverPublicKey(vector.m); const recovered = usig.recoverPublicKey(vector.m);
deepStrictEqual(recovered.toHex(), hex(vpub)); deepStrictEqual(recovered.toHex(), hex(vpub));
} }
}); });
});
describe('getSharedSecret()', () => {
// TODO: Real implementation. // TODO: Real implementation.
function derToPub(der) { function derToPub(der) {
return der.slice(46); return der.slice(46);
} }
should('getSharedSecret()/should produce correct results', () => { should('produce correct results', () => {
// TODO: Once der is there, run all tests. // TODO: Once der is there, run all tests.
for (const vector of ecdh.testGroups[0].tests.slice(0, 230)) { for (const vector of ecdh.testGroups[0].tests.slice(0, 230)) {
if (vector.result === 'invalid' || vector.private.length !== 64) { if (vector.result === 'invalid' || vector.private.length !== 64) {
@@ -420,7 +406,7 @@ describe('secp256k1', () => {
} }
} }
}); });
should('getSharedSecret()/priv/pub order matters', () => { should('priv/pub order matters', () => {
for (const vector of ecdh.testGroups[0].tests.slice(0, 100)) { for (const vector of ecdh.testGroups[0].tests.slice(0, 100)) {
if (vector.result === 'valid') { if (vector.result === 'valid') {
let priv = vector.private; let priv = vector.private;
@@ -429,9 +415,10 @@ describe('secp256k1', () => {
} }
} }
}); });
should('getSharedSecret()/rejects invalid keys', () => { should('reject invalid keys', () => {
throws(() => secp.getSharedSecret('01', '02')); throws(() => secp.getSharedSecret('01', '02'));
}); });
});
should('utils.isValidPrivateKey()', () => { should('utils.isValidPrivateKey()', () => {
for (const vector of privates.valid.isPrivate) { for (const vector of privates.valid.isPrivate) {
@@ -442,58 +429,52 @@ describe('secp256k1', () => {
should('have proper curve equation in assertValidity()', () => { should('have proper curve equation in assertValidity()', () => {
throws(() => { throws(() => {
const { Fp } = secp.CURVE; const { Fp } = secp.CURVE;
let point = new secp.Point(Fp.create(-2n), Fp.create(-1n)); let point = new Point(Fp.create(-2n), Fp.create(-1n), Fp.create(1n));
point.assertValidity(); point.assertValidity();
}); });
}); });
const Fn = Fp(secp.CURVE.n); describe('tweak utilities (legacy)', () => {
const normal = secp.utils._normalizePrivateKey; const normal = secp.utils.normPrivateKeyToScalar;
const tweakUtils = { const tweakUtils = {
privateAdd: (privateKey, tweak) => { privateAdd: (privateKey, tweak) => {
const p = normal(privateKey); return numberToBytesBE(mod(normal(privateKey) + normal(tweak), secp.CURVE.n), 32);
const t = normal(tweak);
return secp.utils._bigintToBytes(Fn.create(p + t));
}, },
privateNegate: (privateKey) => { privateNegate: (privateKey) => {
const p = normal(privateKey); return numberToBytesBE(mod(-normal(privateKey), secp.CURVE.n), 32);
return secp.utils._bigintToBytes(Fn.negate(p));
}, },
pointAddScalar: (p, tweak, isCompressed) => { pointAddScalar: (p, tweak, isCompressed) => {
const P = secp.Point.fromHex(p); const tweaked = Point.fromHex(p).add(Point.fromPrivateKey(tweak));
const t = normal(tweak); if (tweaked.equals(Point.ZERO)) throw new Error('Tweaked point at infinity');
const Q = secp.Point.BASE.multiplyAndAddUnsafe(P, t, 1n); return tweaked.toRawBytes(isCompressed);
if (!Q) throw new Error('Tweaked point at infinity');
return Q.toRawBytes(isCompressed);
}, },
pointMultiply: (p, tweak, isCompressed) => { pointMultiply: (p, tweak, isCompressed) => {
const P = secp.Point.fromHex(p); if (typeof tweak === 'string') tweak = hexToBytes(tweak);
const h = typeof tweak === 'string' ? tweak : bytesToHex(tweak); const t = bytesToNumberBE(tweak);
const t = BigInt(`0x${h}`); return Point.fromHex(p).multiply(t).toRawBytes(isCompressed);
return P.multiply(t).toRawBytes(isCompressed);
}, },
}; };
should('privateAdd()', () => { should('privateAdd()', () => {
for (const vector of privates.valid.add) { for (const vector of privates.valid.add) {
const { a, b, expected } = vector; const { a, b, expected } = vector;
deepStrictEqual(bytesToHex(tweakUtils.privateAdd(a, b)), expected); deepStrictEqual(hex(tweakUtils.privateAdd(a, b)), expected);
} }
}); });
should('privateNegate()', () => { should('privateNegate()', () => {
for (const vector of privates.valid.negate) { for (const vector of privates.valid.negate) {
const { a, expected } = vector; const { a, expected } = vector;
deepStrictEqual(bytesToHex(tweakUtils.privateNegate(a)), expected); deepStrictEqual(hex(tweakUtils.privateNegate(a)), expected);
} }
}); });
should('pointAddScalar()', () => { should('pointAddScalar()', () => {
for (const vector of points.valid.pointAddScalar) { for (const vector of points.valid.pointAddScalar) {
const { description, P, d, expected } = vector; const { description, P, d, expected } = vector;
const compressed = !!expected && expected.length === 66; // compressed === 33 bytes const compressed = !!expected && expected.length === 66; // compressed === 33 bytes
deepStrictEqual(bytesToHex(tweakUtils.pointAddScalar(P, d, compressed)), expected); deepStrictEqual(hex(tweakUtils.pointAddScalar(P, d, compressed)), expected);
} }
}); });
should('pointAddScalar() invalid', () => { should('pointAddScalar() invalid', () => {
@@ -505,7 +486,7 @@ describe('secp256k1', () => {
should('pointMultiply()', () => { should('pointMultiply()', () => {
for (const vector of points.valid.pointMultiply) { for (const vector of points.valid.pointMultiply) {
const { P, d, expected } = vector; const { P, d, expected } = vector;
deepStrictEqual(bytesToHex(tweakUtils.pointMultiply(P, d, true)), expected); deepStrictEqual(hex(tweakUtils.pointMultiply(P, d, true)), expected);
} }
}); });
should('pointMultiply() invalid', () => { should('pointMultiply() invalid', () => {
@@ -514,15 +495,19 @@ describe('secp256k1', () => {
throws(() => tweakUtils.pointMultiply(P, d)); throws(() => tweakUtils.pointMultiply(P, d));
} }
}); });
});
should('wychenproof vectors', () => { should('wycheproof vectors', () => {
for (let group of wp.testGroups) { for (let group of wp.testGroups) {
const pubKey = secp.Point.fromHex(group.key.uncompressed); // const pubKey = Point.fromHex().toRawBytes();
const pubKey = group.key.uncompressed;
for (let test of group.tests) { for (let test of group.tests) {
const m = secp.CURVE.hash(hexToBytes(test.msg)); const h = selectHash(secp);
const m = h(hexToBytes(test.msg));
if (test.result === 'valid' || test.result === 'acceptable') { if (test.result === 'valid' || test.result === 'acceptable') {
const verified = secp.verify(test.sig, m, pubKey); const verified = secp.verify(normVerifySig(test.sig), m, pubKey);
if (secp.Signature.fromDER(test.sig).hasHighS()) { if (sigFromDER(test.sig).hasHighS()) {
deepStrictEqual(verified, false); deepStrictEqual(verified, false);
} else { } else {
deepStrictEqual(verified, true); deepStrictEqual(verified, true);

View File

@@ -1,200 +0,0 @@
import { deepStrictEqual, throws } from 'assert';
import { should } from 'micro-should';
import * as starknet from '../../lib/esm/stark.js';
import { default as issue2 } from './fixtures/issue2.json' assert { type: 'json' };
should('Basic elliptic sanity check', () => {
const g1 = starknet.Point.BASE;
deepStrictEqual(
g1.x.toString(16),
'1ef15c18599971b7beced415a40f0c7deacfd9b0d1819e03d723d8bc943cfca'
);
deepStrictEqual(
g1.y.toString(16),
'5668060aa49730b7be4801df46ec62de53ecd11abe43a32873000c36e8dc1f'
);
const g2 = g1.double();
deepStrictEqual(
g2.x.toString(16),
'759ca09377679ecd535a81e83039658bf40959283187c654c5416f439403cf5'
);
deepStrictEqual(
g2.y.toString(16),
'6f524a3400e7708d5c01a28598ad272e7455aa88778b19f93b562d7a9646c41'
);
const g3 = g2.add(g1);
deepStrictEqual(
g3.x.toString(16),
'411494b501a98abd8262b0da1351e17899a0c4ef23dd2f96fec5ba847310b20'
);
deepStrictEqual(
g3.y.toString(16),
'7e1b3ebac08924d2c26f409549191fcf94f3bf6f301ed3553e22dfb802f0686'
);
const g32 = g1.multiply(3);
deepStrictEqual(
g32.x.toString(16),
'411494b501a98abd8262b0da1351e17899a0c4ef23dd2f96fec5ba847310b20'
);
deepStrictEqual(
g32.y.toString(16),
'7e1b3ebac08924d2c26f409549191fcf94f3bf6f301ed3553e22dfb802f0686'
);
const minus1 = g1.multiply(starknet.CURVE.n - 1n);
deepStrictEqual(
minus1.x.toString(16),
'1ef15c18599971b7beced415a40f0c7deacfd9b0d1819e03d723d8bc943cfca'
);
deepStrictEqual(
minus1.y.toString(16),
'7a997f9f55b68e04841b7fe20b9139d21ac132ee541bc5cd78cfff3c91723e2'
);
});
should('Pedersen', () => {
deepStrictEqual(
starknet.pedersen(2, 3),
'0x5774fa77b3d843ae9167abd61cf80365a9b2b02218fc2f628494b5bdc9b33b8'
);
deepStrictEqual(
starknet.pedersen(1, 2),
'0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026'
);
deepStrictEqual(
starknet.pedersen(3, 4),
'0x262697b88544f733e5c6907c3e1763131e9f14c51ee7951258abbfb29415fbf'
);
});
should('Hash chain', () => {
deepStrictEqual(
starknet.hashChain([1, 2, 3]),
'0x5d9d62d4040b977c3f8d2389d494e4e89a96a8b45c44b1368f1cc6ec5418915'
);
});
should('Pedersen hash edgecases', () => {
// >>> pedersen_hash(0,0)
const zero = '0x49ee3eba8c1600700ee1b87eb599f16716b0b1022947733551fde4050ca6804';
deepStrictEqual(starknet.pedersen(0, 0), zero);
deepStrictEqual(starknet.pedersen(0n, 0n), zero);
deepStrictEqual(starknet.pedersen('0', '0'), zero);
deepStrictEqual(starknet.pedersen('0x0', '0x0'), zero);
// >>> pedersen_hash(3618502788666131213697322783095070105623107215331596699973092056135872020475,3618502788666131213697322783095070105623107215331596699973092056135872020475)
// 3226051580231087455100099637526672350308978851161639703631919449959447036451
const big = 3618502788666131213697322783095070105623107215331596699973092056135872020475n;
const bigExp = '0x721e167a36655994e88efa865e2ed8a0488d36db4d988fec043cda755728223';
deepStrictEqual(starknet.pedersen(big, big), bigExp);
// >= FIELD
const big2 = 36185027886661312136973227830950701056231072153315966999730920561358720204751n;
throws(() => starknet.pedersen(big2, big2), 'big2');
// FIELD -1
const big3 = 3618502788666131213697322783095070105623107215331596699973092056135872020480n;
const big3exp = '0x7258fccaf3371fad51b117471d9d888a1786c5694c3e6099160477b593a576e';
deepStrictEqual(starknet.pedersen(big3, big3), big3exp, 'big3');
// FIELD
const big4 = 3618502788666131213697322783095070105623107215331596699973092056135872020481n;
throws(() => starknet.pedersen(big4, big4), 'big4');
throws(() => starknet.pedersen(-1, -1), 'neg');
throws(() => starknet.pedersen(false, false), 'false');
throws(() => starknet.pedersen(true, true), 'true');
throws(() => starknet.pedersen(10.1, 10.1), 'float');
});
should('hashChain edgecases', () => {
deepStrictEqual(starknet.hashChain([32312321312321312312312321n]), '0x1aba6672c014b4838cc201');
deepStrictEqual(
starknet.hashChain([1n, 2n]),
'0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026'
);
deepStrictEqual(
starknet.hashChain([1, 2]),
'0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026'
);
throws(() => starknet.hashChain([]));
throws(() => starknet.hashChain('123'));
deepStrictEqual(
starknet.hashChain([1, 2]),
'0x5bb9440e27889a364bcb678b1f679ecd1347acdedcbf36e83494f857cc58026'
);
});
should('Pedersen hash, issue #2', () => {
// Verified with starnet.js
deepStrictEqual(
starknet.computeHashOnElements(issue2),
'0x22064462ea33a6ce5272a295e0f551c5da3834f80d8444e7a4df68190b1bc42'
);
deepStrictEqual(
starknet.computeHashOnElements([]),
'0x49ee3eba8c1600700ee1b87eb599f16716b0b1022947733551fde4050ca6804'
);
deepStrictEqual(
starknet.computeHashOnElements([1]),
'0x78d74f61aeaa8286418fd34b3a12a610445eba11d00ecc82ecac2542d55f7a4'
);
});
import * as bip32 from '@scure/bip32';
import * as bip39 from '@scure/bip39';
should('Seed derivation (example)', () => {
const layer = 'starkex';
const application = 'starkdeployement';
const mnemonic =
'range mountain blast problem vibrant void vivid doctor cluster enough melody ' +
'salt layer language laptop boat major space monkey unit glimpse pause change vibrant';
const ethAddress = '0xa4864d977b944315389d1765ffa7e66F74ee8cd7';
const hdKey = bip32.HDKey.fromMasterSeed(bip39.mnemonicToSeedSync(mnemonic)).derive(
starknet.getAccountPath(layer, application, ethAddress, 0)
);
deepStrictEqual(
starknet.grindKey(hdKey.privateKey),
'6cf0a8bf113352eb863157a45c5e5567abb34f8d32cddafd2c22aa803f4892c'
);
});
should('Compressed keys', () => {
const G = starknet.Point.BASE;
const half = starknet.CURVE.n / 2n;
const last = starknet.CURVE.n;
const vectors = [
1,
2,
3,
4,
5,
half - 5n,
half - 4n,
half - 3n,
half - 2n,
half - 1n,
half,
half + 1n,
half + 2n,
half + 3n,
half + 4n,
half + 5n,
last - 5n,
last - 4n,
last - 3n,
last - 2n,
last - 1n,
].map((i) => G.multiply(i));
const fixPoint = (pt) => ({ ...pt, _WINDOW_SIZE: undefined });
for (const v of vectors) {
const uncompressed = v.toHex();
const compressed = v.toHex(true);
const exp = fixPoint(v);
deepStrictEqual(fixPoint(starknet.Point.fromHex(uncompressed)), exp);
deepStrictEqual(fixPoint(starknet.Point.fromHex(compressed)), exp);
deepStrictEqual(starknet.Point.fromHex(compressed).toHex(), uncompressed);
}
});
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run();
}

View File

@@ -1,57 +0,0 @@
import * as microStark from '../../../lib/esm/stark.js';
import * as starkwareCrypto from '@starkware-industries/starkware-crypto-utils';
import * as bench from 'micro-bmark';
const { run, mark } = bench; // or bench.mark
const privateKey = '2dccce1da22003777062ee0870e9881b460a8b7eca276870f57c601f182136c';
const msgHash = 'c465dd6b1bbffdb05442eb17f5ca38ad1aa78a6f56bf4415bdee219114a47';
const keyPair = starkwareCrypto.default.ec.keyFromPrivate(privateKey, 'hex');
const publicKeyStark = starkwareCrypto.default.ec.keyFromPublic(
keyPair.getPublic(true, 'hex'),
'hex'
);
const publicKeyMicro = microStark.getPublicKey(privateKey);
const FNS = {
pedersenHash: {
samples: 250,
starkware: () =>
starkwareCrypto.default.pedersen([
'3d937c035c878245caf64531a5756109c53068da139362728feb561405371cb',
'208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a',
]),
'micro-starknet': () =>
microStark.pedersen(
'3d937c035c878245caf64531a5756109c53068da139362728feb561405371cb',
'208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a'
),
},
signVerify: {
samples: 500,
starkware: () =>
starkwareCrypto.default.verify(
publicKeyStark,
msgHash,
starkwareCrypto.default.sign(keyPair, msgHash)
),
'micro-starknet': () =>
microStark.verify(microStark.sign(msgHash, privateKey), msgHash, publicKeyMicro),
},
};
const main = () =>
run(async () => {
for (let [k, libs] of Object.entries(FNS)) {
console.log(`==== ${k} ====`);
for (const [lib, fn] of Object.entries(libs)) {
if (lib === 'samples') continue;
let title = `${k} (${lib})`;
await mark(title, libs.samples, () => fn());
}
console.log();
}
// Log current RAM
bench.logMem();
});
main();

View File

@@ -1,19 +0,0 @@
{
"name": "benchmark",
"private": true,
"version": "0.1.0",
"description": "benchmarks",
"main": "index.js",
"type": "module",
"scripts": {
"bench": "node index.js"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@starkware-industries/starkware-crypto-utils": "^0.0.2",
"micro-bmark": "0.2.0",
"micro-should": "0.2.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +0,0 @@
{
"0x1": "0x1ef15c18599971b7beced415a40f0c7deacfd9b0d1819e03d723d8bc943cfca",
"0x2": "0x759ca09377679ecd535a81e83039658bf40959283187c654c5416f439403cf5",
"0x3": "0x411494b501a98abd8262b0da1351e17899a0c4ef23dd2f96fec5ba847310b20",
"0x4": "0xa7da05a4d664859ccd6e567b935cdfbfe3018c7771cb980892ef38878ae9bc",
"0x5": "0x788435d61046d3eec54d77d25bd194525f4fa26ebe6575536bc6f656656b74c",
"0x6": "0x1efc3d7c9649900fcbd03f578a8248d095bc4b6a13b3c25f9886ef971ff96fa",
"0x7": "0x743829e0a179f8afe223fc8112dfc8d024ab6b235fd42283c4f5970259ce7b7",
"0x8": "0x6eeee2b0c71d681692559735e08a2c3ba04e7347c0c18d4d49b83bb89771591",
"0x9": "0x216b4f076ff47e03a05032d1c6ee17933d8de8b2b4c43eb5ad5a7e1b25d3849",
"0x800000000000000000000000000000000000000000000000000000000000000": "0x5c79074e7f7b834c12c81a9bb0d46691a5e7517767a849d9d98cb84e2176ed2",
"0x800000000000000000000000000000000000000000000000000000000000001": "0x1c4f24e3bd16db0e2457bc005a9d61965105a535554c6b338871e34cb8e2d3a",
"0x800000000000000000000000000000000000000000000000000000000000002": "0xdfbb89b39288a9ddacf3942b4481b04d4fa2f8ed3c424757981cc6357f27ac",
"0x800000000000000000000000000000000000000000000000000000000000003": "0x41bef28265fd750b102f4f2d1e0231de7f4a33900a214f191a63d4fec4e72f4",
"0x800000000000000000000000000000000000000000000000000000000000004": "0x24de66eb164797d4b414e81ded0cfa1a592ef0a9363ebbcb440d4d03cb18af1",
"0x800000000000000000000000000000000000000000000000000000000000005": "0x5efb18c3bc9b69003746acc85fb6ee0cfbdc6adfb982f089cc63e1e5495daad",
"0x800000000000000000000000000000000000000000000000000000000000006": "0x10dc71f00918a8ebfe4085c834d41dd22b251b9f81eef8b9a4fab77e7e1afe9",
"0x800000000000000000000000000000000000000000000000000000000000007": "0x4267ebfd379b1c8caae73febc5920b0c95bd6f9f3536f47c5ddad1259c332ff",
"0x800000000000000000000000000000000000000000000000000000000000008": "0x6da515118c8e01fd5b2e96b814ee95bad7d60be4d2ba6b47e0d283f579d9671",
"0x800000000000000000000000000000000000000000000000000000000000009": "0x7a5b4797f4e56ed1473876bc2693fbe3f2fef7e050717cbae924ff23d426052",
"0x2e9c99d8382fa004dcbbee720aef8a97002de0e991f6a8344e6dc636a71b59e": "0x1ff6803ae740e7e596504ac5c6afbea472e53679361e214f12be0155b13e25d",
"0x8620458785138df8722214e073a91b8f55076ea78197cf41007692dd27fd90": "0x5967da40b90d7ca1e36dc4024381d7d4b403c6ac1a0ab358b0743984934a805",
"0x1b920e7dfb49ba5ada673882af5342e7448d3e9335e0ac37feb6280cd7289ce": "0x78c7ab46333968fbde3201cf512c1eeb5529360259072c459a158dee4449b57",
"0x704170dbfd5dc63caef69d2ce6dfc2b2dbb2af6e75851242bbe79fb6e62a118": "0x534bd8d6ebe4bb2f6992e2d7c19ef3146247e10c2849f357e44eddd283b2af6",
"0x4b58bf4228f39550eca59b5c96a0cb606036cc9495eef9a546f24f01b1b7829": "0x1097a8c5a46d94596f1c8e70ca66941f2bb11e3c8d4fd58fdc4589f09965be8",
"0x2e93226c90fb7a2381a24e940a94b98433e3553dcbf745d3f54d62963c75604": "0x369f0e8c8e984f244290267393a004dba435a4df091767ad5063fece7b1884c",
"0x4615f94598cd756ad1a551d7e57fd725916adfd0054eb773ceb482eef87d0b2": "0x1ee5b8d612102a2408cde59ce52a6498d2e38fe8789bb26d400dea310684ec9",
"0x6ade54b7debd7ca1d4e8e932f9545f8fa4024d73be1efcc86df86367fc333f8": "0x37de3bf52412b2fb9b0030d232ca9dd921cd8f71fd67975cdc62546826e121",
"0x618e7467dd24c2a3449c4df640439c12cdd0f8ea779afcee6e252b2cf494354": "0x71c2b578c432f2d305d3808bb645ecc46dd670cb43d4f4a076f75ccbff74fbc",
"0x7eae185e1f41ec76d214d763f0592f194933622a9dd5f3d52d0209f71619c1a": "0x2b0160052e70176e5b0ff2a6eff90896ae07b732fc27219e36e077735abd57e",
"0x178047D3869489C055D7EA54C014FFB834A069C9595186ABE04EA4D1223A03F": "0x1895a6a77ae14e7987b9cb51329a5adfb17bd8e7c638f92d6892d76e51cebcf"
}

View File

@@ -1,57 +0,0 @@
{
"private_key": "0x3c1e9550e66958296d11b60f8e8e7a7ad990d07fa65d5f7652c4a6c87d4e3cc",
"messages": [
{
"hash": "0x1",
"r": "3162358736122783857144396205516927012128897537504463716197279730251407200037",
"s": "1447067116407676619871126378936374427636662490882969509559888874644844560850"
},
{
"hash": "0x11",
"r": "2282960348362869237018441985726545922711140064809058182483721438101695251648",
"s": "2905868291002627709651322791912000820756370440695830310841564989426104902684"
},
{
"hash": "0x223",
"r": "2851492577225522862152785068304516872062840835882746625971400995051610132955",
"s": "2227464623243182122770469099770977514100002325017609907274766387592987135410"
},
{
"hash": "0x9999",
"r": "3551214266795401081823453828727326248401688527835302880992409448142527576296",
"s": "2580950807716503852408066180369610390914312729170066679103651110985466032285"
},
{
"hash": "0x387e76d1667c4454bfb835144120583af836f8e32a516765497d23eabe16b3f",
"r": "3518448914047769356425227827389998721396724764083236823647519654917215164512",
"s": "3042321032945513635364267149196358883053166552342928199041742035443537684462"
},
{
"hash": "0x3a7e76d1697c4455bfb835144120283af236f8e32a516765497d23eabe16b2",
"r": "2261926635950780594216378185339927576862772034098248230433352748057295357217",
"s": "2708700003762962638306717009307430364534544393269844487939098184375356178572"
},
{
"hash": "0xfa5f0cd1ebff93c9e6474379a213ba111f9e42f2f1cb361b0327e0737203",
"r": "3016953906936760149710218073693613509330129567629289734816320774638425763370",
"s": "306146275372136078470081798635201810092238376869367156373203048583896337506"
},
{
"hash": "0x4c1e9550e66958296d11b60f8e8e7f7ae99dd0cfa6bd5fa652c1a6c87d4e2cc",
"r": "3562728603055564208884290243634917206833465920158600288670177317979301056463",
"s": "1958799632261808501999574190111106370256896588537275453140683641951899459876"
},
{
"hash": "0x6362b40c218fb4c8a8bd42ca482145e8513b78e00faa0de76a98ba14fc37ae8",
"r": "3485557127492692423490706790022678621438670833185864153640824729109010175518",
"s": "897592218067946175671768586886915961592526001156186496738437723857225288280"
}
]
}

View File

@@ -1,5 +0,0 @@
import './basic.test.js';
import './stark.test.js';
import './property.test.js';

View File

@@ -1,51 +0,0 @@
import { deepStrictEqual, throws } from 'assert';
import { should } from 'micro-should';
import * as starknet from '../../lib/esm/stark.js';
import * as fc from 'fast-check';
const FC_BIGINT = fc.bigInt(1n + 1n, starknet.CURVE.n - 1n);
should('Point#toHex() roundtrip', () => {
fc.assert(
fc.property(FC_BIGINT, (x) => {
const point1 = starknet.Point.fromPrivateKey(x);
const hex = point1.toHex(true);
deepStrictEqual(starknet.Point.fromHex(hex).toHex(true), hex);
})
);
});
should('Signature.fromCompactHex() roundtrip', () => {
fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, (r, s) => {
const sig = new starknet.Signature(r, s);
deepStrictEqual(starknet.Signature.fromCompact(sig.toCompactHex()), sig);
})
);
});
should('Signature.fromDERHex() roundtrip', () => {
fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, (r, s) => {
const sig = new starknet.Signature(r, s);
deepStrictEqual(starknet.Signature.fromDER(sig.toDERHex()), sig);
})
);
});
should('verify()/should verify random signatures', () =>
fc.assert(
fc.asyncProperty(FC_BIGINT, fc.hexaString({ minLength: 64, maxLength: 64 }), (privNum, msg) => {
const privKey = privNum.toString(16).padStart(64, '0');
const pub = starknet.getPublicKey(privKey);
const sig = starknet.sign(msg, privKey);
deepStrictEqual(starknet.verify(sig, msg, pub), true);
})
)
);
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run();
}

View File

@@ -1,286 +0,0 @@
import { deepStrictEqual, throws } from 'assert';
import { should } from 'micro-should';
import { hex, utf8 } from '@scure/base';
import * as bip32 from '@scure/bip32';
import * as bip39 from '@scure/bip39';
import * as starknet from '../../lib/esm/stark.js';
import { default as sigVec } from './fixtures/rfc6979_signature_test_vector.json' assert { type: 'json' };
import { default as precomputedKeys } from './fixtures/keys_precomputed.json' assert { type: 'json' };
should('Starknet keccak', () => {
const value = starknet.keccak(utf8.decode('hello'));
deepStrictEqual(value, 0x8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8n);
deepStrictEqual(value < 2n ** 250n, true);
});
should('RFC6979', () => {
for (const msg of sigVec.messages) {
const { r, s } = starknet.sign(msg.hash, sigVec.private_key);
// const { r, s } = starknet.Signature.fromDER(sig);
deepStrictEqual(r.toString(10), msg.r);
deepStrictEqual(s.toString(10), msg.s);
}
});
should('Signatures', () => {
const vectors = [
{
// Message hash of length 61.
msg: 'c465dd6b1bbffdb05442eb17f5ca38ad1aa78a6f56bf4415bdee219114a47',
r: '5f496f6f210b5810b2711c74c15c05244dad43d18ecbbdbe6ed55584bc3b0a2',
s: '4e8657b153787f741a67c0666bad6426c3741b478c8eaa3155196fc571416f3',
},
{
// Message hash of length 61, with leading zeros.
msg: '00c465dd6b1bbffdb05442eb17f5ca38ad1aa78a6f56bf4415bdee219114a47',
r: '5f496f6f210b5810b2711c74c15c05244dad43d18ecbbdbe6ed55584bc3b0a2',
s: '4e8657b153787f741a67c0666bad6426c3741b478c8eaa3155196fc571416f3',
},
{
// Message hash of length 62.
msg: 'c465dd6b1bbffdb05442eb17f5ca38ad1aa78a6f56bf4415bdee219114a47a',
r: '233b88c4578f0807b4a7480c8076eca5cfefa29980dd8e2af3c46a253490e9c',
s: '28b055e825bc507349edfb944740a35c6f22d377443c34742c04e0d82278cf1',
},
{
// Message hash of length 63.
msg: '7465dd6b1bbffdb05442eb17f5ca38ad1aa78a6f56bf4415bdee219114a47a1',
r: 'b6bee8010f96a723f6de06b5fa06e820418712439c93850dd4e9bde43ddf',
s: '1a3d2bc954ed77e22986f507d68d18115fa543d1901f5b4620db98e2f6efd80',
},
];
const privateKey = '2dccce1da22003777062ee0870e9881b460a8b7eca276870f57c601f182136c';
const publicKey = starknet.getPublicKey(privateKey);
for (const v of vectors) {
const sig = starknet.sign(v.msg, privateKey);
const { r, s } = sig;
// const { r, s } = starknet.Signature.fromDER(sig);
deepStrictEqual(r.toString(16), v.r, 'r equality');
deepStrictEqual(s.toString(16), v.s, 's equality');
deepStrictEqual(starknet.verify(sig, v.msg, publicKey), true, 'verify');
}
});
should('Invalid signatures', () => {
/*
it('should not verify invalid signature inputs lengths', () => {
const ecOrder = starkwareCrypto.ec.n;
const {maxEcdsaVal} = starkwareCrypto;
const maxMsgHash = maxEcdsaVal.sub(oneBn);
const maxR = maxEcdsaVal.sub(oneBn);
const maxS = ecOrder.sub(oneBn).sub(oneBn);
const maxStarkKey = maxEcdsaVal.sub(oneBn);
// Test invalid message length.
expect(() =>
starkwareCrypto.verify(maxStarkKey, maxMsgHash.add(oneBn).toString(16), {
r: maxR,
s: maxS
})
).to.throw('Message not signable, invalid msgHash length.');
// Test invalid r length.
expect(() =>
starkwareCrypto.verify(maxStarkKey, maxMsgHash.toString(16), {
r: maxR.add(oneBn),
s: maxS
})
).to.throw('Message not signable, invalid r length.');
// Test invalid w length.
expect(() =>
starkwareCrypto.verify(maxStarkKey, maxMsgHash.toString(16), {
r: maxR,
s: maxS.add(oneBn)
})
).to.throw('Message not signable, invalid w length.');
// Test invalid s length.
expect(() =>
starkwareCrypto.verify(maxStarkKey, maxMsgHash.toString(16), {
r: maxR,
s: maxS.add(oneBn).add(oneBn)
})
).to.throw('Message not signable, invalid s length.');
});
it('should not verify invalid signatures', () => {
const privKey = generateRandomStarkPrivateKey();
const keyPair = starkwareCrypto.ec.keyFromPrivate(privKey, 'hex');
const keyPairPub = starkwareCrypto.ec.keyFromPublic(
keyPair.getPublic(),
'BN'
);
const msgHash = new BN(randomHexString(61));
const msgSignature = starkwareCrypto.sign(keyPair, msgHash);
// Test invalid public key.
const invalidKeyPairPub = starkwareCrypto.ec.keyFromPublic(
{x: keyPairPub.pub.getX().add(oneBn), y: keyPairPub.pub.getY()},
'BN'
);
expect(
starkwareCrypto.verify(
invalidKeyPairPub,
msgHash.toString(16),
msgSignature
)
).to.be.false;
// Test invalid message.
expect(
starkwareCrypto.verify(
keyPair,
msgHash.add(oneBn).toString(16),
msgSignature
)
).to.be.false;
expect(
starkwareCrypto.verify(
keyPairPub,
msgHash.add(oneBn).toString(16),
msgSignature
)
).to.be.false;
// Test invalid r.
msgSignature.r.iadd(oneBn);
expect(starkwareCrypto.verify(keyPair, msgHash.toString(16), msgSignature))
.to.be.false;
expect(
starkwareCrypto.verify(keyPairPub, msgHash.toString(16), msgSignature)
).to.be.false;
// Test invalid s.
msgSignature.r.isub(oneBn);
msgSignature.s.iadd(oneBn);
expect(starkwareCrypto.verify(keyPair, msgHash.toString(16), msgSignature))
.to.be.false;
expect(
starkwareCrypto.verify(keyPairPub, msgHash.toString(16), msgSignature)
).to.be.false;
});
});
*/
});
should('Pedersen', () => {
deepStrictEqual(
starknet.pedersen(
'0x3d937c035c878245caf64531a5756109c53068da139362728feb561405371cb',
'0x208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a'
),
'0x30e480bed5fe53fa909cc0f8c4d99b8f9f2c016be4c41e13a4848797979c662'
);
deepStrictEqual(
starknet.pedersen(
'0x58f580910a6ca59b28927c08fe6c43e2e303ca384badc365795fc645d479d45',
'0x78734f65a067be9bdb39de18434d71e79f7b6466a4b66bbd979ab9e7515fe0b'
),
'0x68cc0b76cddd1dd4ed2301ada9b7c872b23875d5ff837b3a87993e0d9996b87'
);
});
should('Hash chain', () => {
deepStrictEqual(starknet.hashChain([1, 2, 3]), starknet.pedersen(1, starknet.pedersen(2, 3)));
});
should('Key grinding', () => {
deepStrictEqual(
starknet.grindKey('86F3E7293141F20A8BAFF320E8EE4ACCB9D4A4BF2B4D295E8CEE784DB46E0519'),
'5c8c8683596c732541a59e03007b2d30dbbbb873556fe65b5fb63c16688f941'
);
// Loops more than once (verified manually)
deepStrictEqual(
starknet.grindKey('94F3E7293141F20A8BAFF320E8EE4ACCB9D4A4BF2B4D295E8CEE784DB46E0595'),
'33880b9aba464c1c01c9f8f5b4fc1134698f9b0a8d18505cab6cdd34d93dc02'
);
});
should('Private to stark key', () => {
deepStrictEqual(
starknet.getStarkKey('0x178047D3869489C055D7EA54C014FFB834A069C9595186ABE04EA4D1223A03F'),
'0x1895a6a77ae14e7987b9cb51329a5adfb17bd8e7c638f92d6892d76e51cebcf'
);
for (const [privKey, expectedPubKey] of Object.entries(precomputedKeys)) {
deepStrictEqual(starknet.getStarkKey(privKey), expectedPubKey);
}
});
should('Private stark key from eth signature', () => {
const ethSignature =
'0x21fbf0696d5e0aa2ef41a2b4ffb623bcaf070461d61cf7251c74161f82fec3a43' +
'70854bc0a34b3ab487c1bc021cd318c734c51ae29374f2beb0e6f2dd49b4bf41c';
deepStrictEqual(
starknet.ethSigToPrivate(ethSignature),
'766f11e90cd7c7b43085b56da35c781f8c067ac0d578eabdceebc4886435bda'
);
});
should('Key derivation', () => {
const layer = 'starkex';
const application = 'starkdeployement';
const mnemonic =
'range mountain blast problem vibrant void vivid doctor cluster enough melody ' +
'salt layer language laptop boat major space monkey unit glimpse pause change vibrant';
const ethAddress = '0xa4864d977b944315389d1765ffa7e66F74ee8cd7';
const VECTORS = [
{
index: 0,
path: "m/2645'/579218131'/891216374'/1961790679'/2135936222'/0",
privateKey: '6cf0a8bf113352eb863157a45c5e5567abb34f8d32cddafd2c22aa803f4892c',
},
{
index: 7,
path: "m/2645'/579218131'/891216374'/1961790679'/2135936222'/7",
privateKey: '341751bdc42841da35ab74d13a1372c1f0250617e8a2ef96034d9f46e6847af',
},
{
index: 598,
path: "m/2645'/579218131'/891216374'/1961790679'/2135936222'/598",
privateKey: '41a4d591a868353d28b7947eb132aa4d00c4a022743689ffd20a3628d6ca28c',
},
];
const hd = bip32.HDKey.fromMasterSeed(bip39.mnemonicToSeedSync(mnemonic));
for (const { index, path, privateKey } of VECTORS) {
const realPath = starknet.getAccountPath(layer, application, ethAddress, index);
deepStrictEqual(realPath, path);
deepStrictEqual(starknet.grindKey(hd.derive(realPath).privateKey), privateKey);
}
});
// Verified against starknet.js
should('Starknet.js cross-tests', () => {
const privateKey = '0x019800ea6a9a73f94aee6a3d2edf018fc770443e90c7ba121e8303ec6b349279';
// NOTE: there is no compressed keys here, getPubKey returns stark-key (which is schnorr-like X coordinate)
// But it is not used in signing/verifying
deepStrictEqual(
starknet.getStarkKey(privateKey),
'0x33f45f07e1bd1a51b45fc24ec8c8c9908db9e42191be9e169bfcac0c0d99745'
);
const msgHash = '0x6d1706bd3d1ba7c517be2a2a335996f63d4738e2f182144d078a1dd9997062e';
const sig = starknet.sign(msgHash, privateKey);
const { r, s } = (sig);
deepStrictEqual(
r.toString(),
'1427981024487605678086498726488552139932400435436186597196374630267616399345'
);
deepStrictEqual(
s.toString(),
'1853664302719670721837677288395394946745467311923401353018029119631574115563'
);
const hashMsg2 = starknet.pedersen(
'0x33f45f07e1bd1a51b45fc24ec8c8c9908db9e42191be9e169bfcac0c0d99745',
'1'
);
deepStrictEqual(hashMsg2, '0x2b0d4d43acce8ff68416f667f92ec7eab2b96f1d2224abd4d9d4d1e7fa4bb00');
const pubKey =
'04033f45f07e1bd1a51b45fc24ec8c8c9908db9e42191be9e169bfcac0c0d997450319d0f53f6ca077c4fa5207819144a2a4165daef6ee47a7c1d06c0dcaa3e456';
const sig2 = new starknet.Signature(
558858382392827003930138586379728730695763862039474863361948210004201119180n,
2440689354481625417078677634625227600823892606910345662891037256374285369343n
);
deepStrictEqual(starknet.verify(sig2.toDERHex(), hashMsg2, pubKey), true);
});
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run();
}

1080
test/vectors/poseidon.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
{ {
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"outDir": "lib/esm", "outDir": "esm",
"target": "es2020", "target": "es2020",
"module": "es6", "module": "es6",
"moduleResolution": "node16", "moduleResolution": "node16",
"noUnusedLocals": true, "noUnusedLocals": true,
"sourceMap": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@noble/hashes/crypto": [ "src/crypto" ] "@noble/hashes/crypto": [ "src/crypto" ]

View File

@@ -2,7 +2,9 @@
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"declaration": true, "declaration": true,
"outDir": "lib", "declarationMap": true,
"sourceMap": true,
"outDir": ".",
"target": "es2020", "target": "es2020",
"lib": ["es2020"], // Set explicitly to remove DOM "lib": ["es2020"], // Set explicitly to remove DOM
"module": "commonjs", "module": "commonjs",