Compare commits
181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ffa38db6b | ||
|
|
c4c580edc0 | ||
|
|
abe8adac7b | ||
|
|
4fd2ae82b6 | ||
|
|
e2411f7dfd | ||
|
|
cb61e4f292 | ||
|
|
bb875791bd | ||
|
|
3df2553ced | ||
|
|
8fabc7ff06 | ||
|
|
f3c21eb347 | ||
|
|
a8b8192714 | ||
|
|
1c6aa07ff7 | ||
|
|
e110237298 | ||
|
|
45393db807 | ||
|
|
acc3a9dc4d | ||
|
|
9295b0dbae | ||
|
|
5784ef23f6 | ||
|
|
ef55efe842 | ||
|
|
1cfd6a76ca | ||
|
|
89f81b2204 | ||
|
|
d77ac16f51 | ||
|
|
fe68da61f6 | ||
|
|
32c0841bed | ||
|
|
49a659b248 | ||
|
|
9d0a2e25dc | ||
|
|
7c461af2b2 | ||
|
|
4a8f447c8d | ||
|
|
4b2d31ce7f | ||
|
|
16115f27a6 | ||
|
|
0e0d0f530d | ||
|
|
fa5105aef2 | ||
|
|
11f1626ecc | ||
|
|
53ff287bf7 | ||
|
|
214c9aa553 | ||
|
|
ec2c3e1248 | ||
|
|
e64a9d654c | ||
|
|
088edd0fbb | ||
|
|
3e90930e9d | ||
|
|
b8b2e91f74 | ||
|
|
9ee694ae23 | ||
|
|
6bc4b35cf4 | ||
|
|
0163b63532 | ||
|
|
7e825520f1 | ||
|
|
d739297b2c | ||
|
|
285aa6375d | ||
|
|
8c77331ef2 | ||
|
|
669641e0a3 | ||
|
|
68dd57ed31 | ||
|
|
a9fdd6df9f | ||
|
|
d485d8b0e6 | ||
|
|
0fdd763dc7 | ||
|
|
586e2ad5fb | ||
|
|
ed81707bdc | ||
|
|
6d56b2d78e | ||
|
|
8397241a8f | ||
|
|
001d0cc24a | ||
|
|
ce9d165657 | ||
|
|
2902b0299a | ||
|
|
e1cb8549e8 | ||
|
|
26ebb5dcce | ||
|
|
8b2863aeac | ||
|
|
b1f50d9364 | ||
|
|
b81d74d3cb | ||
|
|
d5fe537159 | ||
|
|
cde1d5c488 | ||
|
|
3486bbf6b8 | ||
|
|
0d7a8296c5 | ||
|
|
0f1e7a5a43 | ||
|
|
3da48cf899 | ||
|
|
4ec46dd65d | ||
|
|
7073f63c6b | ||
|
|
80966cbd03 | ||
|
|
98ea15dca4 | ||
|
|
e1910e85ea | ||
|
|
4d311d7294 | ||
|
|
c36d90cae6 | ||
|
|
af5aa8424f | ||
|
|
67b99652fc | ||
|
|
c8d292976b | ||
|
|
daffaa2339 | ||
|
|
a462fc5779 | ||
|
|
fe3491c5aa | ||
|
|
c0877ba69a | ||
|
|
8e449cc78c | ||
|
|
1b6071cabd | ||
|
|
debb9d9709 | ||
|
|
d2c6459756 | ||
|
|
47533b6336 | ||
|
|
00b73b68d3 | ||
|
|
cef4b52d12 | ||
|
|
47ce547dcf | ||
|
|
e2a7594eae | ||
|
|
823149ecd9 | ||
|
|
e57aec63d8 | ||
|
|
837aca98c9 | ||
|
|
dbb16b0e5e | ||
|
|
e14af67254 | ||
|
|
4780850748 | ||
|
|
3374a70f47 | ||
|
|
131f88b504 | ||
|
|
4333e9a686 | ||
|
|
a60d15ff05 | ||
|
|
ceffbc69da | ||
|
|
c75129e629 | ||
|
|
f39fb80c52 | ||
|
|
fcd422d246 | ||
|
|
ed9bf89038 | ||
|
|
7262b4219f | ||
|
|
02b0b25147 | ||
|
|
79100c2d47 | ||
|
|
4ef2cad685 | ||
|
|
69b3ab5a57 | ||
|
|
9465e60d30 | ||
|
|
0fb78b7097 | ||
|
|
be0b2a32a5 | ||
|
|
3d77422731 | ||
|
|
c46914f1bc | ||
|
|
f250f355e8 | ||
|
|
c095d74673 | ||
|
|
ac52fea952 | ||
|
|
f2ee24bee4 | ||
|
|
cffea91061 | ||
|
|
5fc38fc0e7 | ||
|
|
849dc38f3c | ||
|
|
0422e6ef38 | ||
|
|
21d2438a33 | ||
|
|
cea4696599 | ||
|
|
f14b8d2be5 | ||
|
|
2ed27da8eb | ||
|
|
17e5be5f1b | ||
|
|
a49f0d266e | ||
|
|
bfbcf733e6 | ||
|
|
7fda6de619 | ||
|
|
2b908ad602 | ||
|
|
ceb3f67faa | ||
|
|
a2c87f9c2f | ||
|
|
e1fd346279 | ||
|
|
11e78aadbf | ||
|
|
055147f1be | ||
|
|
6f99f6042e | ||
|
|
1e47bf2372 | ||
|
|
40530eae0c | ||
|
|
b9482bb17d | ||
|
|
74475dca68 | ||
|
|
f4cf21b9c8 | ||
|
|
5312d92b2c | ||
|
|
d1770c0ac7 | ||
|
|
2d37edf7d1 | ||
|
|
36998fede8 | ||
|
|
83960d445d | ||
|
|
23cc2aa5d1 | ||
|
|
e45d7c2d25 | ||
|
|
bfe929aac3 | ||
|
|
069452dbe7 | ||
|
|
2e81f31d2e | ||
|
|
9f7df0f13b | ||
|
|
5600629bca | ||
|
|
2bd5e9ac16 | ||
|
|
6890c26091 | ||
|
|
a15e3a93a9 | ||
|
|
910c508da9 | ||
|
|
12da04a2bb | ||
|
|
cc2c84f040 | ||
|
|
5d42549acc | ||
|
|
65d7256b9e | ||
|
|
d77a98a7aa | ||
|
|
1bfab42620 | ||
|
|
f1ab259941 | ||
|
|
242ee620c5 | ||
|
|
d837831d22 | ||
|
|
cae888d942 | ||
|
|
1ab77b95dd | ||
|
|
8b5819b12d | ||
|
|
4b5560ab4b | ||
|
|
ba121ff24c | ||
|
|
0277c01efd | ||
|
|
6ffe656871 | ||
|
|
135e69bd7b | ||
|
|
7a34c16c2b | ||
|
|
458cddcc7f | ||
|
|
ccfb8695d5 |
3
.github/workflows/nodejs.yml
vendored
3
.github/workflows/nodejs.yml
vendored
@@ -13,6 +13,5 @@ jobs:
|
|||||||
node-version: 18
|
node-version: 18
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build --if-present
|
- run: npm run build --if-present
|
||||||
- run: cd curve-definitions; npm install; npm run build --if-present
|
|
||||||
- run: npm test
|
|
||||||
- run: npm run lint --if-present
|
- run: npm run lint --if-present
|
||||||
|
- run: npm test
|
||||||
|
|||||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -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
6
.vscode/settings.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
18
SECURITY.md
Normal file
18
SECURITY.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| >=1.0.0 | :white_check_mark: |
|
||||||
|
| <1.0.0 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Use maintainer's email specified at https://github.com/paulmillr.
|
||||||
|
|
||||||
|
It's preferred that you use
|
||||||
|
PGP key from [pgp proof](https://paulmillr.com/pgp_proof.txt) (current is [697079DA6878B89B](https://paulmillr.com/pgp_proof.txt)).
|
||||||
|
Ensure the pgp proof page has maintainer's site/github specified.
|
||||||
|
|
||||||
|
You will get an update as soon as the email is read; a "Security vulnerability" phrase in email's title would help.
|
||||||
BIN
audit/2023-01-trailofbits-audit-curves.pdf
Normal file
BIN
audit/2023-01-trailofbits-audit-curves.pdf
Normal file
Binary file not shown.
11
audit/README.md
Normal file
11
audit/README.md
Normal 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
7
benchmark/_shared.js
Normal 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
52
benchmark/bls.js
Normal 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
23
benchmark/curves.js
Normal 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
19
benchmark/ecdh.js
Normal 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);
|
||||||
|
});
|
||||||
29
benchmark/hash-to-curve.js
Normal file
29
benchmark/hash-to-curve.js
Normal 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));
|
||||||
|
}
|
||||||
|
});
|
||||||
13
benchmark/modular.js
Normal file
13
benchmark/modular.js
Normal 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))
|
||||||
|
});
|
||||||
21
benchmark/package.json
Normal file
21
benchmark/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"micro-bmark": "0.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "^1.1.5",
|
||||||
|
"elliptic": "^6.5.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
benchmark/secp256k1.js
Normal file
22
benchmark/secp256k1.js
Normal 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();
|
||||||
|
});
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2022 Paul Miller (https://paulmillr.com)
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the “Software”), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# micro-curve-definitions
|
|
||||||
|
|
||||||
Elliptic curves implementations. `@noble/curves` is zero-dependency library for internal arithmetics.
|
|
||||||
|
|
||||||
`micro-curve-definitions` is the actual implementations. Current functionality:
|
|
||||||
|
|
||||||
- NIST curves: P192, P224, P256, P384, P521 (ECDSA)
|
|
||||||
- secp256k1 (ECDSA, without Schnorr)
|
|
||||||
- stark curve
|
|
||||||
- bn254
|
|
||||||
|
|
||||||
Pairings are not implemented.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install micro-curve-definitions
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import * as nist from 'micro-curve-definitions';
|
|
||||||
|
|
||||||
// P192, P224, P256, P384, P521, bn254
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT (c) Paul Miller [(https://paulmillr.com)](https://paulmillr.com), see LICENSE file.
|
|
||||||
@@ -1,397 +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),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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),
|
|
||||||
},
|
|
||||||
// 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)),
|
|
||||||
},
|
|
||||||
'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'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
@@ -1,23 +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": {
|
|
||||||
"micro-bmark": "0.2.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@noble/bls12-381": "^1.4.0",
|
|
||||||
"@noble/ed25519": "^1.7.1",
|
|
||||||
"@noble/secp256k1": "^1.7.0",
|
|
||||||
"@starkware-industries/starkware-crypto-utils": "^0.0.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "micro-curve-definitions",
|
|
||||||
"version": "0.4.0",
|
|
||||||
"description": "Curve definitions for @noble/curves",
|
|
||||||
"files": [
|
|
||||||
"lib"
|
|
||||||
],
|
|
||||||
"main": "lib/index.js",
|
|
||||||
"module": "lib/index.js",
|
|
||||||
"types": "lib/index.d.ts",
|
|
||||||
"dependencies": {
|
|
||||||
"@noble/curves": "0.4.0",
|
|
||||||
"@noble/hashes": "1.1.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@scure/base": "~1.1.0",
|
|
||||||
"@scure/bip32": "^1.1.1",
|
|
||||||
"@scure/bip39": "^1.1.0",
|
|
||||||
"@types/node": "18.11.3",
|
|
||||||
"fast-check": "3.0.0",
|
|
||||||
"micro-should": "0.2.0",
|
|
||||||
"prettier": "2.6.2",
|
|
||||||
"typescript": "4.7.3"
|
|
||||||
},
|
|
||||||
"author": "Paul Miller (https://paulmillr.com)",
|
|
||||||
"license": "MIT",
|
|
||||||
"homepage": "https://github.com/paulmillr/noble-curves",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/paulmillr/noble-curves.git"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc && tsc -p tsconfig.esm.json",
|
|
||||||
"lint": "prettier --check src",
|
|
||||||
"test": "node test/index.test.js"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"secp192r1",
|
|
||||||
"secp224r1",
|
|
||||||
"secp256r1",
|
|
||||||
"secp384r1",
|
|
||||||
"secp521r1",
|
|
||||||
"NIST P192",
|
|
||||||
"NIST P224",
|
|
||||||
"NIST P256",
|
|
||||||
"NIST P384",
|
|
||||||
"NIST P521",
|
|
||||||
"NIST curves",
|
|
||||||
"EC",
|
|
||||||
"elliptic curves"
|
|
||||||
],
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
||||||
import { shake256 } from '@noble/hashes/sha3';
|
|
||||||
import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/hashes/utils';
|
|
||||||
import { twistedEdwards } from '@noble/curves/edwards';
|
|
||||||
import { mod, pow2, Fp } from '@noble/curves/modular';
|
|
||||||
import { montgomery } from '../../lib/montgomery.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edwards448 (not Ed448-Goldilocks) curve with following addons:
|
|
||||||
* * X448 ECDH
|
|
||||||
* Conforms to RFC 8032 https://www.rfc-editor.org/rfc/rfc8032.html#section-5.2
|
|
||||||
*/
|
|
||||||
|
|
||||||
const shake256_114 = wrapConstructor(() => shake256.create({ dkLen: 114 }));
|
|
||||||
const shake256_64 = wrapConstructor(() => shake256.create({ dkLen: 64 }));
|
|
||||||
const ed448P = BigInt(
|
|
||||||
'726838724295606890549323807888004534353641360687318060281490199180612328166730772686396383698676545930088884461843637361053498018365439'
|
|
||||||
);
|
|
||||||
|
|
||||||
// powPminus3div4 calculates z = x^k mod p, where k = (p-3)/4.
|
|
||||||
function ed448_pow_Pminus3div4(x: bigint): bigint {
|
|
||||||
const P = ed448P;
|
|
||||||
// prettier-ignore
|
|
||||||
let [_1n, _2n, _3n, _11n, _22n, _44n, _88n, _223n] = [1, 2, 3, 11, 22, 44, 88, 223]
|
|
||||||
.map(n => BigInt(n));
|
|
||||||
// x ** ((P - 3n)/4n) % P
|
|
||||||
// [223 of 1, 0, 222 of 1], almost same as secp!
|
|
||||||
const b2 = (x * x * x) % P;
|
|
||||||
const b3 = (b2 * b2 * x) % P;
|
|
||||||
const b6 = (pow2(b3, _3n, P) * b3) % P;
|
|
||||||
const b9 = (pow2(b6, _3n, P) * b3) % P;
|
|
||||||
const b11 = (pow2(b9, _2n, P) * b2) % P;
|
|
||||||
const b22 = (pow2(b11, _11n, P) * b11) % P;
|
|
||||||
const b44 = (pow2(b22, _22n, P) * b22) % P;
|
|
||||||
const b88 = (pow2(b44, _44n, P) * b44) % P;
|
|
||||||
const b176 = (pow2(b88, _88n, P) * b88) % P;
|
|
||||||
const b220 = (pow2(b176, _44n, P) * b44) % P;
|
|
||||||
const b222 = (pow2(b220, _2n, P) * b2) % P;
|
|
||||||
const b223 = (pow2(b222, _1n, P) * x) % P;
|
|
||||||
return (pow2(b223, _223n, P) * b222) % P;
|
|
||||||
}
|
|
||||||
|
|
||||||
function adjustScalarBytes(bytes: Uint8Array): Uint8Array {
|
|
||||||
// Section 5: Likewise, for X448, set the two least significant bits of the first byte to 0, and the most
|
|
||||||
// significant bit of the last byte to 1.
|
|
||||||
bytes[0] &= 252; // 0b11111100
|
|
||||||
// and the most significant bit of the last byte to 1.
|
|
||||||
bytes[55] |= 128; // 0b10000000
|
|
||||||
// NOTE: is is NOOP for 56 bytes scalars (X25519/X448)
|
|
||||||
bytes[56] = 0; // Byte outside of group (456 buts vs 448 bits)
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ED448_DEF = {
|
|
||||||
// Param: a
|
|
||||||
a: BigInt(1),
|
|
||||||
// -39081. Negative number is P - number
|
|
||||||
d: BigInt(
|
|
||||||
'726838724295606890549323807888004534353641360687318060281490199180612328166730772686396383698676545930088884461843637361053498018326358'
|
|
||||||
),
|
|
||||||
// Finite field 𝔽p over which we'll do calculations; 2n ** 448n - 2n ** 224n - 1n
|
|
||||||
Fp: Fp(ed448P, 456),
|
|
||||||
// Subgroup order: how many points ed448 has; 2n**446n - 13818066809895115352007386748515426880336692474882178609894547503885n
|
|
||||||
n: BigInt(
|
|
||||||
'181709681073901722637330951972001133588410340171829515070372549795146003961539585716195755291692375963310293709091662304773755859649779'
|
|
||||||
),
|
|
||||||
nBitLength: 456,
|
|
||||||
// Cofactor
|
|
||||||
h: BigInt(4),
|
|
||||||
// Base point (x, y) aka generator point
|
|
||||||
Gx: BigInt(
|
|
||||||
'224580040295924300187604334099896036246789641632564134246125461686950415467406032909029192869357953282578032075146446173674602635247710'
|
|
||||||
),
|
|
||||||
Gy: BigInt(
|
|
||||||
'298819210078481492676017930443930673437544040154080242095928241372331506189835876003536878655418784733982303233503462500531545062832660'
|
|
||||||
),
|
|
||||||
// SHAKE256(dom4(phflag,context)||x, 114)
|
|
||||||
hash: shake256_114,
|
|
||||||
randomBytes,
|
|
||||||
adjustScalarBytes,
|
|
||||||
// dom4
|
|
||||||
domain: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => {
|
|
||||||
if (ctx.length > 255) throw new Error(`Context is too big: ${ctx.length}`);
|
|
||||||
return concatBytes(
|
|
||||||
utf8ToBytes('SigEd448'),
|
|
||||||
new Uint8Array([phflag ? 1 : 0, ctx.length]),
|
|
||||||
ctx,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
},
|
|
||||||
// Constant-time ratio of u to v. Allows to combine inversion and square root u/√v.
|
|
||||||
// Uses algo from RFC8032 5.1.3.
|
|
||||||
uvRatio: (u: bigint, v: bigint): { isValid: boolean; value: bigint } => {
|
|
||||||
const P = ed448P;
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc8032#section-5.2.3
|
|
||||||
// To compute the square root of (u/v), the first step is to compute the
|
|
||||||
// candidate root x = (u/v)^((p+1)/4). This can be done using the
|
|
||||||
// following trick, to use a single modular powering for both the
|
|
||||||
// inversion of v and the square root:
|
|
||||||
// (p+1)/4 3 (p-3)/4
|
|
||||||
// x = (u/v) = u v (u^5 v^3) (mod p)
|
|
||||||
const u2v = mod(u * u * v, P);
|
|
||||||
const u3v = mod(u2v * u, P); // u^2v
|
|
||||||
const u5v3 = mod(u3v * u2v * v, P); // u^5v^3
|
|
||||||
const root = ed448_pow_Pminus3div4(u5v3);
|
|
||||||
const x = mod(u3v * root, P);
|
|
||||||
// Verify that root is exists
|
|
||||||
const x2 = mod(x * x, P); // x^2
|
|
||||||
// If v * x^2 = u, the recovered x-coordinate is x. Otherwise, no
|
|
||||||
// square root exists, and the decoding fails.
|
|
||||||
return { isValid: mod(x2 * v, P) === u, value: x };
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const ed448 = twistedEdwards(ED448_DEF);
|
|
||||||
// NOTE: there is no ed448ctx, since ed448 supports ctx by default
|
|
||||||
export const ed448ph = twistedEdwards({ ...ED448_DEF, preHash: shake256_64 });
|
|
||||||
|
|
||||||
export const x448 = montgomery({
|
|
||||||
a24: BigInt(39081),
|
|
||||||
montgomeryBits: 448,
|
|
||||||
nByteLength: 57,
|
|
||||||
P: ed448P,
|
|
||||||
Gu: '0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
|
|
||||||
powPminus2: (x: bigint): bigint => {
|
|
||||||
const P = ed448P;
|
|
||||||
const Pminus3div4 = ed448_pow_Pminus3div4(x);
|
|
||||||
const Pminus3 = pow2(Pminus3div4, BigInt(2), P);
|
|
||||||
return mod(Pminus3 * x, P); // Pminus3 * x = Pminus2
|
|
||||||
},
|
|
||||||
adjustScalarBytes,
|
|
||||||
// The 4-isogeny maps between the Montgomery curve and this Edwards
|
|
||||||
// curve are:
|
|
||||||
// (u, v) = (y^2/x^2, (2 - x^2 - y^2)*y/x^3)
|
|
||||||
// (x, y) = (4*v*(u^2 - 1)/(u^4 - 2*u^2 + 4*v^2 + 1),
|
|
||||||
// -(u^5 - 2*u^3 - 4*u*v^2 + u)/
|
|
||||||
// (u^5 - 2*u^2*v^2 - 2*u^3 - 2*v^2 + u))
|
|
||||||
// xyToU: (p: PointType) => {
|
|
||||||
// const P = ed448P;
|
|
||||||
// const { x, y } = p;
|
|
||||||
// if (x === _0n) throw new Error(`Point with x=0 doesn't have mapping`);
|
|
||||||
// const invX = invert(x * x, P); // x^2
|
|
||||||
// const u = mod(y * y * invX, P); // (y^2/x^2)
|
|
||||||
// return numberToBytesLE(u, 56);
|
|
||||||
// },
|
|
||||||
});
|
|
||||||
@@ -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 '@noble/curves/modular';
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
@@ -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 '@noble/curves/modular';
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
sha256 // TODO: replace with sha224 when new @noble/hashes released
|
|
||||||
);
|
|
||||||
export const secp224r1 = P224;
|
|
||||||
@@ -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 '@noble/curves/modular';
|
|
||||||
|
|
||||||
// NIST secp256r1 aka P256
|
|
||||||
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-256
|
|
||||||
export const P256 = createCurve(
|
|
||||||
{
|
|
||||||
// Params: a, b
|
|
||||||
a: BigInt('0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc'),
|
|
||||||
b: BigInt('0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b'),
|
|
||||||
// Field over which we'll do calculations; 2n**224n * (2n**32n-1n) + 2n**192n + 2n**96n-1n
|
|
||||||
Fp: Fp(BigInt('0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff')),
|
|
||||||
// Curve order, total count of valid points in the field
|
|
||||||
n: BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551'),
|
|
||||||
// Base point (x, y) aka generator point
|
|
||||||
Gx: BigInt('0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296'),
|
|
||||||
Gy: BigInt('0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5'),
|
|
||||||
h: BigInt(1),
|
|
||||||
lowS: false,
|
|
||||||
} as const,
|
|
||||||
sha256
|
|
||||||
);
|
|
||||||
export const secp256r1 = P256;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
||||||
import { createCurve } from './_shortw_utils.js';
|
|
||||||
import { sha384 } from '@noble/hashes/sha512';
|
|
||||||
import { Fp } from '@noble/curves/modular';
|
|
||||||
|
|
||||||
// NIST secp384r1 aka P384
|
|
||||||
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-384
|
|
||||||
// prettier-ignore
|
|
||||||
export const P384 = createCurve({
|
|
||||||
// Params: a, b
|
|
||||||
a: BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc'),
|
|
||||||
b: BigInt('0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef'),
|
|
||||||
// Field over which we'll do calculations. 2n**384n - 2n**128n - 2n**96n + 2n**32n - 1n
|
|
||||||
Fp: Fp(BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff')),
|
|
||||||
// Curve order, total count of valid points in the field.
|
|
||||||
n: BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973'),
|
|
||||||
// Base point (x, y) aka generator point
|
|
||||||
Gx: BigInt('0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7'),
|
|
||||||
Gy: BigInt('0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f'),
|
|
||||||
h: BigInt(1),
|
|
||||||
lowS: false,
|
|
||||||
} as const, sha384);
|
|
||||||
export const secp384r1 = P384;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
||||||
import { createCurve } from './_shortw_utils.js';
|
|
||||||
import { Fp } from '@noble/curves/modular';
|
|
||||||
import { sha512 } from '@noble/hashes/sha512';
|
|
||||||
import { bytesToHex, PrivKey } from '@noble/curves/utils';
|
|
||||||
|
|
||||||
// NIST secp521r1 aka P521
|
|
||||||
// Note that it's 521, which differs from 512 of its hash function.
|
|
||||||
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-521
|
|
||||||
// prettier-ignore
|
|
||||||
export const P521 = createCurve({
|
|
||||||
// Params: a, b
|
|
||||||
a: BigInt('0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc'),
|
|
||||||
b: BigInt('0x0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00'),
|
|
||||||
// Field over which we'll do calculations; 2n**521n - 1n
|
|
||||||
Fp: Fp(BigInt('0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')),
|
|
||||||
// Curve order, total count of valid points in the field
|
|
||||||
n: BigInt('0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409'),
|
|
||||||
// Base point (x, y) aka generator point
|
|
||||||
Gx: BigInt('0x00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66'),
|
|
||||||
Gy: BigInt('0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650'),
|
|
||||||
h: BigInt(1),
|
|
||||||
lowS: false,
|
|
||||||
normalizePrivateKey(key: PrivKey) {
|
|
||||||
if (typeof key === 'bigint') return key;
|
|
||||||
if (key instanceof Uint8Array) key = bytesToHex(key);
|
|
||||||
if (typeof key !== 'string' || !([130, 131, 132].includes(key.length))) {
|
|
||||||
throw new Error('Invalid key');
|
|
||||||
}
|
|
||||||
return key.padStart(66 * 2, '0');
|
|
||||||
}
|
|
||||||
} as const, sha512);
|
|
||||||
export const secp521r1 = P521;
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
||||||
import { sha256 } from '@noble/hashes/sha256';
|
|
||||||
import { Fp as FpFn, mod, pow2 } from '@noble/curves/modular';
|
|
||||||
import { createCurve } from './_shortw_utils.js';
|
|
||||||
import { PointType } from '@noble/curves/weierstrass';
|
|
||||||
import {
|
|
||||||
ensureBytes,
|
|
||||||
concatBytes,
|
|
||||||
Hex,
|
|
||||||
hexToBytes,
|
|
||||||
bytesToNumberBE,
|
|
||||||
PrivKey,
|
|
||||||
} from '@noble/curves/utils';
|
|
||||||
import { randomBytes } from '@noble/hashes/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* secp256k1 belongs to Koblitz curves: it has
|
|
||||||
* efficiently computable Frobenius endomorphism.
|
|
||||||
* Endomorphism improves efficiency:
|
|
||||||
* Uses 2x less RAM, speeds up precomputation by 2x and ECDH / sign key recovery by 20%.
|
|
||||||
* Should always be used for Jacobian's double-and-add multiplication.
|
|
||||||
* 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 secp256k1N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141');
|
|
||||||
const _1n = BigInt(1);
|
|
||||||
const _2n = BigInt(2);
|
|
||||||
const divNearest = (a: bigint, b: bigint) => (a + b / _2n) / b;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows to compute square root √y 2x faster.
|
|
||||||
* 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]
|
|
||||||
*/
|
|
||||||
// prettier-ignore
|
|
||||||
function sqrtMod(y: bigint): bigint {
|
|
||||||
const P = secp256k1P;
|
|
||||||
const _3n = BigInt(3), _6n = BigInt(6), _11n = BigInt(11); const _22n = BigInt(22);
|
|
||||||
const _23n = BigInt(23), _44n = BigInt(44), _88n = BigInt(88);
|
|
||||||
const b2 = (y * y * y) % P; // x^3, 11
|
|
||||||
const b3 = (b2 * b2 * y) % P; // x^7
|
|
||||||
const b6 = (pow2(b3, _3n, P) * b3) % P;
|
|
||||||
const b9 = (pow2(b6, _3n, P) * b3) % P;
|
|
||||||
const b11 = (pow2(b9, _2n, P) * b2) % P;
|
|
||||||
const b22 = (pow2(b11, _11n, P) * b11) % P;
|
|
||||||
const b44 = (pow2(b22, _22n, P) * b22) % P;
|
|
||||||
const b88 = (pow2(b44, _44n, P) * b44) % P;
|
|
||||||
const b176 = (pow2(b88, _88n, P) * b88) % P;
|
|
||||||
const b220 = (pow2(b176, _44n, P) * b44) % P;
|
|
||||||
const b223 = (pow2(b220, _3n, P) * b3) % P;
|
|
||||||
const t1 = (pow2(b223, _23n, P) * b22) % P;
|
|
||||||
const t2 = (pow2(t1, _6n, P) * b2) % P;
|
|
||||||
return pow2(t2, _2n, P);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Fp = FpFn(secp256k1P, undefined, undefined, { sqrt: sqrtMod });
|
|
||||||
|
|
||||||
export const secp256k1 = createCurve(
|
|
||||||
{
|
|
||||||
// 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');
|
|
||||||
|
|
||||||
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
|
|
||||||
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) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
// Do we need that in schnorr at all?
|
|
||||||
return secp256k1.Point.fromHex(publicKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
if (bytes.length !== 64)
|
|
||||||
throw new TypeError(`SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}`);
|
|
||||||
const r = bytesToNumberBE(bytes.subarray(0, 32));
|
|
||||||
const s = bytesToNumberBE(bytes.subarray(32, 64));
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1,270 +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 { hmac } from '@noble/hashes/hmac';
|
|
||||||
import { concatBytes, randomBytes } from '@noble/hashes/utils';
|
|
||||||
import { weierstrass, JacobianPointType } from '@noble/curves/weierstrass';
|
|
||||||
import * as cutils from '@noble/curves/utils';
|
|
||||||
import { Fp } from '@noble/curves/modular';
|
|
||||||
import { getHash } from './_shortw_utils.js';
|
|
||||||
|
|
||||||
type JacobianPoint = JacobianPointType<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, JacobianPoint, Signature } = starkCurve;
|
|
||||||
export const utils = starkCurve.utils;
|
|
||||||
export {
|
|
||||||
CURVE,
|
|
||||||
Point,
|
|
||||||
Signature,
|
|
||||||
JacobianPoint,
|
|
||||||
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 limit = sha256mask - starkCurve.utils.mod(sha256mask, starkCurve.CURVE.n);
|
|
||||||
for (let i = 0; ; i++) {
|
|
||||||
const key = hashKeyWithIndex(_seed, i);
|
|
||||||
// key should be in [0, limit)
|
|
||||||
if (key < limit) return starkCurve.utils.mod(key, starkCurve.CURVE.n).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 = [
|
|
||||||
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_JACOBIAN = PEDERSEN_POINTS.map(JacobianPoint.fromAffine);
|
|
||||||
|
|
||||||
function pedersenPrecompute(p1: JacobianPoint, p2: JacobianPoint): JacobianPoint[] {
|
|
||||||
const out: JacobianPoint[] = [];
|
|
||||||
let p = p1;
|
|
||||||
for (let i = 0; i < 248; i++) {
|
|
||||||
out.push(p);
|
|
||||||
p = p.double();
|
|
||||||
}
|
|
||||||
p = p2;
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
out.push(p);
|
|
||||||
p = p.double();
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
const PEDERSEN_POINTS1 = pedersenPrecompute(
|
|
||||||
PEDERSEN_POINTS_JACOBIAN[1],
|
|
||||||
PEDERSEN_POINTS_JACOBIAN[2]
|
|
||||||
);
|
|
||||||
const PEDERSEN_POINTS2 = pedersenPrecompute(
|
|
||||||
PEDERSEN_POINTS_JACOBIAN[3],
|
|
||||||
PEDERSEN_POINTS_JACOBIAN[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: JacobianPoint, value: PedersenArg, constants: JacobianPoint[]) {
|
|
||||||
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: JacobianPoint = PEDERSEN_POINTS_JACOBIAN[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;
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
import { deepStrictEqual, throws } from 'assert';
|
|
||||||
import { should } from 'micro-should';
|
|
||||||
import * as fc from 'fast-check';
|
|
||||||
import * as mod from '@noble/curves/modular';
|
|
||||||
import { randomBytes } from '@noble/hashes/utils';
|
|
||||||
// Generic tests for all curves in package
|
|
||||||
import { secp192r1 } from '../lib/p192.js';
|
|
||||||
import { secp224r1 } from '../lib/p224.js';
|
|
||||||
import { secp256r1 } from '../lib/p256.js';
|
|
||||||
import { secp384r1 } from '../lib/p384.js';
|
|
||||||
import { secp521r1 } from '../lib/p521.js';
|
|
||||||
import { secp256k1 } from '../lib/secp256k1.js';
|
|
||||||
import { ed25519, ed25519ctx, ed25519ph } from '../lib/ed25519.js';
|
|
||||||
import { ed448, ed448ph } from '../lib/ed448.js';
|
|
||||||
import { starkCurve } from '../lib/stark.js';
|
|
||||||
import { pallas, vesta } from '../lib/pasta.js';
|
|
||||||
import { bn254 } from '../lib/bn.js';
|
|
||||||
import { jubjub } from '../lib/jubjub.js';
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const CURVES = {
|
|
||||||
secp192r1, secp224r1, secp256r1, secp384r1, secp521r1,
|
|
||||||
secp256k1,
|
|
||||||
ed25519, ed25519ctx, ed25519ph,
|
|
||||||
ed448, ed448ph,
|
|
||||||
starkCurve,
|
|
||||||
pallas, vesta,
|
|
||||||
bn254,
|
|
||||||
jubjub,
|
|
||||||
};
|
|
||||||
|
|
||||||
const NUM_RUNS = 5;
|
|
||||||
const getXY = (p) => ({ x: p.x, y: p.y });
|
|
||||||
|
|
||||||
function equal(a, b, comment) {
|
|
||||||
deepStrictEqual(a.equals(b), true, `eq(${comment})`);
|
|
||||||
if (a.toAffine && b.toAffine) {
|
|
||||||
deepStrictEqual(getXY(a.toAffine()), getXY(b.toAffine()), `eqToAffine(${comment})`);
|
|
||||||
} else if (!a.toAffine && !b.toAffine) {
|
|
||||||
// Already affine
|
|
||||||
deepStrictEqual(getXY(a), getXY(b), `eqAffine(${comment})`);
|
|
||||||
} else throw new Error('Different point types');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const name in CURVES) {
|
|
||||||
const C = CURVES[name];
|
|
||||||
const CURVE_ORDER = C.CURVE.n;
|
|
||||||
const FC_BIGINT = fc.bigInt(1n + 1n, CURVE_ORDER - 1n);
|
|
||||||
|
|
||||||
// Check that curve doesn't accept points from other curves
|
|
||||||
const O = name === 'secp256k1' ? secp256r1 : secp256k1;
|
|
||||||
const POINTS = {};
|
|
||||||
const OTHER_POINTS = {};
|
|
||||||
for (const name of ['Point', 'JacobianPoint', 'ExtendedPoint', 'ProjectivePoint']) {
|
|
||||||
POINTS[name] = C[name];
|
|
||||||
OTHER_POINTS[name] = O[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const pointName in POINTS) {
|
|
||||||
const p = POINTS[pointName];
|
|
||||||
const o = OTHER_POINTS[pointName];
|
|
||||||
if (!p) continue;
|
|
||||||
|
|
||||||
const G = [p.ZERO, p.BASE];
|
|
||||||
for (let i = 2; i < 10; i++) G.push(G[1].multiply(i));
|
|
||||||
// Here we check basic group laws, to verify that points works as group
|
|
||||||
should(`${name}/${pointName}/Basic group laws (zero)`, () => {
|
|
||||||
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].subtract(G[0]), G[0], '0*G - 0*G = 0');
|
|
||||||
equal(G[0].negate(), G[0], '-0 = 0');
|
|
||||||
for (let i = 0; i < G.length; i++) {
|
|
||||||
const p = G[i];
|
|
||||||
equal(p, p.add(G[0]), `${i}*G + 0 = ${i}*G`);
|
|
||||||
equal(G[0].multiply(i + 1), G[0], `${i + 1}*0 = 0`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should(`${name}/${pointName}/Basic group laws (one)`, () => {
|
|
||||||
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].add(G[1]), G[2], '1*G + 1*G = 2*G');
|
|
||||||
});
|
|
||||||
should(`${name}/${pointName}/Basic group laws (sanity tests)`, () => {
|
|
||||||
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[7].add(G[3].negate()), G[4], `7*G - 3*G = 4*G`);
|
|
||||||
});
|
|
||||||
should(`${name}/${pointName}/Basic group laws (addition 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[2]).add(G[2]), `4*G + 3*G = 3*G + 2*G + 2*G`);
|
|
||||||
});
|
|
||||||
should(`${name}/${pointName}/Basic group laws (double)`, () => {
|
|
||||||
equal(G[3].double(), G[6], '(3*G).double() = 6*G');
|
|
||||||
});
|
|
||||||
should(`${name}/${pointName}/Basic group laws (multiply)`, () => {
|
|
||||||
equal(G[2].multiply(3), G[6], '(2*G).multiply(3) = 6*G');
|
|
||||||
});
|
|
||||||
should(`${name}/${pointName}/Basic group laws (same point addition)`, () => {
|
|
||||||
equal(G[3].add(G[3]), G[6], `3*G + 3*G = 6*G`);
|
|
||||||
});
|
|
||||||
should(`${name}/${pointName}/Basic group laws (same point (negative) addition)`, () => {
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
should(`${name}/${pointName}/Basic group laws (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[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');
|
|
||||||
const half = CURVE_ORDER / 2n;
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
should(`${name}/${pointName}/Basic group laws (inversion)`, () => {
|
|
||||||
const a = 1234n;
|
|
||||||
const b = 5678n;
|
|
||||||
const c = a * b;
|
|
||||||
equal(G[1].multiply(a).multiply(b), G[1].multiply(c), 'a*b*G = c*G');
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
should(`${name}/${pointName}/Basic group laws (multiply, rand)`, () =>
|
|
||||||
fc.assert(
|
|
||||||
fc.property(FC_BIGINT, FC_BIGINT, (a, b) => {
|
|
||||||
const c = mod.mod(a + b, CURVE_ORDER);
|
|
||||||
if (c === CURVE_ORDER || c < 1n) return;
|
|
||||||
const pA = G[1].multiply(a);
|
|
||||||
const pB = G[1].multiply(b);
|
|
||||||
const pC = G[1].multiply(c);
|
|
||||||
equal(pA.add(pB), pB.add(pA), `pA + pB = pB + pA`);
|
|
||||||
equal(pA.add(pB), pC, `pA + pB = pC`);
|
|
||||||
}),
|
|
||||||
{ numRuns: NUM_RUNS }
|
|
||||||
)
|
|
||||||
);
|
|
||||||
should(`${name}/${pointName}/Basic group laws (multiply2, rand)`, () =>
|
|
||||||
fc.assert(
|
|
||||||
fc.property(FC_BIGINT, FC_BIGINT, (a, b) => {
|
|
||||||
const c = mod.mod(a * b, CURVE_ORDER);
|
|
||||||
const pA = G[1].multiply(a);
|
|
||||||
const pB = G[1].multiply(b);
|
|
||||||
equal(pA.multiply(b), pB.multiply(a), `b*pA = a*pB`);
|
|
||||||
equal(pA.multiply(b), G[1].multiply(c), `b*pA = c*G`);
|
|
||||||
}),
|
|
||||||
{ numRuns: NUM_RUNS }
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const op of ['add', 'subtract']) {
|
|
||||||
should(`${name}/${pointName}/${op} type check`, () => {
|
|
||||||
throws(() => G[1][op](0), '0');
|
|
||||||
throws(() => G[1][op](0n), '0n');
|
|
||||||
G[1][op](G[2]);
|
|
||||||
throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER');
|
|
||||||
throws(() => G[1][op](123.456), '123.456');
|
|
||||||
throws(() => G[1][op](true), 'true');
|
|
||||||
throws(() => G[1][op]('1'), "'1'");
|
|
||||||
throws(() => G[1][op]({ x: 1n, y: 1n, z: 1n, t: 1n }), '{ x: 1n, y: 1n, z: 1n, t: 1n }');
|
|
||||||
throws(() => G[1][op](new Uint8Array([])), 'ui8a([])');
|
|
||||||
throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])');
|
|
||||||
throws(() => G[1][op](new Uint8Array([1])), 'ui8a([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}`);
|
|
||||||
throws(() => G[1][op](o.BASE), `${op}/other curve point`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
should(`${name}/${pointName}/equals type check`, () => {
|
|
||||||
throws(() => G[1].equals(0), '0');
|
|
||||||
throws(() => G[1].equals(0n), '0n');
|
|
||||||
deepStrictEqual(G[1].equals(G[2]), false, '1*G != 2*G');
|
|
||||||
deepStrictEqual(G[1].equals(G[1]), true, '1*G == 1*G');
|
|
||||||
deepStrictEqual(G[2].equals(G[2]), true, '2*G == 2*G');
|
|
||||||
throws(() => G[1].equals(CURVE_ORDER), 'CURVE_ORDER');
|
|
||||||
throws(() => G[1].equals(123.456), '123.456');
|
|
||||||
throws(() => G[1].equals(true), 'true');
|
|
||||||
throws(() => G[1].equals('1'), "'1'");
|
|
||||||
throws(() => G[1].equals({ x: 1n, y: 1n, z: 1n, t: 1n }), '{ x: 1n, y: 1n, z: 1n, t: 1n }');
|
|
||||||
throws(() => G[1].equals(new Uint8Array([])), 'ui8a([])');
|
|
||||||
throws(() => G[1].equals(new Uint8Array([0])), 'ui8a([0])');
|
|
||||||
throws(() => G[1].equals(new Uint8Array([1])), 'ui8a([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})`);
|
|
||||||
throws(() => G[1].equals(o.BASE), 'other curve point');
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const op of ['multiply', 'multiplyUnsafe']) {
|
|
||||||
if (!p.BASE[op]) continue;
|
|
||||||
should(`${name}/${pointName}/${op} type check`, () => {
|
|
||||||
if (op !== 'multiplyUnsafe') {
|
|
||||||
throws(() => G[1][op](0), '0');
|
|
||||||
throws(() => G[1][op](0n), '0n');
|
|
||||||
}
|
|
||||||
G[1][op](1n);
|
|
||||||
G[1][op](CURVE_ORDER - 1n);
|
|
||||||
throws(() => G[1][op](G[2]), 'G[2]');
|
|
||||||
throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER');
|
|
||||||
throws(() => G[1][op](CURVE_ORDER + 1n), 'CURVE_ORDER+1');
|
|
||||||
throws(() => G[1][op](123.456), '123.456');
|
|
||||||
throws(() => G[1][op](true), 'true');
|
|
||||||
throws(() => G[1][op]('1'), '1');
|
|
||||||
throws(() => G[1][op](new Uint8Array([])), 'ui8a([])');
|
|
||||||
throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])');
|
|
||||||
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](o.BASE), 'other curve point');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Complex point (Extended/Jacobian/Projective?)
|
|
||||||
if (p.BASE.toAffine) {
|
|
||||||
should(`${name}/${pointName}/toAffine()`, () => {
|
|
||||||
equal(p.ZERO.toAffine(), C.Point.ZERO, `0 = 0`);
|
|
||||||
equal(p.BASE.toAffine(), C.Point.BASE, `1 = 1`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (p.fromAffine) {
|
|
||||||
should(`${name}/${pointName}/fromAffine()`, () => {
|
|
||||||
equal(p.ZERO, p.fromAffine(C.Point.ZERO), `0 = 0`);
|
|
||||||
equal(p.BASE, p.fromAffine(C.Point.BASE), `1 = 1`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// toHex/fromHex (if available)
|
|
||||||
if (p.fromHex && p.BASE.toHex) {
|
|
||||||
should(`${name}/${pointName}/fromHex(toHex()) roundtrip`, () => {
|
|
||||||
fc.assert(
|
|
||||||
fc.property(FC_BIGINT, (x) => {
|
|
||||||
const hex = p.BASE.multiply(x).toHex();
|
|
||||||
deepStrictEqual(p.fromHex(hex).toHex(), hex);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Generic complex things (getPublicKey/sign/verify/getSharedSecret)
|
|
||||||
should(`${name}/getPublicKey type check`, () => {
|
|
||||||
throws(() => C.getPublicKey(0), '0');
|
|
||||||
throws(() => C.getPublicKey(0n), '0n');
|
|
||||||
throws(() => C.getPublicKey(false), 'false');
|
|
||||||
throws(() => C.getPublicKey(123.456), '123.456');
|
|
||||||
throws(() => C.getPublicKey(true), 'true');
|
|
||||||
throws(() => C.getPublicKey(''), "''");
|
|
||||||
// NOTE: passes because of disabled hex padding checks for starknet, maybe enable?
|
|
||||||
//throws(() => C.getPublicKey('1'), "'1'");
|
|
||||||
throws(() => C.getPublicKey('key'), "'key'");
|
|
||||||
throws(() => C.getPublicKey(new Uint8Array([])));
|
|
||||||
throws(() => C.getPublicKey(new Uint8Array([0])));
|
|
||||||
throws(() => C.getPublicKey(new Uint8Array([1])));
|
|
||||||
throws(() => C.getPublicKey(new Uint8Array(4096).fill(1)));
|
|
||||||
});
|
|
||||||
should(`${name}.verify()/should verify random signatures`, () =>
|
|
||||||
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(C.verify(sig, msg, pub), true);
|
|
||||||
}),
|
|
||||||
{ numRuns: NUM_RUNS }
|
|
||||||
)
|
|
||||||
);
|
|
||||||
should(`${name}.sign()/edge cases`, () => {
|
|
||||||
throws(() => C.sign());
|
|
||||||
throws(() => C.sign(''));
|
|
||||||
});
|
|
||||||
|
|
||||||
should(`${name}.verify()/should not verify signature with wrong hash`, () => {
|
|
||||||
const MSG = '01'.repeat(32);
|
|
||||||
const PRIV_KEY = 0x2n;
|
|
||||||
const WRONG_MSG = '11'.repeat(32);
|
|
||||||
const signature = C.sign(MSG, PRIV_KEY);
|
|
||||||
const publicKey = C.getPublicKey(PRIV_KEY);
|
|
||||||
deepStrictEqual(C.verify(signature, WRONG_MSG, publicKey), false);
|
|
||||||
});
|
|
||||||
// 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?
|
|
||||||
// should(`${name}/should not verify signature with wrong message`, () => {
|
|
||||||
// fc.assert(
|
|
||||||
// fc.property(
|
|
||||||
// fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
|
||||||
// fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
|
||||||
// (bytes, wrongBytes) => {
|
|
||||||
// const privKey = C.utils.randomPrivateKey();
|
|
||||||
// const message = new Uint8Array(bytes);
|
|
||||||
// const wrongMessage = new Uint8Array(wrongBytes);
|
|
||||||
// const publicKey = C.getPublicKey(privKey);
|
|
||||||
// const signature = C.sign(message, privKey);
|
|
||||||
// deepStrictEqual(
|
|
||||||
// C.verify(signature, wrongMessage, publicKey),
|
|
||||||
// bytes.toString() === wrongBytes.toString()
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// ),
|
|
||||||
// { numRuns: NUM_RUNS }
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
|
|
||||||
if (C.getSharedSecret) {
|
|
||||||
should(`${name}/getSharedSecret() should be commutative`, () => {
|
|
||||||
for (let i = 0; i < NUM_RUNS; i++) {
|
|
||||||
const asec = C.utils.randomPrivateKey();
|
|
||||||
const apub = C.getPublicKey(asec);
|
|
||||||
const bsec = C.utils.randomPrivateKey();
|
|
||||||
const bpub = C.getPublicKey(bsec);
|
|
||||||
try {
|
|
||||||
deepStrictEqual(C.getSharedSecret(asec, bpub), C.getSharedSecret(bsec, apub));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('not commutative', { asec, apub, bsec, bpub });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ESM is broken.
|
|
||||||
import url from 'url';
|
|
||||||
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
|
||||||
should.run();
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,657 +0,0 @@
|
|||||||
import { deepStrictEqual, throws } from 'assert';
|
|
||||||
import { should } from 'micro-should';
|
|
||||||
import * as fc from 'fast-check';
|
|
||||||
import { ed25519, ed25519ctx, ed25519ph, x25519, RistrettoPoint } from '../lib/ed25519.js';
|
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
|
|
||||||
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
|
||||||
import { numberToBytesLE } from '@noble/curves/utils';
|
|
||||||
import { sha512 } from '@noble/hashes/sha512';
|
|
||||||
import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' };
|
|
||||||
import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' };
|
|
||||||
|
|
||||||
const ed = ed25519;
|
|
||||||
const hex = bytesToHex;
|
|
||||||
|
|
||||||
function to32Bytes(numOrStr) {
|
|
||||||
let hex = typeof numOrStr === 'string' ? numOrStr : numOrStr.toString(16);
|
|
||||||
return hexToBytes(hex.padStart(64, '0'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function utf8ToBytes(str) {
|
|
||||||
if (typeof str !== 'string') {
|
|
||||||
throw new TypeError(`utf8ToBytes expected string, got ${typeof str}`);
|
|
||||||
}
|
|
||||||
return new TextEncoder().encode(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
ed.utils.precompute(8);
|
|
||||||
|
|
||||||
should('ed25519/should not accept >32byte private keys', () => {
|
|
||||||
const invalidPriv =
|
|
||||||
100000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800073278156000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000n;
|
|
||||||
throws(() => ed.getPublicKey(invalidPriv));
|
|
||||||
});
|
|
||||||
should('ed25519/should verify recent signature', () => {
|
|
||||||
fc.assert(
|
|
||||||
fc.property(
|
|
||||||
fc.hexaString({ minLength: 2, maxLength: 32 }),
|
|
||||||
fc.bigInt(2n, ed.CURVE.n),
|
|
||||||
(message, privateKey) => {
|
|
||||||
const publicKey = ed.getPublicKey(to32Bytes(privateKey));
|
|
||||||
const signature = ed.sign(to32Bytes(message), to32Bytes(privateKey));
|
|
||||||
deepStrictEqual(publicKey.length, 32);
|
|
||||||
deepStrictEqual(signature.length, 64);
|
|
||||||
deepStrictEqual(ed.verify(signature, to32Bytes(message), publicKey), true);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{ numRuns: 5 }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
should('ed25519/should not verify signature with wrong message', () => {
|
|
||||||
fc.assert(
|
|
||||||
fc.property(
|
|
||||||
fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
|
||||||
fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
|
||||||
fc.bigInt(1n, ed.CURVE.n),
|
|
||||||
(bytes, wrongBytes, privateKey) => {
|
|
||||||
const privKey = to32Bytes(privateKey);
|
|
||||||
const message = new Uint8Array(bytes);
|
|
||||||
const wrongMessage = new Uint8Array(wrongBytes);
|
|
||||||
const publicKey = ed.getPublicKey(privKey);
|
|
||||||
const signature = ed.sign(message, privKey);
|
|
||||||
deepStrictEqual(
|
|
||||||
ed.verify(signature, wrongMessage, publicKey),
|
|
||||||
bytes.toString() === wrongBytes.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{ numRuns: 5 }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const privKey = to32Bytes('a665a45920422f9d417e4867ef');
|
|
||||||
const msg = hexToBytes('874f9960c5d2b7a9b5fad383e1ba44719ebb743a');
|
|
||||||
const wrongMsg = hexToBytes('589d8c7f1da0a24bc07b7381ad48b1cfc211af1c');
|
|
||||||
should('ed25519/basic methods/should sign and verify', () => {
|
|
||||||
const publicKey = ed.getPublicKey(privKey);
|
|
||||||
const signature = ed.sign(msg, privKey);
|
|
||||||
deepStrictEqual(ed.verify(signature, msg, publicKey), true);
|
|
||||||
});
|
|
||||||
should('ed25519/basic methods/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('ed25519/basic methods/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);
|
|
||||||
});
|
|
||||||
|
|
||||||
should('ed25519/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('ed25519/sync methods/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('ed25519/sync methods/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);
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://xmr.llcoins.net/addresstests.html
|
|
||||||
should(
|
|
||||||
'ed25519/BASE_POINT.multiply()/should create right publicKey without SHA-512 hashing TEST 1',
|
|
||||||
() => {
|
|
||||||
const publicKey =
|
|
||||||
ed.Point.BASE.multiply(0x90af56259a4b6bfbc4337980d5d75fbe3c074630368ff3804d33028e5dbfa77n);
|
|
||||||
deepStrictEqual(
|
|
||||||
publicKey.toHex(),
|
|
||||||
'0f3b913371411b27e646b537e888f685bf929ea7aab93c950ed84433f064480d'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
should(
|
|
||||||
'ed25519/BASE_POINT.multiply()/should create right publicKey without SHA-512 hashing TEST 2',
|
|
||||||
() => {
|
|
||||||
const publicKey =
|
|
||||||
ed.Point.BASE.multiply(0x364e8711a60780382a5d57b061c126f039940f28a9e91fe039d4d3094d8b88n);
|
|
||||||
deepStrictEqual(
|
|
||||||
publicKey.toHex(),
|
|
||||||
'ad545340b58610f0cd62f17d55af1ab11ecde9c084d5476865ddb4dbda015349'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
should(
|
|
||||||
'ed25519/BASE_POINT.multiply()/should create right publicKey without SHA-512 hashing TEST 3',
|
|
||||||
() => {
|
|
||||||
const publicKey =
|
|
||||||
ed.Point.BASE.multiply(0xb9bf90ff3abec042752cac3a07a62f0c16cfb9d32a3fc2305d676ec2d86e941n);
|
|
||||||
deepStrictEqual(
|
|
||||||
publicKey.toHex(),
|
|
||||||
'e097c4415fe85724d522b2e449e8fd78dd40d20097bdc9ae36fe8ec6fe12cb8c'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
should(
|
|
||||||
'ed25519/BASE_POINT.multiply()/should create right publicKey without SHA-512 hashing TEST 4',
|
|
||||||
() => {
|
|
||||||
const publicKey =
|
|
||||||
ed.Point.BASE.multiply(0x69d896f02d79524c9878e080308180e2859d07f9f54454e0800e8db0847a46en);
|
|
||||||
deepStrictEqual(
|
|
||||||
publicKey.toHex(),
|
|
||||||
'f12cb7c43b59971395926f278ce7c2eaded9444fbce62ca717564cb508a0db1d'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
should('ed25519/BASE_POINT.multiply()/should throw Point#multiply on TEST 5', () => {
|
|
||||||
for (const num of [0n, 0, -1n, -1, 1.1]) {
|
|
||||||
throws(() => ed.Point.BASE.multiply(num));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://ed25519.cr.yp.to/python/sign.py
|
|
||||||
// https://ed25519.cr.yp.to/python/sign.input
|
|
||||||
const data = readFileSync('./test/ed25519/vectors.txt', 'utf-8');
|
|
||||||
const vectors = data
|
|
||||||
.trim()
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.split(':'));
|
|
||||||
should('ed25519 official vectors/should match 1024 official vectors', () => {
|
|
||||||
for (let i = 0; i < vectors.length; i++) {
|
|
||||||
const vector = vectors[i];
|
|
||||||
// Extract.
|
|
||||||
const priv = vector[0].slice(0, 64);
|
|
||||||
const expectedPub = vector[1];
|
|
||||||
const msg = vector[2];
|
|
||||||
const expectedSignature = vector[3].slice(0, 128);
|
|
||||||
|
|
||||||
// Calculate
|
|
||||||
const pub = ed.getPublicKey(to32Bytes(priv));
|
|
||||||
deepStrictEqual(hex(pub), expectedPub);
|
|
||||||
deepStrictEqual(pub, ed.Point.fromHex(pub).toRawBytes());
|
|
||||||
|
|
||||||
const signature = hex(ed.sign(msg, priv));
|
|
||||||
// console.log('vector', i);
|
|
||||||
// expect(pub).toBe(expectedPub);
|
|
||||||
deepStrictEqual(signature, expectedSignature);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc8032#section-7
|
|
||||||
should('rfc8032 vectors/should create right signature for 0x9d and empty string', () => {
|
|
||||||
const privateKey = '9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60';
|
|
||||||
const publicKey = ed.getPublicKey(privateKey);
|
|
||||||
const message = '';
|
|
||||||
const signature = ed.sign(message, privateKey);
|
|
||||||
deepStrictEqual(
|
|
||||||
hex(publicKey),
|
|
||||||
'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'
|
|
||||||
);
|
|
||||||
deepStrictEqual(
|
|
||||||
hex(signature),
|
|
||||||
'e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
should('rfc8032 vectors/should create right signature for 0x4c and 72', () => {
|
|
||||||
const privateKey = '4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb';
|
|
||||||
const publicKey = ed.getPublicKey(privateKey);
|
|
||||||
const message = '72';
|
|
||||||
const signature = ed.sign(message, privateKey);
|
|
||||||
deepStrictEqual(
|
|
||||||
hex(publicKey),
|
|
||||||
'3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c'
|
|
||||||
);
|
|
||||||
deepStrictEqual(
|
|
||||||
hex(signature),
|
|
||||||
'92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
should('rfc8032 vectors/should create right signature for 0x00 and 5a', () => {
|
|
||||||
const privateKey = '002fdd1f7641793ab064bb7aa848f762e7ec6e332ffc26eeacda141ae33b1783';
|
|
||||||
const publicKey = ed.getPublicKey(privateKey);
|
|
||||||
const message =
|
|
||||||
'5ac1dfc324f43e6cb79a87ab0470fa857b51fb944982e19074ca44b1e40082c1d07b92efa7ea55ad42b7c027e0b9e33756d95a2c1796a7c2066811dc41858377d4b835c1688d638884cd2ad8970b74c1a54aadd27064163928a77988b24403aa85af82ceab6b728e554761af7175aeb99215b7421e4474c04d213e01ff03e3529b11077cdf28964b8c49c5649e3a46fa0a09dcd59dcad58b9b922a83210acd5e65065531400234f5e40cddcf9804968e3e9ac6f5c44af65001e158067fc3a660502d13fa8874fa93332138d9606bc41b4cee7edc39d753dae12a873941bb357f7e92a4498847d6605456cb8c0b425a47d7d3ca37e54e903a41e6450a35ebe5237c6f0c1bbbc1fd71fb7cd893d189850295c199b7d88af26bc8548975fda1099ffefee42a52f3428ddff35e0173d3339562507ac5d2c45bbd2c19cfe89b';
|
|
||||||
const signature = ed.sign(message, privateKey);
|
|
||||||
deepStrictEqual(
|
|
||||||
hex(publicKey),
|
|
||||||
'77d1d8ebacd13f4e2f8a40e28c4a63bc9ce3bfb69716334bcb28a33eb134086c'
|
|
||||||
);
|
|
||||||
deepStrictEqual(
|
|
||||||
hex(signature),
|
|
||||||
'0df3aa0d0999ad3dc580378f52d152700d5b3b057f56a66f92112e441e1cb9123c66f18712c87efe22d2573777296241216904d7cdd7d5ea433928bd2872fa0c'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
should('rfc8032 vectors/should create right signature for 0xf5 and long msg', () => {
|
|
||||||
const privateKey = 'f5e5767cf153319517630f226876b86c8160cc583bc013744c6bf255f5cc0ee5';
|
|
||||||
const publicKey = ed.getPublicKey(privateKey);
|
|
||||||
const message =
|
|
||||||
'08b8b2b733424243760fe426a4b54908632110a66c2f6591eabd3345e3e4eb98fa6e264bf09efe12ee50f8f54e9f77b1e355f6c50544e23fb1433ddf73be84d879de7c0046dc4996d9e773f4bc9efe5738829adb26c81b37c93a1b270b20329d658675fc6ea534e0810a4432826bf58c941efb65d57a338bbd2e26640f89ffbc1a858efcb8550ee3a5e1998bd177e93a7363c344fe6b199ee5d02e82d522c4feba15452f80288a821a579116ec6dad2b3b310da903401aa62100ab5d1a36553e06203b33890cc9b832f79ef80560ccb9a39ce767967ed628c6ad573cb116dbefefd75499da96bd68a8a97b928a8bbc103b6621fcde2beca1231d206be6cd9ec7aff6f6c94fcd7204ed3455c68c83f4a41da4af2b74ef5c53f1d8ac70bdcb7ed185ce81bd84359d44254d95629e9855a94a7c1958d1f8ada5d0532ed8a5aa3fb2d17ba70eb6248e594e1a2297acbbb39d502f1a8c6eb6f1ce22b3de1a1f40cc24554119a831a9aad6079cad88425de6bde1a9187ebb6092cf67bf2b13fd65f27088d78b7e883c8759d2c4f5c65adb7553878ad575f9fad878e80a0c9ba63bcbcc2732e69485bbc9c90bfbd62481d9089beccf80cfe2df16a2cf65bd92dd597b0707e0917af48bbb75fed413d238f5555a7a569d80c3414a8d0859dc65a46128bab27af87a71314f318c782b23ebfe808b82b0ce26401d2e22f04d83d1255dc51addd3b75a2b1ae0784504df543af8969be3ea7082ff7fc9888c144da2af58429ec96031dbcad3dad9af0dcbaaaf268cb8fcffead94f3c7ca495e056a9b47acdb751fb73e666c6c655ade8297297d07ad1ba5e43f1bca32301651339e22904cc8c42f58c30c04aafdb038dda0847dd988dcda6f3bfd15c4b4c4525004aa06eeff8ca61783aacec57fb3d1f92b0fe2fd1a85f6724517b65e614ad6808d6f6ee34dff7310fdc82aebfd904b01e1dc54b2927094b2db68d6f903b68401adebf5a7e08d78ff4ef5d63653a65040cf9bfd4aca7984a74d37145986780fc0b16ac451649de6188a7dbdf191f64b5fc5e2ab47b57f7f7276cd419c17a3ca8e1b939ae49e488acba6b965610b5480109c8b17b80e1b7b750dfc7598d5d5011fd2dcc5600a32ef5b52a1ecc820e308aa342721aac0943bf6686b64b2579376504ccc493d97e6aed3fb0f9cd71a43dd497f01f17c0e2cb3797aa2a2f256656168e6c496afc5fb93246f6b1116398a346f1a641f3b041e989f7914f90cc2c7fff357876e506b50d334ba77c225bc307ba537152f3f1610e4eafe595f6d9d90d11faa933a15ef1369546868a7f3a45a96768d40fd9d03412c091c6315cf4fde7cb68606937380db2eaaa707b4c4185c32eddcdd306705e4dc1ffc872eeee475a64dfac86aba41c0618983f8741c5ef68d3a101e8a3b8cac60c905c15fc910840b94c00a0b9d0';
|
|
||||||
const signature = ed.sign(message, privateKey);
|
|
||||||
deepStrictEqual(
|
|
||||||
hex(publicKey),
|
|
||||||
'278117fc144c72340f67d0f2316e8386ceffbf2b2428c9c51fef7c597f1d426e'
|
|
||||||
);
|
|
||||||
deepStrictEqual(
|
|
||||||
hex(signature),
|
|
||||||
'0aab4c900501b3e24d7cdf4663326a3a87df5e4843b2cbdb67cbf6e460fec350aa5371b1508f9f4528ecea23c436d94b5e8fcd4f681e30a6ac00a9704a188a03'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// const PRIVATE_KEY = 0xa665a45920422f9d417e4867efn;
|
|
||||||
// const MESSAGE = ripemd160(new Uint8Array([97, 98, 99, 100, 101, 102, 103]));
|
|
||||||
// prettier-ignore
|
|
||||||
// const MESSAGE = new Uint8Array([
|
|
||||||
// 135, 79, 153, 96, 197, 210, 183, 169, 181, 250, 211, 131, 225, 186, 68, 113, 158, 187, 116, 58,
|
|
||||||
// ]);
|
|
||||||
// const WRONG_MESSAGE = ripemd160(new Uint8Array([98, 99, 100, 101, 102, 103]));
|
|
||||||
// prettier-ignore
|
|
||||||
// const WRONG_MESSAGE = new Uint8Array([
|
|
||||||
// 88, 157, 140, 127, 29, 160, 162, 75, 192, 123, 115, 129, 173, 72, 177, 207, 194, 17, 175, 28,
|
|
||||||
// ]);
|
|
||||||
// // it("should verify just signed message", async () => {
|
|
||||||
// // await fc.assert(fc.asyncProperty(
|
|
||||||
// // fc.hexa(),
|
|
||||||
// // fc.bigInt(2n, ristretto25519.PRIME_ORDER),
|
|
||||||
// // async (message, privateKey) => {
|
|
||||||
// // const publicKey = await ristretto25519.getPublicKey(privateKey);
|
|
||||||
// // const signature = await ristretto25519.sign(message, privateKey);
|
|
||||||
// // expect(publicKey.length).toBe(32);
|
|
||||||
// // expect(signature.length).toBe(64);
|
|
||||||
// // expect(await ristretto25519.verify(signature, message, publicKey)).toBe(true);
|
|
||||||
// // }),
|
|
||||||
// // { numRuns: 1 }
|
|
||||||
// // );
|
|
||||||
// // });
|
|
||||||
// // it("should not verify sign with wrong message", async () => {
|
|
||||||
// // await fc.assert(fc.asyncProperty(
|
|
||||||
// // fc.array(fc.integer(0x00, 0xff)),
|
|
||||||
// // fc.array(fc.integer(0x00, 0xff)),
|
|
||||||
// // fc.bigInt(2n, ristretto25519.PRIME_ORDER),
|
|
||||||
// // async (bytes, wrongBytes, privateKey) => {
|
|
||||||
// // const message = new Uint8Array(bytes);
|
|
||||||
// // const wrongMessage = new Uint8Array(wrongBytes);
|
|
||||||
// // const publicKey = await ristretto25519.getPublicKey(privateKey);
|
|
||||||
// // const signature = await ristretto25519.sign(message, privateKey);
|
|
||||||
// // expect(await ristretto25519.verify(signature, wrongMessage, publicKey)).toBe(
|
|
||||||
// // bytes.toString() === wrongBytes.toString()
|
|
||||||
// // );
|
|
||||||
// // }),
|
|
||||||
// // { numRuns: 1 }
|
|
||||||
// // );
|
|
||||||
// // });
|
|
||||||
// // it("should sign and verify", async () => {
|
|
||||||
// // const publicKey = await ristretto25519.getPublicKey(PRIVATE_KEY);
|
|
||||||
// // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY);
|
|
||||||
// // expect(await ristretto25519.verify(signature, MESSAGE, publicKey)).toBe(true);
|
|
||||||
// // });
|
|
||||||
// // it("should not verify signature with wrong public key", async () => {
|
|
||||||
// // const publicKey = await ristretto25519.getPublicKey(12);
|
|
||||||
// // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY);
|
|
||||||
// // expect(await ristretto25519.verify(signature, MESSAGE, publicKey)).toBe(false);
|
|
||||||
// // });
|
|
||||||
// // it("should not verify signature with wrong hash", async () => {
|
|
||||||
// // const publicKey = await ristretto25519.getPublicKey(PRIVATE_KEY);
|
|
||||||
// // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY);
|
|
||||||
// // 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^2.
|
|
||||||
'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', () => {
|
|
||||||
const privateKey = ed.utils.randomPrivateKey();
|
|
||||||
const publicKey = ed.getPublicKey(privateKey);
|
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
let payload = randomBytes(100);
|
|
||||||
let signature = ed.sign(payload, privateKey);
|
|
||||||
if (!ed.verify(signature, payload, publicKey)) {
|
|
||||||
throw new Error('Signature verification failed');
|
|
||||||
}
|
|
||||||
const signatureCopy = Buffer.alloc(signature.byteLength);
|
|
||||||
signatureCopy.set(signature, 0); // <-- breaks
|
|
||||||
payload = payload.slice();
|
|
||||||
signature = signature.slice();
|
|
||||||
|
|
||||||
if (!ed.verify(signatureCopy, payload, publicKey))
|
|
||||||
throw new Error('Copied signature verification failed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://zips.z.cash/zip-0215
|
|
||||||
// Vectors from https://gist.github.com/hdevalence/93ed42d17ecab8e42138b213812c8cc7
|
|
||||||
should('ZIP-215 compliance tests/should pass all of them', () => {
|
|
||||||
const str = utf8ToBytes('Zcash');
|
|
||||||
for (let v of zip215) {
|
|
||||||
let noble = false;
|
|
||||||
try {
|
|
||||||
noble = ed.verify(v.sig_bytes, str, v.vk_bytes);
|
|
||||||
} catch (e) {
|
|
||||||
noble = false;
|
|
||||||
}
|
|
||||||
deepStrictEqual(noble, v.valid_zip215);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('ZIP-215 compliance tests/disallows sig.s >= CURVE.n', () => {
|
|
||||||
const sig = new ed.Signature(ed.Point.BASE, 1n);
|
|
||||||
sig.s = ed.CURVE.n + 1n;
|
|
||||||
throws(() => ed.verify(sig, 'deadbeef', ed.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.u, v.scalar)), 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(u, k), 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(bobPublic, alicePrivate)), shared);
|
|
||||||
deepStrictEqual(hex(x25519.scalarMult(alicePublic, bobPrivate)), shared);
|
|
||||||
});
|
|
||||||
|
|
||||||
// should('X25519/getSharedSecret() should be commutative', () => {
|
|
||||||
// for (let i = 0; i < 512; i++) {
|
|
||||||
// const asec = ed.utils.randomPrivateKey();
|
|
||||||
// const apub = ed.getPublicKey(asec);
|
|
||||||
// const bsec = ed.utils.randomPrivateKey();
|
|
||||||
// const bpub = ed.getPublicKey(bsec);
|
|
||||||
// try {
|
|
||||||
// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('not commutative', { asec, apub, bsec, bpub });
|
|
||||||
// throw error;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// should('X25519: should convert base point to montgomery using fromPoint', () => {
|
|
||||||
// deepStrictEqual(
|
|
||||||
// hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)),
|
|
||||||
// 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.public, v.private));
|
|
||||||
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.public, v.private);
|
|
||||||
} catch (error) {
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
deepStrictEqual(failed, true, comment);
|
|
||||||
} else throw new Error('unknown test result');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
should(`Wycheproof/ED25519`, () => {
|
|
||||||
for (let g = 0; g < ed25519vectors.testGroups.length; g++) {
|
|
||||||
const group = ed25519vectors.testGroups[g];
|
|
||||||
const key = group.key;
|
|
||||||
deepStrictEqual(hex(ed.getPublicKey(key.sk)), key.pk, `(${g}, public)`);
|
|
||||||
for (let i = 0; i < group.tests.length; i++) {
|
|
||||||
const v = group.tests[i];
|
|
||||||
const comment = `(${g}/${i}, ${v.result}): ${v.comment}`;
|
|
||||||
if (v.result === 'valid' || v.result === 'acceptable') {
|
|
||||||
deepStrictEqual(hex(ed.sign(v.msg, key.sk)), v.sig, comment);
|
|
||||||
deepStrictEqual(ed.verify(v.sig, v.msg, key.pk), true, comment);
|
|
||||||
} else if (v.result === 'invalid') {
|
|
||||||
let failed = false;
|
|
||||||
try {
|
|
||||||
failed = !ed.verify(v.sig, v.msg, key.pk);
|
|
||||||
} catch (error) {
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
deepStrictEqual(failed, true, comment);
|
|
||||||
} else throw new Error('unknown test result');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('Property test issue #1', () => {
|
|
||||||
const message = new Uint8Array([12, 12, 12]);
|
|
||||||
const signature = ed.sign(message, to32Bytes(1n));
|
|
||||||
const publicKey = ed.getPublicKey(to32Bytes(1n)); // <- was 1n
|
|
||||||
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 u = ed25519.utils.mod((y + 1n) * ed25519.utils.invert(1n - y, ed25519.CURVE.P));
|
|
||||||
deepStrictEqual(hex(numberToBytesLE(u, 32)), x25519.Gu);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ESM is broken.
|
|
||||||
import url from 'url';
|
|
||||||
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
|
||||||
should.run();
|
|
||||||
}
|
|
||||||
@@ -1,664 +0,0 @@
|
|||||||
import { deepStrictEqual, throws } from 'assert';
|
|
||||||
import { should } from 'micro-should';
|
|
||||||
import * as fc from 'fast-check';
|
|
||||||
import { ed448, ed448ph, x448 } from '../lib/ed448.js';
|
|
||||||
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
|
||||||
import { numberToBytesLE } from '@noble/curves/utils';
|
|
||||||
import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' };
|
|
||||||
import { default as x448vectors } from './wycheproof/x448_test.json' assert { type: 'json' };
|
|
||||||
|
|
||||||
const ed = ed448;
|
|
||||||
const hex = bytesToHex;
|
|
||||||
ed.utils.precompute(4);
|
|
||||||
|
|
||||||
should(`Basic`, () => {
|
|
||||||
const G1 = ed.Point.BASE;
|
|
||||||
deepStrictEqual(
|
|
||||||
G1.x,
|
|
||||||
224580040295924300187604334099896036246789641632564134246125461686950415467406032909029192869357953282578032075146446173674602635247710n
|
|
||||||
);
|
|
||||||
deepStrictEqual(
|
|
||||||
G1.y,
|
|
||||||
298819210078481492676017930443930673437544040154080242095928241372331506189835876003536878655418784733982303233503462500531545062832660n
|
|
||||||
);
|
|
||||||
const G2 = ed.Point.BASE.multiply(2n);
|
|
||||||
deepStrictEqual(
|
|
||||||
G2.x,
|
|
||||||
484559149530404593699549205258669689569094240458212040187660132787056912146709081364401144455726350866276831544947397859048262938744149n
|
|
||||||
);
|
|
||||||
deepStrictEqual(
|
|
||||||
G2.y,
|
|
||||||
494088759867433727674302672526735089350544552303727723746126484473087719117037293890093462157703888342865036477787453078312060500281069n
|
|
||||||
);
|
|
||||||
const G3 = ed.Point.BASE.multiply(3n);
|
|
||||||
deepStrictEqual(
|
|
||||||
G3.x,
|
|
||||||
23839778817283171003887799738662344287085130522697782688245073320169861206004018274567429238677677920280078599146891901463786155880335n
|
|
||||||
);
|
|
||||||
deepStrictEqual(
|
|
||||||
G3.y,
|
|
||||||
636046652612779686502873775776967954190574036985351036782021535703553242737829645273154208057988851307101009474686328623630835377952508n
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
should('Basic/decompress', () => {
|
|
||||||
const G1 = ed.Point.BASE;
|
|
||||||
const G2 = ed.Point.BASE.multiply(2n);
|
|
||||||
const G3 = ed.Point.BASE.multiply(3n);
|
|
||||||
const points = [G1, G2, G3];
|
|
||||||
const getXY = (p) => ({ x: p.x, y: p.y });
|
|
||||||
for (const p of points) deepStrictEqual(getXY(ed.Point.fromHex(p.toHex())), getXY(p));
|
|
||||||
});
|
|
||||||
|
|
||||||
const VECTORS_RFC8032 = [
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'6c82a562cb808d10d632be89c8513ebf' +
|
|
||||||
'6c929f34ddfa8c9f63c9960ef6e348a3' +
|
|
||||||
'528c8a3fcc2f044e39a3fc5b94492f8f' +
|
|
||||||
'032e7549a20098f95b',
|
|
||||||
publicKey:
|
|
||||||
'5fd7449b59b461fd2ce787ec616ad46a' +
|
|
||||||
'1da1342485a70e1f8a0ea75d80e96778' +
|
|
||||||
'edf124769b46c7061bd6783df1e50f6c' +
|
|
||||||
'd1fa1abeafe8256180',
|
|
||||||
message: '',
|
|
||||||
signature:
|
|
||||||
'533a37f6bbe457251f023c0d88f976ae' +
|
|
||||||
'2dfb504a843e34d2074fd823d41a591f' +
|
|
||||||
'2b233f034f628281f2fd7a22ddd47d78' +
|
|
||||||
'28c59bd0a21bfd3980ff0d2028d4b18a' +
|
|
||||||
'9df63e006c5d1c2d345b925d8dc00b41' +
|
|
||||||
'04852db99ac5c7cdda8530a113a0f4db' +
|
|
||||||
'b61149f05a7363268c71d95808ff2e65' +
|
|
||||||
'2600',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'c4eab05d357007c632f3dbb48489924d' +
|
|
||||||
'552b08fe0c353a0d4a1f00acda2c463a' +
|
|
||||||
'fbea67c5e8d2877c5e3bc397a659949e' +
|
|
||||||
'f8021e954e0a12274e',
|
|
||||||
publicKey:
|
|
||||||
'43ba28f430cdff456ae531545f7ecd0a' +
|
|
||||||
'c834a55d9358c0372bfa0c6c6798c086' +
|
|
||||||
'6aea01eb00742802b8438ea4cb82169c' +
|
|
||||||
'235160627b4c3a9480',
|
|
||||||
|
|
||||||
message: '03',
|
|
||||||
signature:
|
|
||||||
'26b8f91727bd62897af15e41eb43c377' +
|
|
||||||
'efb9c610d48f2335cb0bd0087810f435' +
|
|
||||||
'2541b143c4b981b7e18f62de8ccdf633' +
|
|
||||||
'fc1bf037ab7cd779805e0dbcc0aae1cb' +
|
|
||||||
'cee1afb2e027df36bc04dcecbf154336' +
|
|
||||||
'c19f0af7e0a6472905e799f1953d2a0f' +
|
|
||||||
'f3348ab21aa4adafd1d234441cf807c0' +
|
|
||||||
'3a00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'cd23d24f714274e744343237b93290f5' +
|
|
||||||
'11f6425f98e64459ff203e8985083ffd' +
|
|
||||||
'f60500553abc0e05cd02184bdb89c4cc' +
|
|
||||||
'd67e187951267eb328',
|
|
||||||
publicKey:
|
|
||||||
'dcea9e78f35a1bf3499a831b10b86c90' +
|
|
||||||
'aac01cd84b67a0109b55a36e9328b1e3' +
|
|
||||||
'65fce161d71ce7131a543ea4cb5f7e9f' +
|
|
||||||
'1d8b00696447001400',
|
|
||||||
message: '0c3e544074ec63b0265e0c',
|
|
||||||
signature:
|
|
||||||
'1f0a8888ce25e8d458a21130879b840a' +
|
|
||||||
'9089d999aaba039eaf3e3afa090a09d3' +
|
|
||||||
'89dba82c4ff2ae8ac5cdfb7c55e94d5d' +
|
|
||||||
'961a29fe0109941e00b8dbdeea6d3b05' +
|
|
||||||
'1068df7254c0cdc129cbe62db2dc957d' +
|
|
||||||
'bb47b51fd3f213fb8698f064774250a5' +
|
|
||||||
'028961c9bf8ffd973fe5d5c206492b14' +
|
|
||||||
'0e00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'258cdd4ada32ed9c9ff54e63756ae582' +
|
|
||||||
'fb8fab2ac721f2c8e676a72768513d93' +
|
|
||||||
'9f63dddb55609133f29adf86ec9929dc' +
|
|
||||||
'cb52c1c5fd2ff7e21b',
|
|
||||||
publicKey:
|
|
||||||
'3ba16da0c6f2cc1f30187740756f5e79' +
|
|
||||||
'8d6bc5fc015d7c63cc9510ee3fd44adc' +
|
|
||||||
'24d8e968b6e46e6f94d19b945361726b' +
|
|
||||||
'd75e149ef09817f580',
|
|
||||||
message: '64a65f3cdedcdd66811e2915',
|
|
||||||
signature:
|
|
||||||
'7eeeab7c4e50fb799b418ee5e3197ff6' +
|
|
||||||
'bf15d43a14c34389b59dd1a7b1b85b4a' +
|
|
||||||
'e90438aca634bea45e3a2695f1270f07' +
|
|
||||||
'fdcdf7c62b8efeaf00b45c2c96ba457e' +
|
|
||||||
'b1a8bf075a3db28e5c24f6b923ed4ad7' +
|
|
||||||
'47c3c9e03c7079efb87cb110d3a99861' +
|
|
||||||
'e72003cbae6d6b8b827e4e6c143064ff' +
|
|
||||||
'3c00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'7ef4e84544236752fbb56b8f31a23a10' +
|
|
||||||
'e42814f5f55ca037cdcc11c64c9a3b29' +
|
|
||||||
'49c1bb60700314611732a6c2fea98eeb' +
|
|
||||||
'c0266a11a93970100e',
|
|
||||||
publicKey:
|
|
||||||
'b3da079b0aa493a5772029f0467baebe' +
|
|
||||||
'e5a8112d9d3a22532361da294f7bb381' +
|
|
||||||
'5c5dc59e176b4d9f381ca0938e13c6c0' +
|
|
||||||
'7b174be65dfa578e80',
|
|
||||||
message: '64a65f3cdedcdd66811e2915e7',
|
|
||||||
signature:
|
|
||||||
'6a12066f55331b6c22acd5d5bfc5d712' +
|
|
||||||
'28fbda80ae8dec26bdd306743c5027cb' +
|
|
||||||
'4890810c162c027468675ecf645a8317' +
|
|
||||||
'6c0d7323a2ccde2d80efe5a1268e8aca' +
|
|
||||||
'1d6fbc194d3f77c44986eb4ab4177919' +
|
|
||||||
'ad8bec33eb47bbb5fc6e28196fd1caf5' +
|
|
||||||
'6b4e7e0ba5519234d047155ac727a105' +
|
|
||||||
'3100',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'd65df341ad13e008567688baedda8e9d' +
|
|
||||||
'cdc17dc024974ea5b4227b6530e339bf' +
|
|
||||||
'f21f99e68ca6968f3cca6dfe0fb9f4fa' +
|
|
||||||
'b4fa135d5542ea3f01',
|
|
||||||
publicKey:
|
|
||||||
'df9705f58edbab802c7f8363cfe5560a' +
|
|
||||||
'b1c6132c20a9f1dd163483a26f8ac53a' +
|
|
||||||
'39d6808bf4a1dfbd261b099bb03b3fb5' +
|
|
||||||
'0906cb28bd8a081f00',
|
|
||||||
message:
|
|
||||||
'bd0f6a3747cd561bdddf4640a332461a' +
|
|
||||||
'4a30a12a434cd0bf40d766d9c6d458e5' +
|
|
||||||
'512204a30c17d1f50b5079631f64eb31' +
|
|
||||||
'12182da3005835461113718d1a5ef944',
|
|
||||||
signature:
|
|
||||||
'554bc2480860b49eab8532d2a533b7d5' +
|
|
||||||
'78ef473eeb58c98bb2d0e1ce488a98b1' +
|
|
||||||
'8dfde9b9b90775e67f47d4a1c3482058' +
|
|
||||||
'efc9f40d2ca033a0801b63d45b3b722e' +
|
|
||||||
'f552bad3b4ccb667da350192b61c508c' +
|
|
||||||
'f7b6b5adadc2c8d9a446ef003fb05cba' +
|
|
||||||
'5f30e88e36ec2703b349ca229c267083' +
|
|
||||||
'3900',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'2ec5fe3c17045abdb136a5e6a913e32a' +
|
|
||||||
'b75ae68b53d2fc149b77e504132d3756' +
|
|
||||||
'9b7e766ba74a19bd6162343a21c8590a' +
|
|
||||||
'a9cebca9014c636df5',
|
|
||||||
publicKey:
|
|
||||||
'79756f014dcfe2079f5dd9e718be4171' +
|
|
||||||
'e2ef2486a08f25186f6bff43a9936b9b' +
|
|
||||||
'fe12402b08ae65798a3d81e22e9ec80e' +
|
|
||||||
'7690862ef3d4ed3a00',
|
|
||||||
message:
|
|
||||||
'15777532b0bdd0d1389f636c5f6b9ba7' +
|
|
||||||
'34c90af572877e2d272dd078aa1e567c' +
|
|
||||||
'fa80e12928bb542330e8409f31745041' +
|
|
||||||
'07ecd5efac61ae7504dabe2a602ede89' +
|
|
||||||
'e5cca6257a7c77e27a702b3ae39fc769' +
|
|
||||||
'fc54f2395ae6a1178cab4738e543072f' +
|
|
||||||
'c1c177fe71e92e25bf03e4ecb72f47b6' +
|
|
||||||
'4d0465aaea4c7fad372536c8ba516a60' +
|
|
||||||
'39c3c2a39f0e4d832be432dfa9a706a6' +
|
|
||||||
'e5c7e19f397964ca4258002f7c0541b5' +
|
|
||||||
'90316dbc5622b6b2a6fe7a4abffd9610' +
|
|
||||||
'5eca76ea7b98816af0748c10df048ce0' +
|
|
||||||
'12d901015a51f189f3888145c03650aa' +
|
|
||||||
'23ce894c3bd889e030d565071c59f409' +
|
|
||||||
'a9981b51878fd6fc110624dcbcde0bf7' +
|
|
||||||
'a69ccce38fabdf86f3bef6044819de11',
|
|
||||||
signature:
|
|
||||||
'c650ddbb0601c19ca11439e1640dd931' +
|
|
||||||
'f43c518ea5bea70d3dcde5f4191fe53f' +
|
|
||||||
'00cf966546b72bcc7d58be2b9badef28' +
|
|
||||||
'743954e3a44a23f880e8d4f1cfce2d7a' +
|
|
||||||
'61452d26da05896f0a50da66a239a8a1' +
|
|
||||||
'88b6d825b3305ad77b73fbac0836ecc6' +
|
|
||||||
'0987fd08527c1a8e80d5823e65cafe2a' +
|
|
||||||
'3d00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'872d093780f5d3730df7c212664b37b8' +
|
|
||||||
'a0f24f56810daa8382cd4fa3f77634ec' +
|
|
||||||
'44dc54f1c2ed9bea86fafb7632d8be19' +
|
|
||||||
'9ea165f5ad55dd9ce8',
|
|
||||||
publicKey:
|
|
||||||
'a81b2e8a70a5ac94ffdbcc9badfc3feb' +
|
|
||||||
'0801f258578bb114ad44ece1ec0e799d' +
|
|
||||||
'a08effb81c5d685c0c56f64eecaef8cd' +
|
|
||||||
'f11cc38737838cf400',
|
|
||||||
message:
|
|
||||||
'6ddf802e1aae4986935f7f981ba3f035' +
|
|
||||||
'1d6273c0a0c22c9c0e8339168e675412' +
|
|
||||||
'a3debfaf435ed651558007db4384b650' +
|
|
||||||
'fcc07e3b586a27a4f7a00ac8a6fec2cd' +
|
|
||||||
'86ae4bf1570c41e6a40c931db27b2faa' +
|
|
||||||
'15a8cedd52cff7362c4e6e23daec0fbc' +
|
|
||||||
'3a79b6806e316efcc7b68119bf46bc76' +
|
|
||||||
'a26067a53f296dafdbdc11c77f7777e9' +
|
|
||||||
'72660cf4b6a9b369a6665f02e0cc9b6e' +
|
|
||||||
'dfad136b4fabe723d2813db3136cfde9' +
|
|
||||||
'b6d044322fee2947952e031b73ab5c60' +
|
|
||||||
'3349b307bdc27bc6cb8b8bbd7bd32321' +
|
|
||||||
'9b8033a581b59eadebb09b3c4f3d2277' +
|
|
||||||
'd4f0343624acc817804728b25ab79717' +
|
|
||||||
'2b4c5c21a22f9c7839d64300232eb66e' +
|
|
||||||
'53f31c723fa37fe387c7d3e50bdf9813' +
|
|
||||||
'a30e5bb12cf4cd930c40cfb4e1fc6225' +
|
|
||||||
'92a49588794494d56d24ea4b40c89fc0' +
|
|
||||||
'596cc9ebb961c8cb10adde976a5d602b' +
|
|
||||||
'1c3f85b9b9a001ed3c6a4d3b1437f520' +
|
|
||||||
'96cd1956d042a597d561a596ecd3d173' +
|
|
||||||
'5a8d570ea0ec27225a2c4aaff26306d1' +
|
|
||||||
'526c1af3ca6d9cf5a2c98f47e1c46db9' +
|
|
||||||
'a33234cfd4d81f2c98538a09ebe76998' +
|
|
||||||
'd0d8fd25997c7d255c6d66ece6fa56f1' +
|
|
||||||
'1144950f027795e653008f4bd7ca2dee' +
|
|
||||||
'85d8e90f3dc315130ce2a00375a318c7' +
|
|
||||||
'c3d97be2c8ce5b6db41a6254ff264fa6' +
|
|
||||||
'155baee3b0773c0f497c573f19bb4f42' +
|
|
||||||
'40281f0b1f4f7be857a4e59d416c06b4' +
|
|
||||||
'c50fa09e1810ddc6b1467baeac5a3668' +
|
|
||||||
'd11b6ecaa901440016f389f80acc4db9' +
|
|
||||||
'77025e7f5924388c7e340a732e554440' +
|
|
||||||
'e76570f8dd71b7d640b3450d1fd5f041' +
|
|
||||||
'0a18f9a3494f707c717b79b4bf75c984' +
|
|
||||||
'00b096b21653b5d217cf3565c9597456' +
|
|
||||||
'f70703497a078763829bc01bb1cbc8fa' +
|
|
||||||
'04eadc9a6e3f6699587a9e75c94e5bab' +
|
|
||||||
'0036e0b2e711392cff0047d0d6b05bd2' +
|
|
||||||
'a588bc109718954259f1d86678a579a3' +
|
|
||||||
'120f19cfb2963f177aeb70f2d4844826' +
|
|
||||||
'262e51b80271272068ef5b3856fa8535' +
|
|
||||||
'aa2a88b2d41f2a0e2fda7624c2850272' +
|
|
||||||
'ac4a2f561f8f2f7a318bfd5caf969614' +
|
|
||||||
'9e4ac824ad3460538fdc25421beec2cc' +
|
|
||||||
'6818162d06bbed0c40a387192349db67' +
|
|
||||||
'a118bada6cd5ab0140ee273204f628aa' +
|
|
||||||
'd1c135f770279a651e24d8c14d75a605' +
|
|
||||||
'9d76b96a6fd857def5e0b354b27ab937' +
|
|
||||||
'a5815d16b5fae407ff18222c6d1ed263' +
|
|
||||||
'be68c95f32d908bd895cd76207ae7264' +
|
|
||||||
'87567f9a67dad79abec316f683b17f2d' +
|
|
||||||
'02bf07e0ac8b5bc6162cf94697b3c27c' +
|
|
||||||
'd1fea49b27f23ba2901871962506520c' +
|
|
||||||
'392da8b6ad0d99f7013fbc06c2c17a56' +
|
|
||||||
'9500c8a7696481c1cd33e9b14e40b82e' +
|
|
||||||
'79a5f5db82571ba97bae3ad3e0479515' +
|
|
||||||
'bb0e2b0f3bfcd1fd33034efc6245eddd' +
|
|
||||||
'7ee2086ddae2600d8ca73e214e8c2b0b' +
|
|
||||||
'db2b047c6a464a562ed77b73d2d841c4' +
|
|
||||||
'b34973551257713b753632efba348169' +
|
|
||||||
'abc90a68f42611a40126d7cb21b58695' +
|
|
||||||
'568186f7e569d2ff0f9e745d0487dd2e' +
|
|
||||||
'b997cafc5abf9dd102e62ff66cba87',
|
|
||||||
signature:
|
|
||||||
'e301345a41a39a4d72fff8df69c98075' +
|
|
||||||
'a0cc082b802fc9b2b6bc503f926b65bd' +
|
|
||||||
'df7f4c8f1cb49f6396afc8a70abe6d8a' +
|
|
||||||
'ef0db478d4c6b2970076c6a0484fe76d' +
|
|
||||||
'76b3a97625d79f1ce240e7c576750d29' +
|
|
||||||
'5528286f719b413de9ada3e8eb78ed57' +
|
|
||||||
'3603ce30d8bb761785dc30dbc320869e' +
|
|
||||||
'1a00',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < VECTORS_RFC8032.length; i++) {
|
|
||||||
const v = VECTORS_RFC8032[i];
|
|
||||||
should(`RFC8032/${i}`, () => {
|
|
||||||
deepStrictEqual(hex(ed.getPublicKey(v.secretKey)), v.publicKey);
|
|
||||||
deepStrictEqual(hex(ed.sign(v.message, v.secretKey)), v.signature);
|
|
||||||
deepStrictEqual(ed.verify(v.signature, v.message, v.publicKey), true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
should('ed448/should not accept >57byte private keys', async () => {
|
|
||||||
const invalidPriv =
|
|
||||||
100000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800073278156000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000n;
|
|
||||||
throws(() => ed.getPublicKey(invalidPriv));
|
|
||||||
});
|
|
||||||
|
|
||||||
function to57Bytes(numOrStr) {
|
|
||||||
let hex = typeof numOrStr === 'string' ? numOrStr : numOrStr.toString(16);
|
|
||||||
return hexToBytes(hex.padStart(114, '0'));
|
|
||||||
}
|
|
||||||
|
|
||||||
should('ed448/should verify recent signature', () => {
|
|
||||||
fc.assert(
|
|
||||||
fc.property(
|
|
||||||
fc.hexaString({ minLength: 2, maxLength: 57 }),
|
|
||||||
fc.bigInt(2n, ed.CURVE.n),
|
|
||||||
(message, privateKey) => {
|
|
||||||
const publicKey = ed.getPublicKey(to57Bytes(privateKey));
|
|
||||||
const signature = ed.sign(to57Bytes(message), to57Bytes(privateKey));
|
|
||||||
deepStrictEqual(publicKey.length, 57);
|
|
||||||
deepStrictEqual(signature.length, 114);
|
|
||||||
deepStrictEqual(ed.verify(signature, to57Bytes(message), publicKey), true);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{ numRuns: 5 }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
should('ed448/should not verify signature with wrong message', () => {
|
|
||||||
fc.assert(
|
|
||||||
fc.property(
|
|
||||||
fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
|
||||||
fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
|
||||||
fc.bigInt(1n, ed.CURVE.n),
|
|
||||||
(bytes, wrongBytes, privateKey) => {
|
|
||||||
const message = new Uint8Array(bytes);
|
|
||||||
const wrongMessage = new Uint8Array(wrongBytes);
|
|
||||||
const priv = to57Bytes(privateKey);
|
|
||||||
const publicKey = ed.getPublicKey(priv);
|
|
||||||
const signature = ed.sign(message, priv);
|
|
||||||
deepStrictEqual(
|
|
||||||
ed.verify(signature, wrongMessage, publicKey),
|
|
||||||
bytes.toString() === wrongBytes.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{ numRuns: 5 }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const privKey = to57Bytes('a665a45920422f9d417e4867ef');
|
|
||||||
const msg = hexToBytes('874f9960c5d2b7a9b5fad383e1ba44719ebb743a');
|
|
||||||
const wrongMsg = hexToBytes('589d8c7f1da0a24bc07b7381ad48b1cfc211af1c');
|
|
||||||
should('ed25519/basic methods/should sign and verify', () => {
|
|
||||||
const publicKey = ed.getPublicKey(privKey);
|
|
||||||
const signature = ed.sign(msg, privKey);
|
|
||||||
deepStrictEqual(ed.verify(signature, msg, publicKey), true);
|
|
||||||
});
|
|
||||||
should('ed25519/basic methods/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('ed25519/basic methods/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);
|
|
||||||
});
|
|
||||||
|
|
||||||
should('ed25519/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('ed25519/sync methods/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('ed25519/sync methods/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('ed25519/BASE_POINT.multiply()/should throw Point#multiply on TEST 5', () => {
|
|
||||||
for (const num of [0n, 0, -1n, -1, 1.1]) {
|
|
||||||
throws(() => ed.Point.BASE.multiply(num));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('input immutability: sign/verify are immutable', () => {
|
|
||||||
const privateKey = ed.utils.randomPrivateKey();
|
|
||||||
const publicKey = ed.getPublicKey(privateKey);
|
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
let payload = randomBytes(100);
|
|
||||||
let signature = ed.sign(payload, privateKey);
|
|
||||||
if (!ed.verify(signature, payload, publicKey)) {
|
|
||||||
throw new Error('Signature verification failed');
|
|
||||||
}
|
|
||||||
const signatureCopy = Buffer.alloc(signature.byteLength);
|
|
||||||
signatureCopy.set(signature, 0); // <-- breaks
|
|
||||||
payload = payload.slice();
|
|
||||||
signature = signature.slice();
|
|
||||||
|
|
||||||
if (!ed.verify(signatureCopy, payload, publicKey))
|
|
||||||
throw new Error('Copied signature verification failed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
for (let g = 0; g < ed448vectors.testGroups.length; g++) {
|
|
||||||
const group = ed448vectors.testGroups[g];
|
|
||||||
const key = group.key;
|
|
||||||
should(`Wycheproof/ED448(${g}, public)`, () => {
|
|
||||||
deepStrictEqual(hex(ed.getPublicKey(key.sk)), key.pk);
|
|
||||||
});
|
|
||||||
should(`Wycheproof/ED448`, () => {
|
|
||||||
for (let i = 0; i < group.tests.length; i++) {
|
|
||||||
const v = group.tests[i];
|
|
||||||
const index = `${g}/${i} ${v.comment}`;
|
|
||||||
if (v.result === 'valid' || v.result === 'acceptable') {
|
|
||||||
deepStrictEqual(hex(ed.sign(v.msg, key.sk)), v.sig, index);
|
|
||||||
deepStrictEqual(ed.verify(v.sig, v.msg, key.pk), true, index);
|
|
||||||
} else if (v.result === 'invalid') {
|
|
||||||
let failed = false;
|
|
||||||
try {
|
|
||||||
failed = !ed.verify(v.sig, v.msg, key.pk);
|
|
||||||
} catch (error) {
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
deepStrictEqual(failed, true, index);
|
|
||||||
} else throw new Error('unknown test result');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ECDH
|
|
||||||
const rfc7748Mul = [
|
|
||||||
{
|
|
||||||
scalar:
|
|
||||||
'3d262fddf9ec8e88495266fea19a34d28882acef045104d0d1aae121700a779c984c24f8cdd78fbff44943eba368f54b29259a4f1c600ad3',
|
|
||||||
u: '06fce640fa3487bfda5f6cf2d5263f8aad88334cbd07437f020f08f9814dc031ddbdc38c19c6da2583fa5429db94ada18aa7a7fb4ef8a086',
|
|
||||||
outputU:
|
|
||||||
'ce3e4ff95a60dc6697da1db1d85e6afbdf79b50a2412d7546d5f239fe14fbaadeb445fc66a01b0779d98223961111e21766282f73dd96b6f',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scalar:
|
|
||||||
'203d494428b8399352665ddca42f9de8fef600908e0d461cb021f8c538345dd77c3e4806e25f46d3315c44e0a5b4371282dd2c8d5be3095f',
|
|
||||||
u: '0fbcc2f993cd56d3305b0b7d9e55d4c1a8fb5dbb52f8e9a1e9b6201b165d015894e56c4d3570bee52fe205e28a78b91cdfbde71ce8d157db',
|
|
||||||
outputU:
|
|
||||||
'884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
for (let i = 0; i < rfc7748Mul.length; i++) {
|
|
||||||
const v = rfc7748Mul[i];
|
|
||||||
should(`RFC7748: scalarMult (${i})`, () => {
|
|
||||||
deepStrictEqual(hex(x448.scalarMult(v.u, v.scalar)), v.outputU);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const rfc7748Iter = [
|
|
||||||
{
|
|
||||||
scalar:
|
|
||||||
'3f482c8a9f19b01e6c46ee9711d9dc14fd4bf67af30765c2ae2b846a4d23a8cd0db897086239492caf350b51f833868b9bc2b3bca9cf4113',
|
|
||||||
iters: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scalar:
|
|
||||||
'aa3b4749d55b9daf1e5b00288826c467274ce3ebbdd5c17b975e09d4af6c67cf10d087202db88286e2b79fceea3ec353ef54faa26e219f38',
|
|
||||||
iters: 1000,
|
|
||||||
},
|
|
||||||
// { scalar: '077f453681caca3693198420bbe515cae0002472519b3e67661a7e89cab94695c8f4bcd66e61b9b9c946da8d524de3d69bd9d9d66b997e37', iters: 1000000 },
|
|
||||||
];
|
|
||||||
for (let i = 0; i < rfc7748Iter.length; i++) {
|
|
||||||
const { scalar, iters } = rfc7748Iter[i];
|
|
||||||
should(`RFC7748: scalarMult iteration (${i})`, () => {
|
|
||||||
let k = x448.Gu;
|
|
||||||
for (let i = 0, u = k; i < iters; i++) [k, u] = [x448.scalarMult(u, k), k];
|
|
||||||
deepStrictEqual(hex(k), scalar);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
should('RFC7748 getSharedKey', () => {
|
|
||||||
const alicePrivate =
|
|
||||||
'9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b';
|
|
||||||
const alicePublic =
|
|
||||||
'9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0';
|
|
||||||
const bobPrivate =
|
|
||||||
'1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d';
|
|
||||||
const bobPublic =
|
|
||||||
'3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609';
|
|
||||||
const shared =
|
|
||||||
'07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d';
|
|
||||||
deepStrictEqual(alicePublic, hex(x448.getPublicKey(alicePrivate)));
|
|
||||||
deepStrictEqual(bobPublic, hex(x448.getPublicKey(bobPrivate)));
|
|
||||||
deepStrictEqual(hex(x448.scalarMult(bobPublic, alicePrivate)), shared);
|
|
||||||
deepStrictEqual(hex(x448.scalarMult(alicePublic, bobPrivate)), shared);
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
const group = x448vectors.testGroups[0];
|
|
||||||
should(`Wycheproof/X448`, () => {
|
|
||||||
for (let i = 0; i < group.tests.length; i++) {
|
|
||||||
const v = group.tests[i];
|
|
||||||
const index = `(${i}, ${v.result}) ${v.comment}`;
|
|
||||||
if (v.result === 'valid' || v.result === 'acceptable') {
|
|
||||||
try {
|
|
||||||
const shared = hex(x448.scalarMult(v.public, v.private));
|
|
||||||
deepStrictEqual(shared, v.shared, index);
|
|
||||||
} 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;
|
|
||||||
if (e.message.includes('Expected 56 bytes')) return;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
} else if (v.result === 'invalid') {
|
|
||||||
let failed = false;
|
|
||||||
try {
|
|
||||||
x448.scalarMult(v.public, v.private);
|
|
||||||
} catch (error) {
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
deepStrictEqual(failed, true, index);
|
|
||||||
} else throw new Error('unknown test result');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// should('X448: should convert base point to montgomery using fromPoint', () => {
|
|
||||||
// deepStrictEqual(
|
|
||||||
// hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)),
|
|
||||||
// ed.montgomeryCurve.BASE_POINT_U
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
|
|
||||||
// should('X448/getSharedSecret() should be commutative', async () => {
|
|
||||||
// for (let i = 0; i < 512; i++) {
|
|
||||||
// const asec = ed.utils.randomPrivateKey();
|
|
||||||
// const apub = ed.getPublicKey(asec);
|
|
||||||
// const bsec = ed.utils.randomPrivateKey();
|
|
||||||
// const bpub = ed.getPublicKey(bsec);
|
|
||||||
// try {
|
|
||||||
// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('not commutative', { asec, apub, bsec, bpub });
|
|
||||||
// throw error;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
const VECTORS_RFC8032_CTX = [
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'c4eab05d357007c632f3dbb48489924d552b08fe0c353a0d4a1f00acda2c463afbea67c5e8d2877c5e3bc397a659949ef8021e954e0a12274e',
|
|
||||||
publicKey:
|
|
||||||
'43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a9480',
|
|
||||||
message: '03',
|
|
||||||
context: '666f6f',
|
|
||||||
signature:
|
|
||||||
'd4f8f6131770dd46f40867d6fd5d5055' +
|
|
||||||
'de43541f8c5e35abbcd001b32a89f7d2' +
|
|
||||||
'151f7647f11d8ca2ae279fb842d60721' +
|
|
||||||
'7fce6e042f6815ea000c85741de5c8da' +
|
|
||||||
'1144a6a1aba7f96de42505d7a7298524' +
|
|
||||||
'fda538fccbbb754f578c1cad10d54d0d' +
|
|
||||||
'5428407e85dcbc98a49155c13764e66c' +
|
|
||||||
'3c00',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < VECTORS_RFC8032_CTX.length; i++) {
|
|
||||||
const v = VECTORS_RFC8032_CTX[i];
|
|
||||||
should(`RFC8032ctx/${i}`, () => {
|
|
||||||
deepStrictEqual(hex(ed.getPublicKey(v.secretKey)), v.publicKey);
|
|
||||||
deepStrictEqual(hex(ed.sign(v.message, v.secretKey, v.context)), v.signature);
|
|
||||||
deepStrictEqual(ed.verify(v.signature, v.message, v.publicKey, v.context), true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const VECTORS_RFC8032_PH = [
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42ef7822e0d5104127dc05d6dbefde69e3ab2cec7c867c6e2c49',
|
|
||||||
publicKey:
|
|
||||||
'259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743c09428a131d6b1b57303d90d8132c276d5ed3d5d01c0f53880',
|
|
||||||
message: '616263',
|
|
||||||
signature:
|
|
||||||
'822f6901f7480f3d5f562c592994d969' +
|
|
||||||
'3602875614483256505600bbc281ae38' +
|
|
||||||
'1f54d6bce2ea911574932f52a4e6cadd' +
|
|
||||||
'78769375ec3ffd1b801a0d9b3f4030cd' +
|
|
||||||
'433964b6457ea39476511214f97469b5' +
|
|
||||||
'7dd32dbc560a9a94d00bff07620464a3' +
|
|
||||||
'ad203df7dc7ce360c3cd3696d9d9fab9' +
|
|
||||||
'0f00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42ef7822e0d5104127dc05d6dbefde69e3ab2cec7c867c6e2c49',
|
|
||||||
publicKey:
|
|
||||||
'259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743c09428a131d6b1b57303d90d8132c276d5ed3d5d01c0f53880',
|
|
||||||
message: '616263',
|
|
||||||
context: '666f6f',
|
|
||||||
signature:
|
|
||||||
'c32299d46ec8ff02b54540982814dce9' +
|
|
||||||
'a05812f81962b649d528095916a2aa48' +
|
|
||||||
'1065b1580423ef927ecf0af5888f90da' +
|
|
||||||
'0f6a9a85ad5dc3f280d91224ba9911a3' +
|
|
||||||
'653d00e484e2ce232521481c8658df30' +
|
|
||||||
'4bb7745a73514cdb9bf3e15784ab7128' +
|
|
||||||
'4f8d0704a608c54a6b62d97beb511d13' +
|
|
||||||
'2100',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < VECTORS_RFC8032_PH.length; i++) {
|
|
||||||
const v = VECTORS_RFC8032_PH[i];
|
|
||||||
should(`RFC8032ph/${i}`, () => {
|
|
||||||
deepStrictEqual(hex(ed448ph.getPublicKey(v.secretKey)), v.publicKey);
|
|
||||||
deepStrictEqual(hex(ed448ph.sign(v.message, v.secretKey, v.context)), v.signature);
|
|
||||||
deepStrictEqual(ed448ph.verify(v.signature, v.message, v.publicKey, v.context), true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
should('X448 base point', () => {
|
|
||||||
const { x, y } = ed448.Point.BASE;
|
|
||||||
const { P } = ed448.CURVE;
|
|
||||||
const invX = ed448.utils.invert(x * x, P); // x^2
|
|
||||||
const u = ed448.utils.mod(y * y * invX, P); // (y^2/x^2)
|
|
||||||
deepStrictEqual(hex(numberToBytesLE(u, 56)), x448.Gu);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ESM is broken.
|
|
||||||
import url from 'url';
|
|
||||||
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
|
||||||
should.run();
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { jubjub, findGroupHash } from '../lib/jubjub.js';
|
|
||||||
import { should } from 'micro-should';
|
|
||||||
import { deepStrictEqual, throws } from 'assert';
|
|
||||||
import { hexToBytes, bytesToHex } from '@noble/hashes/utils';
|
|
||||||
|
|
||||||
const G_SPEND = new jubjub.ExtendedPoint(
|
|
||||||
0x055f1f24f0f0512287e51c3c5a0a6903fc0baf8711de9eafd7c0e66f69d8d2dbn,
|
|
||||||
0x566178b2505fdd52132a5007d80a04652842e78ffb376897588f406278214ed7n,
|
|
||||||
0x0141fafa1f11088a3b2007c14d652375888f3b37838ba6bdffae096741ceddfen,
|
|
||||||
0x12eada93c0b7d595f5f04f5ebfb4b7d033ef2884136475cab5e41ce17db5be9cn
|
|
||||||
);
|
|
||||||
const G_PROOF = new jubjub.ExtendedPoint(
|
|
||||||
0x0174d54ce9fad258a2f8a86a1deabf15c7a2b51106b0fbcd9d29020f78936f71n,
|
|
||||||
0x16871d6d877dcd222e4ec3bccb3f37cb1865a2d37dd3a5dcbc032a69b62b4445n,
|
|
||||||
0x57a3cd31e496d82bd4aa78bd5ecd751cfb76d54a5d3f4560866379f9fc11c9b3n,
|
|
||||||
0x42cc53f6b519d1f4f52c47ff1256463a616c2c2f49ffe77765481eca04c72081n
|
|
||||||
);
|
|
||||||
|
|
||||||
const getXY = (p) => ({ x: p.x, y: p.y });
|
|
||||||
|
|
||||||
should('toHex/fromHex', () => {
|
|
||||||
// More than field
|
|
||||||
throws(() =>
|
|
||||||
jubjub.Point.fromHex(
|
|
||||||
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,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
// Multiplicative generator (sqrt == null), not on curve.
|
|
||||||
throws(() =>
|
|
||||||
jubjub.Point.fromHex(
|
|
||||||
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, 0,
|
|
||||||
0,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const tmp = jubjub.Point.fromHex(
|
|
||||||
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,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
deepStrictEqual(tmp.x, 0x8d51ccce760304d0ec030002760300000001000000000000n);
|
|
||||||
deepStrictEqual(tmp.y, 0n);
|
|
||||||
|
|
||||||
const S = G_SPEND.toAffine().toRawBytes();
|
|
||||||
const S2 = G_SPEND.double().toAffine().toRawBytes();
|
|
||||||
const P = G_PROOF.toAffine().toRawBytes();
|
|
||||||
const P2 = G_PROOF.double().toAffine().toRawBytes();
|
|
||||||
const S_exp = jubjub.Point.fromHex(S);
|
|
||||||
const S2_exp = jubjub.Point.fromHex(S2);
|
|
||||||
const P_exp = jubjub.Point.fromHex(P);
|
|
||||||
const P2_exp = jubjub.Point.fromHex(P2);
|
|
||||||
deepStrictEqual(getXY(G_SPEND.toAffine()), getXY(S_exp));
|
|
||||||
deepStrictEqual(getXY(G_SPEND.double().toAffine()), getXY(S2_exp));
|
|
||||||
deepStrictEqual(getXY(G_PROOF.toAffine()), getXY(P_exp));
|
|
||||||
deepStrictEqual(getXY(G_PROOF.double().toAffine()), getXY(P2_exp));
|
|
||||||
});
|
|
||||||
|
|
||||||
should('Find generators', () => {
|
|
||||||
const spend = findGroupHash(new Uint8Array(), new Uint8Array([90, 99, 97, 115, 104, 95, 71, 95]));
|
|
||||||
const proof = findGroupHash(new Uint8Array(), new Uint8Array([90, 99, 97, 115, 104, 95, 72, 95]));
|
|
||||||
deepStrictEqual(getXY(spend.toAffine()), getXY(G_SPEND.toAffine()));
|
|
||||||
deepStrictEqual(getXY(proof.toAffine()), getXY(G_PROOF.toAffine()));
|
|
||||||
});
|
|
||||||
|
|
||||||
// ESM is broken.
|
|
||||||
import url from 'url';
|
|
||||||
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
|
||||||
should.run();
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "module",
|
|
||||||
"browser": {
|
|
||||||
"crypto": false,
|
|
||||||
"./crypto": "./esm/cryptoBrowser.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,535 +0,0 @@
|
|||||||
import * as fc from 'fast-check';
|
|
||||||
import { secp256k1, schnorr } from '../lib/secp256k1.js';
|
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
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 privates } from './vectors/privates.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 { should } from 'micro-should';
|
|
||||||
import { deepStrictEqual, throws } from 'assert';
|
|
||||||
import { hexToBytes, bytesToHex } from '@noble/hashes/utils';
|
|
||||||
|
|
||||||
const hex = bytesToHex;
|
|
||||||
const secp = secp256k1;
|
|
||||||
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 P = secp.CURVE.Fp.ORDER;
|
|
||||||
// prettier-ignore
|
|
||||||
const INVALID_ITEMS = ['deadbeef', Math.pow(2, 53), [1], 'xyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxy', secp.CURVE.n + 2n];
|
|
||||||
|
|
||||||
const toBEHex = (n) => n.toString(16).padStart(64, '0');
|
|
||||||
|
|
||||||
function hexToNumber(hex) {
|
|
||||||
if (typeof hex !== 'string') {
|
|
||||||
throw new TypeError('hexToNumber: expected string, got ' + typeof hex);
|
|
||||||
}
|
|
||||||
// Big Endian
|
|
||||||
return BigInt(`0x${hex}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
should('secp256k1.getPublicKey()', () => {
|
|
||||||
const data = privatesTxt
|
|
||||||
.split('\n')
|
|
||||||
.filter((line) => line)
|
|
||||||
.map((line) => line.split(':'));
|
|
||||||
for (let [priv, x, y] of data) {
|
|
||||||
const point = secp.Point.fromPrivateKey(BigInt(priv));
|
|
||||||
deepStrictEqual(toBEHex(point.x), x);
|
|
||||||
deepStrictEqual(toBEHex(point.y), y);
|
|
||||||
|
|
||||||
const point2 = secp.Point.fromHex(secp.getPublicKey(toBEHex(BigInt(priv))));
|
|
||||||
deepStrictEqual(toBEHex(point2.x), x);
|
|
||||||
deepStrictEqual(toBEHex(point2.y), y);
|
|
||||||
|
|
||||||
const point3 = secp.Point.fromHex(secp.getPublicKey(hexToBytes(toBEHex(BigInt(priv)))));
|
|
||||||
deepStrictEqual(toBEHex(point3.x), x);
|
|
||||||
deepStrictEqual(toBEHex(point3.y), y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('secp256k1.getPublicKey() rejects invalid keys', () => {
|
|
||||||
// for (const item of INVALID_ITEMS) {
|
|
||||||
// throws(() => secp.getPublicKey(item));
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
should('secp256k1.precompute', () => {
|
|
||||||
secp.utils.precompute(4);
|
|
||||||
const data = privatesTxt
|
|
||||||
.split('\n')
|
|
||||||
.filter((line) => line)
|
|
||||||
.map((line) => line.split(':'));
|
|
||||||
for (let [priv, x, y] of data) {
|
|
||||||
const point = secp.Point.fromPrivateKey(BigInt(priv));
|
|
||||||
deepStrictEqual(toBEHex(point.x), x);
|
|
||||||
deepStrictEqual(toBEHex(point.y), y);
|
|
||||||
|
|
||||||
const point2 = secp.Point.fromHex(secp.getPublicKey(toBEHex(BigInt(priv))));
|
|
||||||
deepStrictEqual(toBEHex(point2.x), x);
|
|
||||||
deepStrictEqual(toBEHex(point2.y), y);
|
|
||||||
|
|
||||||
const point3 = secp.Point.fromHex(secp.getPublicKey(hexToBytes(toBEHex(BigInt(priv)))));
|
|
||||||
deepStrictEqual(toBEHex(point3.x), x);
|
|
||||||
deepStrictEqual(toBEHex(point3.y), y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.Point.isValidPoint()', () => {
|
|
||||||
for (const vector of points.valid.isPoint) {
|
|
||||||
const { P, expected } = vector;
|
|
||||||
if (expected) {
|
|
||||||
secp.Point.fromHex(P);
|
|
||||||
} else {
|
|
||||||
throws(() => secp.Point.fromHex(P));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.Point.fromPrivateKey()', () => {
|
|
||||||
for (const vector of points.valid.pointFromScalar) {
|
|
||||||
const { d, expected } = vector;
|
|
||||||
let p = secp.Point.fromPrivateKey(d);
|
|
||||||
deepStrictEqual(p.toHex(true), expected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.Point#toHex(compressed)', () => {
|
|
||||||
for (const vector of points.valid.pointCompress) {
|
|
||||||
const { P, compress, expected } = vector;
|
|
||||||
let p = secp.Point.fromHex(P);
|
|
||||||
deepStrictEqual(p.toHex(compress), expected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.Point#toHex() roundtrip (failed case)', () => {
|
|
||||||
const point1 =
|
|
||||||
secp.Point.fromPrivateKey(
|
|
||||||
88572218780422190464634044548753414301110513745532121983949500266768436236425n
|
|
||||||
);
|
|
||||||
// const hex = point1.toHex(true);
|
|
||||||
// deepStrictEqual(secp.Point.fromHex(hex).toHex(true), hex);
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.Point#toHex() roundtrip', () => {
|
|
||||||
fc.assert(
|
|
||||||
fc.property(FC_BIGINT, (x) => {
|
|
||||||
const point1 = secp.Point.fromPrivateKey(x);
|
|
||||||
const hex = point1.toHex(true);
|
|
||||||
deepStrictEqual(secp.Point.fromHex(hex).toHex(true), hex);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.Point#add(other)', () => {
|
|
||||||
for (const vector of points.valid.pointAdd) {
|
|
||||||
const { P, Q, expected } = vector;
|
|
||||||
let p = secp.Point.fromHex(P);
|
|
||||||
let q = secp.Point.fromHex(Q);
|
|
||||||
if (expected) {
|
|
||||||
deepStrictEqual(p.add(q).toHex(true), expected);
|
|
||||||
} else {
|
|
||||||
if (!p.equals(q.negate())) {
|
|
||||||
throws(() => p.add(q).toHex(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.Point#multiply(privateKey)', () => {
|
|
||||||
for (const vector of points.valid.pointMultiply) {
|
|
||||||
const { P, d, expected } = vector;
|
|
||||||
const p = secp.Point.fromHex(P);
|
|
||||||
if (expected) {
|
|
||||||
deepStrictEqual(p.multiply(hexToNumber(d)).toHex(true), expected);
|
|
||||||
} else {
|
|
||||||
throws(() => {
|
|
||||||
p.multiply(hexToNumber(d)).toHex(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const vector of points.invalid.pointMultiply) {
|
|
||||||
const { P, d } = vector;
|
|
||||||
if (hexToNumber(d) < secp.CURVE.n) {
|
|
||||||
throws(() => {
|
|
||||||
const p = secp.Point.fromHex(P);
|
|
||||||
p.multiply(hexToNumber(d)).toHex(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const num of [0n, 0, -1n, -1, 1.1]) {
|
|
||||||
throws(() => secp.Point.BASE.multiply(num));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// multiply() should equal multiplyUnsafe()
|
|
||||||
// should('JacobianPoint#multiplyUnsafe', () => {
|
|
||||||
// const p0 = new secp.JacobianPoint(
|
|
||||||
// 55066263022277343669578718895168534326250603453777594175500187360389116729240n,
|
|
||||||
// 32670510020758816978083085130507043184471273380659243275938904335757337482424n,
|
|
||||||
// 1n
|
|
||||||
// );
|
|
||||||
// const z = 106011723082030650010038151861333186846790370053628296836951575624442507889495n;
|
|
||||||
// console.log(p0.multiply(z));
|
|
||||||
// console.log(secp.JacobianPoint.normalizeZ([p0.multiplyUnsafe(z)])[0])
|
|
||||||
// });
|
|
||||||
|
|
||||||
should('secp256k1.Signature.fromCompactHex() roundtrip', () => {
|
|
||||||
fc.assert(
|
|
||||||
fc.property(FC_BIGINT, FC_BIGINT, (r, s) => {
|
|
||||||
const sig = new secp.Signature(r, s);
|
|
||||||
deepStrictEqual(secp.Signature.fromCompact(sig.toCompactHex()), sig);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.Signature.fromDERHex() roundtrip', () => {
|
|
||||||
fc.assert(
|
|
||||||
fc.property(FC_BIGINT, FC_BIGINT, (r, s) => {
|
|
||||||
const sig = new secp.Signature(r, s);
|
|
||||||
deepStrictEqual(secp.Signature.fromDER(sig.toDERHex()), sig);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.sign()/should create deterministic signatures with RFC 6979', () => {
|
|
||||||
for (const vector of ecdsa.valid) {
|
|
||||||
let usig = secp.sign(vector.m, vector.d);
|
|
||||||
let sig = usig.toCompactHex();
|
|
||||||
const vsig = vector.signature;
|
|
||||||
deepStrictEqual(sig.slice(0, 64), vsig.slice(0, 64));
|
|
||||||
deepStrictEqual(sig.slice(64, 128), vsig.slice(64, 128));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.sign()/should not create invalid deterministic signatures with RFC 6979', () => {
|
|
||||||
for (const vector of ecdsa.invalid.sign) {
|
|
||||||
throws(() => secp.sign(vector.m, vector.d));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.sign()/edge cases', () => {
|
|
||||||
throws(() => secp.sign());
|
|
||||||
throws(() => secp.sign(''));
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.sign()/should create correct DER encoding against libsecp256k1', () => {
|
|
||||||
const CASES = [
|
|
||||||
[
|
|
||||||
'd1a9dc8ed4e46a6a3e5e594615ca351d7d7ef44df1e4c94c1802f3592183794b',
|
|
||||||
'304402203de2559fccb00c148574997f660e4d6f40605acc71267ee38101abf15ff467af02200950abdf40628fd13f547792ba2fc544681a485f2fdafb5c3b909a4df7350e6b',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'5f97983254982546d3976d905c6165033976ee449d300d0e382099fa74deaf82',
|
|
||||||
'3045022100c046d9ff0bd2845b9aa9dff9f997ecebb31e52349f80fe5a5a869747d31dcb88022011f72be2a6d48fe716b825e4117747b397783df26914a58139c3f4c5cbb0e66c',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'0d7017a96b97cd9be21cf28aada639827b2814a654a478c81945857196187808',
|
|
||||||
'3045022100d18990bba7832bb283e3ecf8700b67beb39acc73f4200ed1c331247c46edccc602202e5c8bbfe47ae159512c583b30a3fa86575cddc62527a03de7756517ae4c6c73',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
const privKey = hexToBytes('0101010101010101010101010101010101010101010101010101010101010101');
|
|
||||||
for (const [msg, exp] of CASES) {
|
|
||||||
const res = secp.sign(msg, privKey, { extraEntropy: undefined });
|
|
||||||
deepStrictEqual(res.toDERHex(), exp);
|
|
||||||
const rs = secp.Signature.fromDER(res.toDERHex()).toCompactHex();
|
|
||||||
deepStrictEqual(secp.Signature.fromCompact(rs).toDERHex(), exp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('secp256k1.sign()/sign ecdsa extraData', () => {
|
|
||||||
const ent1 = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
||||||
const ent2 = '0000000000000000000000000000000000000000000000000000000000000001';
|
|
||||||
const ent3 = '6e723d3fd94ed5d2b6bdd4f123364b0f3ca52af829988a63f8afe91d29db1c33';
|
|
||||||
const ent4 = 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141';
|
|
||||||
const ent5 = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
|
|
||||||
|
|
||||||
for (const e of ecdsa.extraEntropy) {
|
|
||||||
const sign = (extraEntropy) => {
|
|
||||||
const s = secp.sign(e.m, e.d, { extraEntropy }).toCompactHex();
|
|
||||||
return s;
|
|
||||||
};
|
|
||||||
deepStrictEqual(sign(), e.signature);
|
|
||||||
deepStrictEqual(sign(ent1), e.extraEntropy0);
|
|
||||||
deepStrictEqual(sign(ent2), e.extraEntropy1);
|
|
||||||
deepStrictEqual(sign(ent3), e.extraEntropyRand);
|
|
||||||
deepStrictEqual(sign(ent4), e.extraEntropyN);
|
|
||||||
deepStrictEqual(sign(ent5), e.extraEntropyMax);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.verify()/should verify signature', () => {
|
|
||||||
const MSG = '01'.repeat(32);
|
|
||||||
const PRIV_KEY = 0x2n;
|
|
||||||
const signature = secp.sign(MSG, PRIV_KEY);
|
|
||||||
const publicKey = secp.getPublicKey(PRIV_KEY);
|
|
||||||
deepStrictEqual(publicKey.length, 65);
|
|
||||||
deepStrictEqual(secp.verify(signature, MSG, publicKey), true);
|
|
||||||
});
|
|
||||||
should('secp256k1.verify()/should not verify signature with wrong public key', () => {
|
|
||||||
const MSG = '01'.repeat(32);
|
|
||||||
const PRIV_KEY = 0x2n;
|
|
||||||
const WRONG_PRIV_KEY = 0x22n;
|
|
||||||
const signature = secp.sign(MSG, PRIV_KEY);
|
|
||||||
const publicKey = secp.Point.fromPrivateKey(WRONG_PRIV_KEY).toHex();
|
|
||||||
deepStrictEqual(publicKey.length, 130);
|
|
||||||
deepStrictEqual(secp.verify(signature, MSG, publicKey), false);
|
|
||||||
});
|
|
||||||
should('secp256k1.verify()/should not verify signature with wrong hash', () => {
|
|
||||||
const MSG = '01'.repeat(32);
|
|
||||||
const PRIV_KEY = 0x2n;
|
|
||||||
const WRONG_MSG = '11'.repeat(32);
|
|
||||||
const signature = secp.sign(MSG, PRIV_KEY);
|
|
||||||
const publicKey = secp.getPublicKey(PRIV_KEY);
|
|
||||||
deepStrictEqual(publicKey.length, 65);
|
|
||||||
deepStrictEqual(secp.verify(signature, WRONG_MSG, publicKey), false);
|
|
||||||
});
|
|
||||||
should('secp256k1.verify()/should verify random signatures', () =>
|
|
||||||
fc.assert(
|
|
||||||
fc.property(FC_BIGINT, fc.hexaString({ minLength: 64, maxLength: 64 }), (privKey, msg) => {
|
|
||||||
const pub = secp.getPublicKey(privKey);
|
|
||||||
const sig = secp.sign(msg, privKey);
|
|
||||||
deepStrictEqual(secp.verify(sig, msg, pub), true);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
should('secp256k1.verify()/should not verify signature with invalid r/s', () => {
|
|
||||||
const msg = new Uint8Array([
|
|
||||||
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, 0x50, 0x23,
|
|
||||||
]);
|
|
||||||
const x = 100260381870027870612475458630405506840396644859280795015145920502443964769584n;
|
|
||||||
const y = 41096923727651821103518389640356553930186852801619204169823347832429067794568n;
|
|
||||||
const r = 1n;
|
|
||||||
const s = 115792089237316195423570985008687907852837564279074904382605163141518162728904n;
|
|
||||||
|
|
||||||
const pub = new secp.Point(x, y);
|
|
||||||
const signature = new secp.Signature(2n, 2n);
|
|
||||||
signature.r = r;
|
|
||||||
signature.s = s;
|
|
||||||
|
|
||||||
const verified = secp.verify(signature, msg, pub);
|
|
||||||
// Verifies, but it shouldn't, because signature S > curve order
|
|
||||||
deepStrictEqual(verified, false);
|
|
||||||
});
|
|
||||||
should('secp256k1.verify()/should not verify msg = curve order', () => {
|
|
||||||
const msg = 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141';
|
|
||||||
const x = 55066263022277343669578718895168534326250603453777594175500187360389116729240n;
|
|
||||||
const y = 32670510020758816978083085130507043184471273380659243275938904335757337482424n;
|
|
||||||
const r = 104546003225722045112039007203142344920046999340768276760147352389092131869133n;
|
|
||||||
const s = 96900796730960181123786672629079577025401317267213807243199432755332205217369n;
|
|
||||||
const pub = new secp.Point(x, y);
|
|
||||||
const sig = new secp.Signature(r, s);
|
|
||||||
deepStrictEqual(secp.verify(sig, msg, pub), false);
|
|
||||||
});
|
|
||||||
should('secp256k1.verify()/should verify non-strict msg bb5a...', () => {
|
|
||||||
const msg = 'bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023';
|
|
||||||
const x = 3252872872578928810725465493269682203671229454553002637820453004368632726370n;
|
|
||||||
const y = 17482644437196207387910659778872952193236850502325156318830589868678978890912n;
|
|
||||||
const r = 432420386565659656852420866390673177323n;
|
|
||||||
const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n;
|
|
||||||
const pub = new secp.Point(x, y);
|
|
||||||
const sig = new secp.Signature(r, s);
|
|
||||||
deepStrictEqual(secp.verify(sig, msg, pub, { strict: false }), true);
|
|
||||||
});
|
|
||||||
should(
|
|
||||||
'secp256k1.verify()/should not verify invalid deterministic signatures with RFC 6979',
|
|
||||||
() => {
|
|
||||||
for (const vector of ecdsa.invalid.verify) {
|
|
||||||
const res = secp.verify(vector.signature, vector.m, vector.Q);
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
should('secp256k1.recoverPublicKey()/should recover public key from recovery bit', () => {
|
|
||||||
const message = '00000000000000000000000000000000000000000000000000000000deadbeef';
|
|
||||||
const privateKey = 123456789n;
|
|
||||||
const publicKey = secp.Point.fromHex(secp.getPublicKey(privateKey)).toHex(false);
|
|
||||||
const sig = secp.sign(message, privateKey);
|
|
||||||
const recoveredPubkey = sig.recoverPublicKey(message);
|
|
||||||
// const recoveredPubkey = secp.recoverPublicKey(message, signature, recovery);
|
|
||||||
deepStrictEqual(recoveredPubkey !== null, true);
|
|
||||||
deepStrictEqual(recoveredPubkey.toHex(), publicKey);
|
|
||||||
deepStrictEqual(secp.verify(sig, message, publicKey), true);
|
|
||||||
});
|
|
||||||
should('secp256k1.recoverPublicKey()/should not recover zero points', () => {
|
|
||||||
const msgHash = '6b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9';
|
|
||||||
const sig =
|
|
||||||
'79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817986b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9';
|
|
||||||
const recovery = 0;
|
|
||||||
throws(() => secp.recoverPublicKey(msgHash, sig, recovery));
|
|
||||||
});
|
|
||||||
should('secp256k1.recoverPublicKey()/should handle all-zeros msghash', () => {
|
|
||||||
const privKey = secp.utils.randomPrivateKey();
|
|
||||||
const pub = secp.getPublicKey(privKey);
|
|
||||||
const zeros = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
||||||
const sig = secp.sign(zeros, privKey, { recovered: true });
|
|
||||||
const recoveredKey = sig.recoverPublicKey(zeros);
|
|
||||||
deepStrictEqual(recoveredKey.toRawBytes(), pub);
|
|
||||||
});
|
|
||||||
should('secp256k1.recoverPublicKey()/should handle RFC 6979 vectors', () => {
|
|
||||||
for (const vector of ecdsa.valid) {
|
|
||||||
if (secp.utils.mod(hexToNumber(vector.m), secp.CURVE.n) === 0n) continue;
|
|
||||||
let usig = secp.sign(vector.m, vector.d);
|
|
||||||
let sig = usig.toDERHex();
|
|
||||||
const vpub = secp.getPublicKey(vector.d);
|
|
||||||
const recovered = usig.recoverPublicKey(vector.m);
|
|
||||||
deepStrictEqual(recovered.toHex(), hex(vpub));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Real implementation.
|
|
||||||
function derToPub(der) {
|
|
||||||
return der.slice(46);
|
|
||||||
}
|
|
||||||
should('secp256k1.getSharedSecret()/should produce correct results', () => {
|
|
||||||
// TODO: Once der is there, run all tests.
|
|
||||||
for (const vector of ecdh.testGroups[0].tests.slice(0, 230)) {
|
|
||||||
if (vector.result === 'invalid' || vector.private.length !== 64) {
|
|
||||||
// We support eth-like hexes
|
|
||||||
if (vector.private.length < 64) continue;
|
|
||||||
throws(() => {
|
|
||||||
secp.getSharedSecret(vector.private, derToPub(vector.public), true);
|
|
||||||
});
|
|
||||||
} else if (vector.result === 'valid') {
|
|
||||||
const res = secp.getSharedSecret(vector.private, derToPub(vector.public), true);
|
|
||||||
deepStrictEqual(hex(res.slice(1)), `${vector.shared}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('secp256k1.getSharedSecret()/priv/pub order matters', () => {
|
|
||||||
for (const vector of ecdh.testGroups[0].tests.slice(0, 100)) {
|
|
||||||
if (vector.result === 'valid') {
|
|
||||||
let priv = vector.private;
|
|
||||||
priv = priv.length === 66 ? priv.slice(2) : priv;
|
|
||||||
throws(() => secp.getSharedSecret(derToPub(vector.public), priv, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('secp256k1.getSharedSecret()/rejects invalid keys', () => {
|
|
||||||
throws(() => secp.getSharedSecret('01', '02'));
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.utils.isValidPrivateKey()', () => {
|
|
||||||
for (const vector of privates.valid.isPrivate) {
|
|
||||||
const { d, expected } = vector;
|
|
||||||
deepStrictEqual(secp.utils.isValidPrivateKey(d), expected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const normal = secp.utils._normalizePrivateKey;
|
|
||||||
const tweakUtils = {
|
|
||||||
privateAdd: (privateKey, tweak) => {
|
|
||||||
const p = normal(privateKey);
|
|
||||||
const t = normal(tweak);
|
|
||||||
return secp.utils._bigintToBytes(secp.utils.mod(p + t, secp.CURVE.n));
|
|
||||||
},
|
|
||||||
|
|
||||||
privateNegate: (privateKey) => {
|
|
||||||
const p = normal(privateKey);
|
|
||||||
return secp.utils._bigintToBytes(secp.CURVE.n - p);
|
|
||||||
},
|
|
||||||
|
|
||||||
pointAddScalar: (p, tweak, isCompressed) => {
|
|
||||||
const P = secp.Point.fromHex(p);
|
|
||||||
const t = normal(tweak);
|
|
||||||
const Q = secp.Point.BASE.multiplyAndAddUnsafe(P, t, 1n);
|
|
||||||
if (!Q) throw new Error('Tweaked point at infinity');
|
|
||||||
return Q.toRawBytes(isCompressed);
|
|
||||||
},
|
|
||||||
|
|
||||||
pointMultiply: (p, tweak, isCompressed) => {
|
|
||||||
const P = secp.Point.fromHex(p);
|
|
||||||
const h = typeof tweak === 'string' ? tweak : bytesToHex(tweak);
|
|
||||||
const t = BigInt(`0x${h}`);
|
|
||||||
return P.multiply(t).toRawBytes(isCompressed);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
should('secp256k1.privateAdd()', () => {
|
|
||||||
for (const vector of privates.valid.add) {
|
|
||||||
const { a, b, expected } = vector;
|
|
||||||
deepStrictEqual(bytesToHex(tweakUtils.privateAdd(a, b)), expected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('secp256k1.privateNegate()', () => {
|
|
||||||
for (const vector of privates.valid.negate) {
|
|
||||||
const { a, expected } = vector;
|
|
||||||
deepStrictEqual(bytesToHex(tweakUtils.privateNegate(a)), expected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('secp256k1.pointAddScalar()', () => {
|
|
||||||
for (const vector of points.valid.pointAddScalar) {
|
|
||||||
const { description, P, d, expected } = vector;
|
|
||||||
const compressed = !!expected && expected.length === 66; // compressed === 33 bytes
|
|
||||||
deepStrictEqual(bytesToHex(tweakUtils.pointAddScalar(P, d, compressed)), expected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('secp256k1.pointAddScalar() invalid', () => {
|
|
||||||
for (const vector of points.invalid.pointAddScalar) {
|
|
||||||
const { P, d, exception } = vector;
|
|
||||||
throws(() => tweakUtils.pointAddScalar(P, d));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('secp256k1.pointMultiply()', () => {
|
|
||||||
for (const vector of points.valid.pointMultiply) {
|
|
||||||
const { P, d, expected } = vector;
|
|
||||||
deepStrictEqual(bytesToHex(tweakUtils.pointMultiply(P, d, true)), expected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('secp256k1.pointMultiply() invalid', () => {
|
|
||||||
for (const vector of points.invalid.pointMultiply) {
|
|
||||||
const { P, d, exception } = vector;
|
|
||||||
throws(() => tweakUtils.pointMultiply(P, d));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('secp256k1.wychenproof vectors', () => {
|
|
||||||
for (let group of wp.testGroups) {
|
|
||||||
const pubKey = secp.Point.fromHex(group.key.uncompressed);
|
|
||||||
for (let test of group.tests) {
|
|
||||||
const m = secp.CURVE.hash(hexToBytes(test.msg));
|
|
||||||
if (test.result === 'valid' || test.result === 'acceptable') {
|
|
||||||
const verified = secp.verify(test.sig, m, pubKey);
|
|
||||||
if (secp.Signature.fromDER(test.sig).hasHighS()) {
|
|
||||||
deepStrictEqual(verified, false);
|
|
||||||
} else {
|
|
||||||
deepStrictEqual(verified, true);
|
|
||||||
}
|
|
||||||
} else if (test.result === 'invalid') {
|
|
||||||
let failed = false;
|
|
||||||
try {
|
|
||||||
const verified = secp.verify(test.sig, m, pubKey);
|
|
||||||
if (!verified) failed = true;
|
|
||||||
} catch (error) {
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
deepStrictEqual(failed, true);
|
|
||||||
} else {
|
|
||||||
deepStrictEqual(false, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should.run();
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import { deepStrictEqual, throws } from 'assert';
|
|
||||||
import { should } from 'micro-should';
|
|
||||||
import * as starknet from '../../lib/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();
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import * as microStark from '../../../lib/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();
|
|
||||||
@@ -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
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
import './basic.test.js';
|
|
||||||
import './stark.test.js';
|
|
||||||
import './property.test.js';
|
|
||||||
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { deepStrictEqual, throws } from 'assert';
|
|
||||||
import { should } from 'micro-should';
|
|
||||||
import * as starknet from '../../lib/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();
|
|
||||||
}
|
|
||||||
@@ -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/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();
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"strict": true,
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"target": "es2020",
|
|
||||||
"lib": [
|
|
||||||
"es2020",
|
|
||||||
"dom"
|
|
||||||
],
|
|
||||||
"module": "es6",
|
|
||||||
"moduleResolution": "node16",
|
|
||||||
"outDir": "lib/esm",
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"preserveConstEnums": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src",
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"lib"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"strict": true,
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"target": "es2020",
|
|
||||||
"lib": [
|
|
||||||
"es2020",
|
|
||||||
"dom"
|
|
||||||
],
|
|
||||||
"module": "commonjs",
|
|
||||||
"moduleResolution": "node16",
|
|
||||||
"outDir": "lib",
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"preserveConstEnums": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src",
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"lib"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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
181
package-lock.json
generated
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
{
|
||||||
|
"name": "@noble/curves",
|
||||||
|
"version": "0.8.3",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@noble/curves",
|
||||||
|
"version": "0.8.3",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
188
package.json
188
package.json
@@ -1,19 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "@noble/curves",
|
"name": "@noble/curves",
|
||||||
"version": "0.4.0",
|
"version": "0.9.0",
|
||||||
"description": "Minimal, zero-dependency JS implementation of elliptic curve cryptography",
|
"description": "Audited & minimal JS implementation of elliptic curve cryptography",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"abstract",
|
||||||
"lib",
|
"esm",
|
||||||
"lib/esm"
|
"src",
|
||||||
|
"*.js",
|
||||||
|
"*.js.map",
|
||||||
|
"*.d.ts",
|
||||||
|
"*.d.ts.map"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bench": "node curve-definitions/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",
|
||||||
"lint": "prettier --check 'src/**/*.{js,ts}' 'curve-definitions/src/**/*.{js,ts}'",
|
"build:clean": "rm *.{js,d.ts,d.ts.map,js.map} esm/*.{js,d.ts,d.ts.map,js.map} 2> /dev/null",
|
||||||
"format": "prettier --write 'src/**/*.{js,ts}' 'curve-definitions/src/**/*.{js,ts}'",
|
"lint": "prettier --check 'src/**/*.{js,ts}' 'test/*.js'",
|
||||||
"test": "cd curve-definitions; node test/index.test.js"
|
"format": "prettier --write 'src/**/*.{js,ts}' 'test/*.js'",
|
||||||
|
"test": "node test/index.test.js"
|
||||||
},
|
},
|
||||||
"author": "Paul Miller (https://paulmillr.com)",
|
"author": "Paul Miller (https://paulmillr.com)",
|
||||||
"homepage": "https://paulmillr.com/noble/",
|
"homepage": "https://paulmillr.com/noble/",
|
||||||
@@ -22,66 +27,151 @@
|
|||||||
"url": "https://github.com/paulmillr/noble-curves.git"
|
"url": "https://github.com/paulmillr/noble-curves.git"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.3.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-node-resolve": "13.3.0",
|
"@scure/bip32": "~1.2.0",
|
||||||
"micro-bmark": "0.2.0",
|
"@scure/bip39": "~1.2.0",
|
||||||
"micro-should": "0.2.0",
|
"@types/node": "18.11.18",
|
||||||
"prettier": "2.6.2",
|
"fast-check": "3.0.0",
|
||||||
"rollup": "2.75.5",
|
"micro-bmark": "0.3.1",
|
||||||
"typescript": "4.7.3"
|
"micro-should": "0.4.0",
|
||||||
|
"prettier": "2.8.4",
|
||||||
|
"typescript": "5.0.2"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./edwards": {
|
".": {
|
||||||
"types": "./lib/edwards.d.ts",
|
"types": "./index.d.ts",
|
||||||
"import": "./lib/esm/edwards.js",
|
"import": "./esm/index.js",
|
||||||
"default": "./lib/edwards.js"
|
"default": "./index.js"
|
||||||
},
|
},
|
||||||
"./modular": {
|
"./abstract/edwards": {
|
||||||
"types": "./lib/modular.d.ts",
|
"types": "./abstract/edwards.d.ts",
|
||||||
"import": "./lib/esm/modular.js",
|
"import": "./esm/abstract/edwards.js",
|
||||||
"default": "./lib/modular.js"
|
"default": "./abstract/edwards.js"
|
||||||
},
|
},
|
||||||
"./montgomery": {
|
"./abstract/modular": {
|
||||||
"types": "./lib/montgomery.d.ts",
|
"types": "./abstract/modular.d.ts",
|
||||||
"import": "./lib/esm/montgomery.js",
|
"import": "./esm/abstract/modular.js",
|
||||||
"default": "./lib/montgomery.js"
|
"default": "./abstract/modular.js"
|
||||||
},
|
},
|
||||||
"./weierstrass": {
|
"./abstract/montgomery": {
|
||||||
"types": "./lib/weierstrass.d.ts",
|
"types": "./abstract/montgomery.d.ts",
|
||||||
"import": "./lib/esm/weierstrass.js",
|
"import": "./esm/abstract/montgomery.js",
|
||||||
"default": "./lib/weierstrass.js"
|
"default": "./abstract/montgomery.js"
|
||||||
},
|
},
|
||||||
"./bls": {
|
"./abstract/weierstrass": {
|
||||||
"types": "./lib/bls.d.ts",
|
"types": "./abstract/weierstrass.d.ts",
|
||||||
"import": "./lib/esm/bls.js",
|
"import": "./esm/abstract/weierstrass.js",
|
||||||
"default": "./lib/bls.js"
|
"default": "./abstract/weierstrass.js"
|
||||||
},
|
},
|
||||||
"./hashToCurve": {
|
"./abstract/bls": {
|
||||||
"types": "./lib/hashToCurve.d.ts",
|
"types": "./abstract/bls.d.ts",
|
||||||
"import": "./lib/esm/hashToCurve.js",
|
"import": "./esm/abstract/bls.js",
|
||||||
"default": "./lib/hashToCurve.js"
|
"default": "./abstract/bls.js"
|
||||||
},
|
},
|
||||||
"./group": {
|
"./abstract/hash-to-curve": {
|
||||||
"types": "./lib/group.d.ts",
|
"types": "./abstract/hash-to-curve.d.ts",
|
||||||
"import": "./lib/esm/group.js",
|
"import": "./esm/abstract/hash-to-curve.js",
|
||||||
"default": "./lib/group.js"
|
"default": "./abstract/hash-to-curve.js"
|
||||||
},
|
},
|
||||||
"./utils": {
|
"./abstract/curve": {
|
||||||
"types": "./lib/utils.d.ts",
|
"types": "./abstract/curve.d.ts",
|
||||||
"import": "./lib/esm/utils.js",
|
"import": "./esm/abstract/curve.js",
|
||||||
"default": "./lib/utils.js"
|
"default": "./abstract/curve.js"
|
||||||
|
},
|
||||||
|
"./abstract/utils": {
|
||||||
|
"types": "./abstract/utils.d.ts",
|
||||||
|
"import": "./esm/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": {
|
||||||
|
"types": "./_shortw_utils.d.ts",
|
||||||
|
"import": "./esm/_shortw_utils.js",
|
||||||
|
"default": "./_shortw_utils.js"
|
||||||
|
},
|
||||||
|
"./bls12-381": {
|
||||||
|
"types": "./bls12-381.d.ts",
|
||||||
|
"import": "./esm/bls12-381.js",
|
||||||
|
"default": "./bls12-381.js"
|
||||||
|
},
|
||||||
|
"./bn": {
|
||||||
|
"types": "./bn.d.ts",
|
||||||
|
"import": "./esm/bn.js",
|
||||||
|
"default": "./bn.js"
|
||||||
|
},
|
||||||
|
"./ed25519": {
|
||||||
|
"types": "./ed25519.d.ts",
|
||||||
|
"import": "./esm/ed25519.js",
|
||||||
|
"default": "./ed25519.js"
|
||||||
|
},
|
||||||
|
"./ed448": {
|
||||||
|
"types": "./ed448.d.ts",
|
||||||
|
"import": "./esm/ed448.js",
|
||||||
|
"default": "./ed448.js"
|
||||||
|
},
|
||||||
|
"./index": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"import": "./esm/index.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"./jubjub": {
|
||||||
|
"types": "./jubjub.d.ts",
|
||||||
|
"import": "./esm/jubjub.js",
|
||||||
|
"default": "./jubjub.js"
|
||||||
|
},
|
||||||
|
"./p256": {
|
||||||
|
"types": "./p256.d.ts",
|
||||||
|
"import": "./esm/p256.js",
|
||||||
|
"default": "./p256.js"
|
||||||
|
},
|
||||||
|
"./p384": {
|
||||||
|
"types": "./p384.d.ts",
|
||||||
|
"import": "./esm/p384.js",
|
||||||
|
"default": "./p384.js"
|
||||||
|
},
|
||||||
|
"./p521": {
|
||||||
|
"types": "./p521.d.ts",
|
||||||
|
"import": "./esm/p521.js",
|
||||||
|
"default": "./p521.js"
|
||||||
|
},
|
||||||
|
"./pasta": {
|
||||||
|
"types": "./pasta.d.ts",
|
||||||
|
"import": "./esm/pasta.js",
|
||||||
|
"default": "./pasta.js"
|
||||||
|
},
|
||||||
|
"./secp256k1": {
|
||||||
|
"types": "./secp256k1.d.ts",
|
||||||
|
"import": "./esm/secp256k1.js",
|
||||||
|
"default": "./secp256k1.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"elliptic",
|
"elliptic",
|
||||||
"curve",
|
"curve",
|
||||||
"cryptography",
|
"cryptography",
|
||||||
"hyperelliptic",
|
"weierstrass",
|
||||||
|
"montgomery",
|
||||||
|
"edwards",
|
||||||
"p256",
|
"p256",
|
||||||
"p384",
|
"p384",
|
||||||
"p521",
|
"p521",
|
||||||
"nist",
|
"secp256r1",
|
||||||
|
"secp256k1",
|
||||||
|
"ed25519",
|
||||||
|
"ed448",
|
||||||
|
"bls12-381",
|
||||||
|
"bn254",
|
||||||
|
"pasta",
|
||||||
|
"bls",
|
||||||
|
"noble",
|
||||||
"ecc",
|
"ecc",
|
||||||
"ecdsa",
|
"ecdsa",
|
||||||
"eddsa",
|
"eddsa",
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
import { hmac } from '@noble/hashes/hmac';
|
import { hmac } from '@noble/hashes/hmac';
|
||||||
import { concatBytes, randomBytes } from '@noble/hashes/utils';
|
import { concatBytes, randomBytes } from '@noble/hashes/utils';
|
||||||
import { weierstrass, CurveType } from '@noble/curves/weierstrass';
|
import { weierstrass, CurveType } from './abstract/weierstrass.js';
|
||||||
import { CHash } from '@noble/curves/utils';
|
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,
|
||||||
376
src/abstract/bls.ts
Normal file
376
src/abstract/bls.ts
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
|
/**
|
||||||
|
* BLS (Barreto-Lynn-Scott) family of pairing-friendly curves.
|
||||||
|
* Implements BLS (Boneh-Lynn-Shacham) signatures.
|
||||||
|
* Consists of two curves: G1 and G2:
|
||||||
|
* - G1 is a subgroup of (x, y) E(Fq) over y² = x³ + 4.
|
||||||
|
* - G2 is a subgroup of ((x₁, x₂+i), (y₁, y₂+i)) E(Fq²) over y² = x³ + 4(1 + i) where i is √-1
|
||||||
|
* - Gt, created by bilinear (ate) pairing e(G1, G2), consists of p-th roots of unity in
|
||||||
|
* Fq^k where k is embedding degree. Only degree 12 is currently supported, 24 is not.
|
||||||
|
* Pairing is used to aggregate and verify signatures.
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
import { AffinePoint } from './curve.js';
|
||||||
|
import { IField, hashToPrivateScalar } from './modular.js';
|
||||||
|
import { Hex, PrivKey, CHash, bitLen, bitGet, ensureBytes } from './utils.js';
|
||||||
|
import * as htf from './hash-to-curve.js';
|
||||||
|
import {
|
||||||
|
CurvePointsType,
|
||||||
|
ProjPointType as ProjPointType,
|
||||||
|
CurvePointsRes,
|
||||||
|
weierstrassPoints,
|
||||||
|
} from './weierstrass.js';
|
||||||
|
|
||||||
|
type Fp = bigint; // Can be different field?
|
||||||
|
|
||||||
|
export type SignatureCoder<Fp2> = {
|
||||||
|
decode(hex: Hex): ProjPointType<Fp2>;
|
||||||
|
encode(point: ProjPointType<Fp2>): Uint8Array;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CurveType<Fp, Fp2, Fp6, Fp12> = {
|
||||||
|
r: bigint;
|
||||||
|
G1: Omit<CurvePointsType<Fp>, 'n'> & {
|
||||||
|
mapToCurve: htf.MapToCurve<Fp>;
|
||||||
|
htfDefaults: htf.Opts;
|
||||||
|
};
|
||||||
|
G2: Omit<CurvePointsType<Fp2>, 'n'> & {
|
||||||
|
Signature: SignatureCoder<Fp2>;
|
||||||
|
mapToCurve: htf.MapToCurve<Fp2>;
|
||||||
|
htfDefaults: htf.Opts;
|
||||||
|
};
|
||||||
|
x: bigint;
|
||||||
|
Fp: IField<Fp>;
|
||||||
|
Fr: IField<bigint>;
|
||||||
|
Fp2: IField<Fp2> & {
|
||||||
|
reim: (num: Fp2) => { re: bigint; im: bigint };
|
||||||
|
multiplyByB: (num: Fp2) => Fp2;
|
||||||
|
frobeniusMap(num: Fp2, power: number): Fp2;
|
||||||
|
};
|
||||||
|
Fp6: IField<Fp6>;
|
||||||
|
Fp12: IField<Fp12> & {
|
||||||
|
frobeniusMap(num: Fp12, power: number): Fp12;
|
||||||
|
multiplyBy014(num: Fp12, o0: Fp2, o1: Fp2, o4: Fp2): Fp12;
|
||||||
|
conjugate(num: Fp12): Fp12;
|
||||||
|
finalExponentiate(num: Fp12): Fp12;
|
||||||
|
};
|
||||||
|
htfDefaults: htf.Opts;
|
||||||
|
hash: CHash; // Because we need outputLen for DRBG
|
||||||
|
randomBytes: (bytesLength?: number) => Uint8Array;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CurveFn<Fp, Fp2, Fp6, Fp12> = {
|
||||||
|
CURVE: CurveType<Fp, Fp2, Fp6, Fp12>;
|
||||||
|
Fr: IField<bigint>;
|
||||||
|
Fp: IField<Fp>;
|
||||||
|
Fp2: IField<Fp2>;
|
||||||
|
Fp6: IField<Fp6>;
|
||||||
|
Fp12: IField<Fp12>;
|
||||||
|
G1: CurvePointsRes<Fp> & ReturnType<typeof htf.createHasher<Fp>>;
|
||||||
|
G2: CurvePointsRes<Fp2> & ReturnType<typeof htf.createHasher<Fp2>>;
|
||||||
|
Signature: SignatureCoder<Fp2>;
|
||||||
|
millerLoop: (ell: [Fp2, Fp2, Fp2][], g1: [Fp, Fp]) => Fp12;
|
||||||
|
calcPairingPrecomputes: (p: AffinePoint<Fp2>) => [Fp2, Fp2, Fp2][];
|
||||||
|
pairing: (P: ProjPointType<Fp>, Q: ProjPointType<Fp2>, withFinalExponent?: boolean) => Fp12;
|
||||||
|
getPublicKey: (privateKey: PrivKey) => Uint8Array;
|
||||||
|
sign: {
|
||||||
|
(message: Hex, privateKey: PrivKey): Uint8Array;
|
||||||
|
(message: ProjPointType<Fp2>, privateKey: PrivKey): ProjPointType<Fp2>;
|
||||||
|
};
|
||||||
|
verify: (
|
||||||
|
signature: Hex | ProjPointType<Fp2>,
|
||||||
|
message: Hex | ProjPointType<Fp2>,
|
||||||
|
publicKey: Hex | ProjPointType<Fp>
|
||||||
|
) => boolean;
|
||||||
|
aggregatePublicKeys: {
|
||||||
|
(publicKeys: Hex[]): Uint8Array;
|
||||||
|
(publicKeys: ProjPointType<Fp>[]): ProjPointType<Fp>;
|
||||||
|
};
|
||||||
|
aggregateSignatures: {
|
||||||
|
(signatures: Hex[]): Uint8Array;
|
||||||
|
(signatures: ProjPointType<Fp2>[]): ProjPointType<Fp2>;
|
||||||
|
};
|
||||||
|
verifyBatch: (
|
||||||
|
signature: Hex | ProjPointType<Fp2>,
|
||||||
|
messages: (Hex | ProjPointType<Fp2>)[],
|
||||||
|
publicKeys: (Hex | ProjPointType<Fp>)[]
|
||||||
|
) => boolean;
|
||||||
|
utils: {
|
||||||
|
randomPrivateKey: () => Uint8Array;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function bls<Fp2, Fp6, Fp12>(
|
||||||
|
CURVE: CurveType<Fp, Fp2, Fp6, Fp12>
|
||||||
|
): CurveFn<Fp, Fp2, Fp6, Fp12> {
|
||||||
|
// Fields looks pretty specific for curve, so for now we need to pass them with opts
|
||||||
|
const { Fp, Fr, Fp2, Fp6, Fp12 } = CURVE;
|
||||||
|
const BLS_X_LEN = bitLen(CURVE.x);
|
||||||
|
const groupLen = 32; // TODO: calculate; hardcoded for now
|
||||||
|
|
||||||
|
// Pre-compute coefficients for sparse multiplication
|
||||||
|
// Point addition and point double calculations is reused for coefficients
|
||||||
|
function calcPairingPrecomputes(p: AffinePoint<Fp2>) {
|
||||||
|
const { x, y } = p;
|
||||||
|
// prettier-ignore
|
||||||
|
const Qx = x, Qy = y, Qz = Fp2.ONE;
|
||||||
|
// prettier-ignore
|
||||||
|
let Rx = Qx, Ry = Qy, Rz = Qz;
|
||||||
|
let ell_coeff: [Fp2, Fp2, Fp2][] = [];
|
||||||
|
for (let i = BLS_X_LEN - 2; i >= 0; i--) {
|
||||||
|
// Double
|
||||||
|
let t0 = Fp2.sqr(Ry); // Ry²
|
||||||
|
let t1 = Fp2.sqr(Rz); // Rz²
|
||||||
|
let t2 = Fp2.multiplyByB(Fp2.mul(t1, 3n)); // 3 * T1 * B
|
||||||
|
let t3 = Fp2.mul(t2, 3n); // 3 * T2
|
||||||
|
let t4 = Fp2.sub(Fp2.sub(Fp2.sqr(Fp2.add(Ry, Rz)), t1), t0); // (Ry + Rz)² - T1 - T0
|
||||||
|
ell_coeff.push([
|
||||||
|
Fp2.sub(t2, t0), // T2 - T0
|
||||||
|
Fp2.mul(Fp2.sqr(Rx), 3n), // 3 * Rx²
|
||||||
|
Fp2.neg(t4), // -T4
|
||||||
|
]);
|
||||||
|
Rx = Fp2.div(Fp2.mul(Fp2.mul(Fp2.sub(t0, t3), Rx), Ry), 2n); // ((T0 - T3) * Rx * Ry) / 2
|
||||||
|
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
|
||||||
|
if (bitGet(CURVE.x, i)) {
|
||||||
|
// Addition
|
||||||
|
let t0 = Fp2.sub(Ry, Fp2.mul(Qy, Rz)); // Ry - Qy * Rz
|
||||||
|
let t1 = Fp2.sub(Rx, Fp2.mul(Qx, Rz)); // Rx - Qx * Rz
|
||||||
|
ell_coeff.push([
|
||||||
|
Fp2.sub(Fp2.mul(t0, Qx), Fp2.mul(t1, Qy)), // T0 * Qx - T1 * Qy
|
||||||
|
Fp2.neg(t0), // -T0
|
||||||
|
t1, // T1
|
||||||
|
]);
|
||||||
|
let t2 = Fp2.sqr(t1); // T1²
|
||||||
|
let t3 = Fp2.mul(t2, t1); // T2 * T1
|
||||||
|
let t4 = Fp2.mul(t2, Rx); // T2 * Rx
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ell_coeff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function millerLoop(ell: [Fp2, Fp2, Fp2][], g1: [Fp, Fp]): Fp12 {
|
||||||
|
const { x } = CURVE;
|
||||||
|
const Px = g1[0];
|
||||||
|
const Py = g1[1];
|
||||||
|
let f12 = Fp12.ONE;
|
||||||
|
for (let j = 0, i = BLS_X_LEN - 2; i >= 0; i--, j++) {
|
||||||
|
const E = ell[j];
|
||||||
|
f12 = Fp12.multiplyBy014(f12, E[0], Fp2.mul(E[1], Px), Fp2.mul(E[2], Py));
|
||||||
|
if (bitGet(x, i)) {
|
||||||
|
j += 1;
|
||||||
|
const F = ell[j];
|
||||||
|
f12 = Fp12.multiplyBy014(f12, F[0], Fp2.mul(F[1], Px), Fp2.mul(F[2], Py));
|
||||||
|
}
|
||||||
|
if (i !== 0) f12 = Fp12.sqr(f12);
|
||||||
|
}
|
||||||
|
return Fp12.conjugate(f12);
|
||||||
|
}
|
||||||
|
|
||||||
|
const utils = {
|
||||||
|
randomPrivateKey: (): Uint8Array => {
|
||||||
|
return Fr.toBytes(hashToPrivateScalar(CURVE.randomBytes(groupLen + 8), CURVE.r));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Point on G1 curve: (x, y)
|
||||||
|
const G1_ = weierstrassPoints({ n: Fr.ORDER, ...CURVE.G1 });
|
||||||
|
const G1 = Object.assign(
|
||||||
|
G1_,
|
||||||
|
htf.createHasher(G1_.ProjectivePoint, CURVE.G1.mapToCurve, {
|
||||||
|
...CURVE.htfDefaults,
|
||||||
|
...CURVE.G1.htfDefaults,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sparse multiplication against precomputed coefficients
|
||||||
|
// TODO: replace with weakmap?
|
||||||
|
type withPairingPrecomputes = { _PPRECOMPUTES: [Fp2, Fp2, Fp2][] | undefined };
|
||||||
|
function pairingPrecomputes(point: G2): [Fp2, Fp2, Fp2][] {
|
||||||
|
const p = point as G2 & withPairingPrecomputes;
|
||||||
|
if (p._PPRECOMPUTES) return p._PPRECOMPUTES;
|
||||||
|
p._PPRECOMPUTES = calcPairingPrecomputes(point.toAffine());
|
||||||
|
return p._PPRECOMPUTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: export
|
||||||
|
// function clearPairingPrecomputes(point: G2) {
|
||||||
|
// const p = point as G2 & withPairingPrecomputes;
|
||||||
|
// p._PPRECOMPUTES = undefined;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Point on G2 curve (complex numbers): (x₁, x₂+i), (y₁, y₂+i)
|
||||||
|
const G2_ = weierstrassPoints({ n: Fr.ORDER, ...CURVE.G2 });
|
||||||
|
const G2 = Object.assign(
|
||||||
|
G2_,
|
||||||
|
htf.createHasher(G2_.ProjectivePoint as htf.H2CPointConstructor<Fp2>, CURVE.G2.mapToCurve, {
|
||||||
|
...CURVE.htfDefaults,
|
||||||
|
...CURVE.G2.htfDefaults,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { Signature } = CURVE.G2;
|
||||||
|
|
||||||
|
// Calculates bilinear pairing
|
||||||
|
function pairing(Q: G1, P: G2, withFinalExponent: boolean = true): Fp12 {
|
||||||
|
if (Q.equals(G1.ProjectivePoint.ZERO) || P.equals(G2.ProjectivePoint.ZERO))
|
||||||
|
throw new Error('pairing is not available for ZERO point');
|
||||||
|
Q.assertValidity();
|
||||||
|
P.assertValidity();
|
||||||
|
// Performance: 9ms for millerLoop and ~14ms for exp.
|
||||||
|
const Qa = Q.toAffine();
|
||||||
|
const looped = millerLoop(pairingPrecomputes(P), [Qa.x, Qa.y]);
|
||||||
|
return withFinalExponent ? Fp12.finalExponentiate(looped) : looped;
|
||||||
|
}
|
||||||
|
type G1 = typeof G1.ProjectivePoint.BASE;
|
||||||
|
type G2 = typeof G2.ProjectivePoint.BASE;
|
||||||
|
|
||||||
|
type G1Hex = Hex | G1;
|
||||||
|
type G2Hex = Hex | G2;
|
||||||
|
function normP1(point: G1Hex): G1 {
|
||||||
|
return point instanceof G1.ProjectivePoint ? (point as G1) : G1.ProjectivePoint.fromHex(point);
|
||||||
|
}
|
||||||
|
function normP2(point: G2Hex): G2 {
|
||||||
|
return point instanceof G2.ProjectivePoint ? point : Signature.decode(point);
|
||||||
|
}
|
||||||
|
function normP2Hash(point: G2Hex, htfOpts?: htf.htfBasicOpts): G2 {
|
||||||
|
return point instanceof G2.ProjectivePoint
|
||||||
|
? point
|
||||||
|
: (G2.hashToCurve(ensureBytes('point', point), htfOpts) as G2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiplies generator by private key.
|
||||||
|
// P = pk x G
|
||||||
|
function getPublicKey(privateKey: PrivKey): Uint8Array {
|
||||||
|
return G1.ProjectivePoint.fromPrivateKey(privateKey).toRawBytes(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executes `hashToCurve` on the message and then multiplies the result by private key.
|
||||||
|
// S = pk x H(m)
|
||||||
|
function sign(message: Hex, privateKey: PrivKey, htfOpts?: htf.htfBasicOpts): Uint8Array;
|
||||||
|
function sign(message: G2, privateKey: PrivKey, htfOpts?: htf.htfBasicOpts): G2;
|
||||||
|
function sign(message: G2Hex, privateKey: PrivKey, htfOpts?: htf.htfBasicOpts): Uint8Array | G2 {
|
||||||
|
const msgPoint = normP2Hash(message, htfOpts);
|
||||||
|
msgPoint.assertValidity();
|
||||||
|
const sigPoint = msgPoint.multiply(G1.normPrivateKeyToScalar(privateKey));
|
||||||
|
if (message instanceof G2.ProjectivePoint) return sigPoint;
|
||||||
|
return Signature.encode(sigPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if pairing of public key & hash is equal to pairing of generator & signature.
|
||||||
|
// e(P, H(m)) == e(G, S)
|
||||||
|
function verify(
|
||||||
|
signature: G2Hex,
|
||||||
|
message: G2Hex,
|
||||||
|
publicKey: G1Hex,
|
||||||
|
htfOpts?: htf.htfBasicOpts
|
||||||
|
): boolean {
|
||||||
|
const P = normP1(publicKey);
|
||||||
|
const Hm = normP2Hash(message, htfOpts);
|
||||||
|
const G = G1.ProjectivePoint.BASE;
|
||||||
|
const S = normP2(signature);
|
||||||
|
// Instead of doing 2 exponentiations, we use property of billinear maps
|
||||||
|
// and do one exp after multiplying 2 points.
|
||||||
|
const ePHm = pairing(P.negate(), Hm, false);
|
||||||
|
const eGS = pairing(G, S, false);
|
||||||
|
const exp = Fp12.finalExponentiate(Fp12.mul(eGS, ePHm));
|
||||||
|
return Fp12.eql(exp, Fp12.ONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a bunch of public key points together.
|
||||||
|
// pk1 + pk2 + pk3 = pkA
|
||||||
|
function aggregatePublicKeys(publicKeys: Hex[]): Uint8Array;
|
||||||
|
function aggregatePublicKeys(publicKeys: G1[]): G1;
|
||||||
|
function aggregatePublicKeys(publicKeys: G1Hex[]): Uint8Array | G1 {
|
||||||
|
if (!publicKeys.length) throw new Error('Expected non-empty array');
|
||||||
|
const agg = publicKeys.map(normP1).reduce((sum, p) => sum.add(p), G1.ProjectivePoint.ZERO);
|
||||||
|
const aggAffine = agg; //.toAffine();
|
||||||
|
if (publicKeys[0] instanceof G1.ProjectivePoint) {
|
||||||
|
aggAffine.assertValidity();
|
||||||
|
return aggAffine;
|
||||||
|
}
|
||||||
|
// toRawBytes ensures point validity
|
||||||
|
return aggAffine.toRawBytes(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a bunch of signature points together.
|
||||||
|
function aggregateSignatures(signatures: Hex[]): Uint8Array;
|
||||||
|
function aggregateSignatures(signatures: G2[]): G2;
|
||||||
|
function aggregateSignatures(signatures: G2Hex[]): Uint8Array | G2 {
|
||||||
|
if (!signatures.length) throw new Error('Expected non-empty array');
|
||||||
|
const agg = signatures.map(normP2).reduce((sum, s) => sum.add(s), G2.ProjectivePoint.ZERO);
|
||||||
|
const aggAffine = agg; //.toAffine();
|
||||||
|
if (signatures[0] instanceof G2.ProjectivePoint) {
|
||||||
|
aggAffine.assertValidity();
|
||||||
|
return aggAffine;
|
||||||
|
}
|
||||||
|
return Signature.encode(aggAffine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407
|
||||||
|
// e(G, S) = e(G, SUM(n)(Si)) = MUL(n)(e(G, Si))
|
||||||
|
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 (publicKeys.length !== messages.length)
|
||||||
|
throw new Error('Pubkey count should equal msg count');
|
||||||
|
const sig = normP2(signature);
|
||||||
|
const nMessages = messages.map((i) => normP2Hash(i, htfOpts));
|
||||||
|
const nPublicKeys = publicKeys.map(normP1);
|
||||||
|
try {
|
||||||
|
const paired = [];
|
||||||
|
for (const message of new Set(nMessages)) {
|
||||||
|
const groupPublicKey = nMessages.reduce(
|
||||||
|
(groupPublicKey, subMessage, i) =>
|
||||||
|
subMessage === message ? groupPublicKey.add(nPublicKeys[i]) : groupPublicKey,
|
||||||
|
G1.ProjectivePoint.ZERO
|
||||||
|
);
|
||||||
|
// const msg = message instanceof PointG2 ? message : await PointG2.hashToCurve(message);
|
||||||
|
// Possible to batch pairing for same msg with different groupPublicKey here
|
||||||
|
paired.push(pairing(groupPublicKey, message, false));
|
||||||
|
}
|
||||||
|
paired.push(pairing(G1.ProjectivePoint.BASE.negate(), sig, false));
|
||||||
|
const product = paired.reduce((a, b) => Fp12.mul(a, b), Fp12.ONE);
|
||||||
|
const exp = Fp12.finalExponentiate(product);
|
||||||
|
return Fp12.eql(exp, Fp12.ONE);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
G1.ProjectivePoint.BASE._setWindowSize(4);
|
||||||
|
|
||||||
|
return {
|
||||||
|
CURVE,
|
||||||
|
Fr,
|
||||||
|
Fp,
|
||||||
|
Fp2,
|
||||||
|
Fp6,
|
||||||
|
Fp12,
|
||||||
|
G1,
|
||||||
|
G2,
|
||||||
|
Signature,
|
||||||
|
millerLoop,
|
||||||
|
calcPairingPrecomputes,
|
||||||
|
pairing,
|
||||||
|
getPublicKey,
|
||||||
|
sign,
|
||||||
|
verify,
|
||||||
|
aggregatePublicKeys,
|
||||||
|
aggregateSignatures,
|
||||||
|
verifyBatch,
|
||||||
|
utils,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,22 +1,41 @@
|
|||||||
/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
// Default group related functions
|
// 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,13 +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 n
|
* @param W window size
|
||||||
* @param affinePoint optional 2d point to save cached precompute windows on it.
|
* @param precomputes precomputed tables
|
||||||
|
* @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);
|
||||||
|
|
||||||
@@ -124,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);
|
||||||
|
}
|
||||||
479
src/abstract/edwards.ts
Normal file
479
src/abstract/edwards.ts
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
|
// Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y²
|
||||||
|
import { mod } from './modular.js';
|
||||||
|
import * as ut from './utils.js';
|
||||||
|
import { ensureBytes, FHash, Hex } from './utils.js';
|
||||||
|
import { Group, GroupConstructor, wNAF, BasicCurve, validateBasic, AffinePoint } from './curve.js';
|
||||||
|
|
||||||
|
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
|
||||||
|
const _0n = BigInt(0);
|
||||||
|
const _1n = BigInt(1);
|
||||||
|
const _2n = BigInt(2);
|
||||||
|
const _8n = BigInt(8);
|
||||||
|
|
||||||
|
// Edwards curves must declare params a & d.
|
||||||
|
export type CurveType = BasicCurve<bigint> & {
|
||||||
|
a: bigint; // curve param a
|
||||||
|
d: bigint; // curve param d
|
||||||
|
hash: FHash; // Hashing
|
||||||
|
randomBytes: (bytesLength?: number) => Uint8Array; // CSPRNG
|
||||||
|
adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array; // clears bits to get valid field elemtn
|
||||||
|
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array; // Used for hashing
|
||||||
|
uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint }; // Ratio √(u/v)
|
||||||
|
preHash?: FHash; // RFC 8032 pre-hashing of messages to sign() / verify()
|
||||||
|
mapToCurve?: (scalar: bigint[]) => AffinePoint<bigint>; // for hash-to-curve standard
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateOpts(curve: CurveType) {
|
||||||
|
const opts = validateBasic(curve);
|
||||||
|
ut.validateObject(
|
||||||
|
curve,
|
||||||
|
{
|
||||||
|
hash: 'function',
|
||||||
|
a: 'bigint',
|
||||||
|
d: 'bigint',
|
||||||
|
randomBytes: 'function',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
adjustScalarBytes: 'function',
|
||||||
|
domain: 'function',
|
||||||
|
uvRatio: 'function',
|
||||||
|
mapToCurve: 'function',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Set defaults
|
||||||
|
return Object.freeze({ ...opts } as const);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance of Extended Point with coordinates in X, Y, Z, T
|
||||||
|
export interface ExtPointType extends Group<ExtPointType> {
|
||||||
|
readonly ex: bigint;
|
||||||
|
readonly ey: bigint;
|
||||||
|
readonly ez: bigint;
|
||||||
|
readonly et: bigint;
|
||||||
|
assertValidity(): void;
|
||||||
|
multiply(scalar: bigint): ExtPointType;
|
||||||
|
multiplyUnsafe(scalar: bigint): ExtPointType;
|
||||||
|
isSmallOrder(): boolean;
|
||||||
|
isTorsionFree(): boolean;
|
||||||
|
clearCofactor(): ExtPointType;
|
||||||
|
toAffine(iz?: bigint): AffinePoint<bigint>;
|
||||||
|
}
|
||||||
|
// Static methods of Extended Point with coordinates in X, Y, Z, T
|
||||||
|
export interface ExtPointConstructor extends GroupConstructor<ExtPointType> {
|
||||||
|
new (x: bigint, y: bigint, z: bigint, t: bigint): ExtPointType;
|
||||||
|
fromAffine(p: AffinePoint<bigint>): ExtPointType;
|
||||||
|
fromHex(hex: Hex): ExtPointType;
|
||||||
|
fromPrivateKey(privateKey: Hex): ExtPointType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CurveFn = {
|
||||||
|
CURVE: ReturnType<typeof validateOpts>;
|
||||||
|
getPublicKey: (privateKey: Hex) => Uint8Array;
|
||||||
|
sign: (message: Hex, privateKey: Hex) => Uint8Array;
|
||||||
|
verify: (sig: Hex, message: Hex, publicKey: Hex) => boolean;
|
||||||
|
ExtendedPoint: ExtPointConstructor;
|
||||||
|
utils: {
|
||||||
|
randomPrivateKey: () => Uint8Array;
|
||||||
|
getExtendedPublicKey: (key: Hex) => {
|
||||||
|
head: Uint8Array;
|
||||||
|
prefix: Uint8Array;
|
||||||
|
scalar: bigint;
|
||||||
|
point: ExtPointType;
|
||||||
|
pointBytes: Uint8Array;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// It is not generic twisted curve for now, but ed25519/ed448 generic implementation
|
||||||
|
export function twistedEdwards(curveDef: CurveType): CurveFn {
|
||||||
|
const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>;
|
||||||
|
const { Fp, n: CURVE_ORDER, preHash, hash: cHash, randomBytes, nByteLength, h: cofactor } = CURVE;
|
||||||
|
const MASK = _2n ** BigInt(nByteLength * 8);
|
||||||
|
const modP = Fp.create; // Function overrides
|
||||||
|
|
||||||
|
// sqrt(u/v)
|
||||||
|
const uvRatio =
|
||||||
|
CURVE.uvRatio ||
|
||||||
|
((u: bigint, v: bigint) => {
|
||||||
|
try {
|
||||||
|
return { isValid: true, value: Fp.sqrt(u * Fp.inv(v)) };
|
||||||
|
} catch (e) {
|
||||||
|
return { isValid: false, value: _0n };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const adjustScalarBytes = CURVE.adjustScalarBytes || ((bytes: Uint8Array) => bytes); // NOOP
|
||||||
|
const domain =
|
||||||
|
CURVE.domain ||
|
||||||
|
((data: Uint8Array, ctx: Uint8Array, phflag: boolean) => {
|
||||||
|
if (ctx.length || phflag) throw new Error('Contexts/pre-hash are not supported');
|
||||||
|
return data;
|
||||||
|
}); // 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]
|
||||||
|
const in0MaskRange = (n: bigint) => n === _0n || inRange(n, MASK); // n in [0..MASK-1]
|
||||||
|
function assertInRange(n: bigint, max: bigint) {
|
||||||
|
// n in [1..max-1]
|
||||||
|
if (inRange(n, max)) return n;
|
||||||
|
throw new Error(`Expected valid scalar < ${max}, got ${typeof n} ${n}`);
|
||||||
|
}
|
||||||
|
function assertGE0(n: bigint) {
|
||||||
|
// n in [0..CURVE_ORDER-1]
|
||||||
|
return n === _0n ? n : assertInRange(n, CURVE_ORDER); // GE = prime subgroup, not full group
|
||||||
|
}
|
||||||
|
const pointPrecomputes = new Map<Point, Point[]>();
|
||||||
|
function isPoint(other: unknown) {
|
||||||
|
if (!(other instanceof Point)) throw new Error('ExtendedPoint expected');
|
||||||
|
}
|
||||||
|
// Extended Point works in extended coordinates: (x, y, z, t) ∋ (x=x/z, y=y/z, t=xy).
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
get x(): bigint {
|
||||||
|
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.
|
||||||
|
equals(other: Point): boolean {
|
||||||
|
isPoint(other);
|
||||||
|
const { ex: X1, ey: Y1, ez: Z1 } = this;
|
||||||
|
const { ex: X2, ey: Y2, ez: Z2 } = other;
|
||||||
|
const X1Z2 = modP(X1 * Z2);
|
||||||
|
const X2Z1 = modP(X2 * Z1);
|
||||||
|
const Y1Z2 = modP(Y1 * Z2);
|
||||||
|
const Y2Z1 = modP(Y2 * Z1);
|
||||||
|
return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected is0(): boolean {
|
||||||
|
return this.equals(Point.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#doubling-dbl-2008-hwcd
|
||||||
|
// Cost: 4M + 4S + 1*a + 6add + 1*2.
|
||||||
|
double(): Point {
|
||||||
|
const { a } = CURVE;
|
||||||
|
const { ex: X1, ey: Y1, ez: Z1 } = this;
|
||||||
|
const A = modP(X1 * X1); // A = X12
|
||||||
|
const B = modP(Y1 * Y1); // B = Y12
|
||||||
|
const C = modP(_2n * modP(Z1 * Z1)); // C = 2*Z12
|
||||||
|
const D = modP(a * A); // D = a*A
|
||||||
|
const x1y1 = X1 + Y1;
|
||||||
|
const E = modP(modP(x1y1 * x1y1) - A - B); // E = (X1+Y1)2-A-B
|
||||||
|
const G = D + B; // G = D+B
|
||||||
|
const F = G - C; // F = G-C
|
||||||
|
const H = D - B; // H = D-B
|
||||||
|
const X3 = modP(E * F); // X3 = E*F
|
||||||
|
const Y3 = modP(G * H); // Y3 = G*H
|
||||||
|
const T3 = modP(E * H); // T3 = E*H
|
||||||
|
const Z3 = modP(F * G); // Z3 = F*G
|
||||||
|
return new Point(X3, Y3, Z3, T3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast algo for adding 2 Extended Points.
|
||||||
|
// https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#addition-add-2008-hwcd
|
||||||
|
// Cost: 9M + 1*a + 1*d + 7add.
|
||||||
|
add(other: Point) {
|
||||||
|
isPoint(other);
|
||||||
|
const { a, d } = CURVE;
|
||||||
|
const { ex: X1, ey: Y1, ez: Z1, et: T1 } = this;
|
||||||
|
const { ex: X2, ey: Y2, ez: Z2, et: T2 } = other;
|
||||||
|
// 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
|
||||||
|
// Cost: 8M + 8add + 2*2.
|
||||||
|
// Note: It does not check whether the `other` point is valid.
|
||||||
|
if (a === BigInt(-1)) {
|
||||||
|
const A = modP((Y1 - X1) * (Y2 + X2));
|
||||||
|
const B = modP((Y1 + X1) * (Y2 - X2));
|
||||||
|
const F = modP(B - A);
|
||||||
|
if (F === _0n) return this.double(); // Same point. Tests say it doesn't affect timing
|
||||||
|
const C = modP(Z1 * _2n * T2);
|
||||||
|
const D = modP(T1 * _2n * Z2);
|
||||||
|
const E = D + C;
|
||||||
|
const G = B + A;
|
||||||
|
const H = D - C;
|
||||||
|
const X3 = modP(E * F);
|
||||||
|
const Y3 = modP(G * H);
|
||||||
|
const T3 = modP(E * H);
|
||||||
|
const Z3 = modP(F * G);
|
||||||
|
return new Point(X3, Y3, Z3, T3);
|
||||||
|
}
|
||||||
|
const A = modP(X1 * X2); // A = X1*X2
|
||||||
|
const B = modP(Y1 * Y2); // B = Y1*Y2
|
||||||
|
const C = modP(T1 * d * T2); // C = T1*d*T2
|
||||||
|
const D = modP(Z1 * Z2); // D = Z1*Z2
|
||||||
|
const E = modP((X1 + Y1) * (X2 + Y2) - A - B); // E = (X1+Y1)*(X2+Y2)-A-B
|
||||||
|
const F = D - C; // F = D-C
|
||||||
|
const G = D + C; // G = D+C
|
||||||
|
const H = modP(B - a * A); // H = B-a*A
|
||||||
|
const X3 = modP(E * F); // X3 = E*F
|
||||||
|
const Y3 = modP(G * H); // Y3 = G*H
|
||||||
|
const T3 = modP(E * H); // T3 = E*H
|
||||||
|
const Z3 = modP(F * G); // Z3 = F*G
|
||||||
|
|
||||||
|
return new Point(X3, Y3, Z3, T3);
|
||||||
|
}
|
||||||
|
|
||||||
|
subtract(other: Point): Point {
|
||||||
|
return this.add(other.negate());
|
||||||
|
}
|
||||||
|
|
||||||
|
private wNAF(n: bigint): { p: Point; f: Point } {
|
||||||
|
return wnaf.wNAFCached(this, pointPrecomputes, n, Point.normalizeZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constant-time multiplication.
|
||||||
|
multiply(scalar: bigint): Point {
|
||||||
|
const { p, f } = this.wNAF(assertInRange(scalar, CURVE_ORDER));
|
||||||
|
return Point.normalizeZ([p, f])[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-constant-time multiplication. Uses double-and-add algorithm.
|
||||||
|
// It's faster, but should only be used when you don't care about
|
||||||
|
// an exposed private key e.g. sig verification.
|
||||||
|
multiplyUnsafe(scalar: bigint): Point {
|
||||||
|
let n = assertGE0(scalar);
|
||||||
|
if (n === _0n) return I;
|
||||||
|
if (this.equals(I) || n === _1n) return this;
|
||||||
|
if (this.equals(G)) return this.wNAF(n).p;
|
||||||
|
return wnaf.unsafeLadder(this, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if point is of small order.
|
||||||
|
// If you add something to small order point, you will have "dirty"
|
||||||
|
// point with torsion component.
|
||||||
|
// Multiplies point by cofactor and checks if the result is 0.
|
||||||
|
isSmallOrder(): boolean {
|
||||||
|
return this.multiplyUnsafe(cofactor).is0();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiplies point by curve order and checks if the result is 0.
|
||||||
|
// Returns `false` is the point is dirty.
|
||||||
|
isTorsionFree(): boolean {
|
||||||
|
return wnaf.unsafeLadder(this, CURVE_ORDER).is0();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts Extended point to default (x, y) coordinates.
|
||||||
|
// Can accept precomputed Z^-1 - for example, from invertBatch.
|
||||||
|
toAffine(iz?: bigint): AffinePoint<bigint> {
|
||||||
|
const { ex: x, ey: y, ez: z } = this;
|
||||||
|
const is0 = this.is0();
|
||||||
|
if (iz == null) iz = is0 ? _8n : (Fp.inv(z) as bigint); // 8 was chosen arbitrarily
|
||||||
|
const ax = modP(x * iz);
|
||||||
|
const ay = modP(y * iz);
|
||||||
|
const zz = modP(z * iz);
|
||||||
|
if (is0) return { x: _0n, y: _1n };
|
||||||
|
if (zz !== _1n) throw new Error('invZ was invalid');
|
||||||
|
return { x: ax, y: ay };
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCofactor(): Point {
|
||||||
|
const { h: cofactor } = CURVE;
|
||||||
|
if (cofactor === _1n) return this;
|
||||||
|
return this.multiplyUnsafe(cofactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts hash string or Uint8Array to Point.
|
||||||
|
// Uses algo from RFC8032 5.1.3.
|
||||||
|
static fromHex(hex: Hex, strict = true): Point {
|
||||||
|
const { d, a } = CURVE;
|
||||||
|
const len = Fp.BYTES;
|
||||||
|
hex = ensureBytes('pointHex', hex, len); // copy hex to a new array
|
||||||
|
const normed = hex.slice(); // copy again, we'll manipulate it
|
||||||
|
const lastByte = hex[len - 1]; // select last byte
|
||||||
|
normed[len - 1] = lastByte & ~0x80; // clear last bit
|
||||||
|
const y = ut.bytesToNumberLE(normed);
|
||||||
|
if (y === _0n) {
|
||||||
|
// y=0 is allowed
|
||||||
|
} else {
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 */
|
||||||
|
function getExtendedPublicKey(key: Hex) {
|
||||||
|
const len = nByteLength;
|
||||||
|
key = ensureBytes('private key', key, len);
|
||||||
|
// Hash private key with curve's hash function to produce uniformingly random input
|
||||||
|
// Check byte lengths: ensure(64, h(ensure(32, key)))
|
||||||
|
const hashed = ensureBytes('hashed private key', cHash(key), 2 * len);
|
||||||
|
const head = adjustScalarBytes(hashed.slice(0, len)); // clear first half bits, produce FE
|
||||||
|
const prefix = hashed.slice(len, 2 * len); // second half is called key prefix (5.1.6)
|
||||||
|
const scalar = modN_LE(head); // The actual private scalar
|
||||||
|
const point = G.multiply(scalar); // Point on Edwards curve aka public key
|
||||||
|
const pointBytes = point.toRawBytes(); // Uint8Array representation
|
||||||
|
return { head, prefix, scalar, point, pointBytes };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates EdDSA pub key. RFC8032 5.1.5. Privkey is hashed. Use first half with 3 bits cleared
|
||||||
|
function getPublicKey(privKey: Hex): Uint8Array {
|
||||||
|
return getExtendedPublicKey(privKey).pointBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// int('LE', SHA512(dom2(F, C) || msgs)) mod N
|
||||||
|
function hashDomainToScalar(context: Hex = new Uint8Array(), ...msgs: Uint8Array[]) {
|
||||||
|
const msg = ut.concatBytes(...msgs);
|
||||||
|
return modN_LE(cHash(domain(msg, ensureBytes('context', context), !!preHash)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Signs message with privateKey. RFC8032 5.1.6 */
|
||||||
|
function sign(msg: Hex, privKey: Hex, context?: Hex): Uint8Array {
|
||||||
|
msg = ensureBytes('message', msg);
|
||||||
|
if (preHash) msg = preHash(msg); // for ed25519ph etc.
|
||||||
|
const { prefix, scalar, pointBytes } = getExtendedPublicKey(privKey);
|
||||||
|
const r = hashDomainToScalar(context, prefix, msg); // r = dom2(F, C) || prefix || PH(M)
|
||||||
|
const R = G.multiply(r).toRawBytes(); // R = rG
|
||||||
|
const k = hashDomainToScalar(context, R, pointBytes, msg); // R || A || PH(M)
|
||||||
|
const s = modN(r + k * scalar); // S = (r + k * s) mod L
|
||||||
|
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 {
|
||||||
|
const len = Fp.BYTES; // Verifies EdDSA signature against message and public key. RFC8032 5.1.7.
|
||||||
|
sig = ensureBytes('signature', sig, 2 * len); // An extended group equation is checked.
|
||||||
|
msg = ensureBytes('message', msg); // ZIP215 compliant, which means not fully RFC8032 compliant.
|
||||||
|
if (preHash) msg = preHash(msg); // for ed25519ph, etc
|
||||||
|
const A = Point.fromHex(publicKey, false); // Check for s bounds, hex validity
|
||||||
|
const R = Point.fromHex(sig.slice(0, len), false); // 0 <= R < 2^256: ZIP215 R can be >= P
|
||||||
|
const s = ut.bytesToNumberLE(sig.slice(len, 2 * len)); // 0 <= s < l
|
||||||
|
const SB = G.multiplyUnsafe(s);
|
||||||
|
const k = hashDomainToScalar(context, R.toRawBytes(), A.toRawBytes(), msg);
|
||||||
|
const RkA = R.add(A.multiplyUnsafe(k));
|
||||||
|
// [8][S]B = [8]R + [8][k]A'
|
||||||
|
return RkA.subtract(SB).clearCofactor().equals(Point.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
G._setWindowSize(8); // Enable precomputes. Slows down first publicKey computation by 20ms.
|
||||||
|
|
||||||
|
const utils = {
|
||||||
|
getExtendedPublicKey,
|
||||||
|
// ed25519 private keys are uniform 32b. No need to check for modulo bias, like in secp256k1.
|
||||||
|
randomPrivateKey: (): Uint8Array => randomBytes(Fp.BYTES),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We're doing scalar multiplication (used in getPublicKey etc) with precomputed BASE_POINT
|
||||||
|
* values. This slows down first getPublicKey() by milliseconds (see Speed section),
|
||||||
|
* but allows to speed-up subsequent getPublicKey() calls up to 20x.
|
||||||
|
* @param windowSize 2, 4, 8, 16
|
||||||
|
*/
|
||||||
|
precompute(windowSize = 8, point = Point.BASE): typeof Point.BASE {
|
||||||
|
point._setWindowSize(windowSize);
|
||||||
|
point.multiply(BigInt(3));
|
||||||
|
return point;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
CURVE,
|
||||||
|
getPublicKey,
|
||||||
|
sign,
|
||||||
|
verify,
|
||||||
|
ExtendedPoint: Point,
|
||||||
|
utils,
|
||||||
|
};
|
||||||
|
}
|
||||||
222
src/abstract/hash-to-curve.ts
Normal file
222
src/abstract/hash-to-curve.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
|
import type { Group, GroupConstructor, AffinePoint } from './curve.js';
|
||||||
|
import { mod, IField } from './modular.js';
|
||||||
|
import { bytesToNumberBE, CHash, concatBytes, utf8ToBytes, validateObject } from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* * `DST` is a domain separation tag, defined in section 2.2.5
|
||||||
|
* * `p` characteristic of F, where F is a finite field of characteristic p and order q = p^m
|
||||||
|
* * `m` is extension degree (1 for prime fields)
|
||||||
|
* * `k` is the target security target in bits (e.g. 128), from section 5.1
|
||||||
|
* * `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;
|
||||||
|
m: number;
|
||||||
|
k: number;
|
||||||
|
expand?: 'xmd' | 'xof';
|
||||||
|
hash: CHash;
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateDST(dst: UnicodeOrBytes): Uint8Array {
|
||||||
|
if (dst instanceof Uint8Array) return dst;
|
||||||
|
if (typeof dst === 'string') return utf8ToBytes(dst);
|
||||||
|
throw new Error('DST must be Uint8Array or string');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Octet Stream to Integer. "spec" implementation of os2ip is 2.5x slower vs bytesToNumberBE.
|
||||||
|
const os2ip = bytesToNumberBE;
|
||||||
|
|
||||||
|
// Integer to Octet Stream (numberToBytesBE)
|
||||||
|
function i2osp(value: number, length: number): Uint8Array {
|
||||||
|
if (value < 0 || value >= 1 << (8 * length)) {
|
||||||
|
throw new Error(`bad I2OSP call: value=${value} length=${length}`);
|
||||||
|
}
|
||||||
|
const res = Array.from({ length }).fill(0) as number[];
|
||||||
|
for (let i = length - 1; i >= 0; i--) {
|
||||||
|
res[i] = value & 0xff;
|
||||||
|
value >>>= 8;
|
||||||
|
}
|
||||||
|
return new Uint8Array(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function strxor(a: Uint8Array, b: Uint8Array): Uint8Array {
|
||||||
|
const arr = new Uint8Array(a.length);
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
arr[i] = a[i] ^ b[i];
|
||||||
|
}
|
||||||
|
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
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.4.1
|
||||||
|
export function expand_message_xmd(
|
||||||
|
msg: Uint8Array,
|
||||||
|
DST: Uint8Array,
|
||||||
|
lenInBytes: number,
|
||||||
|
H: CHash
|
||||||
|
): Uint8Array {
|
||||||
|
isBytes(msg);
|
||||||
|
isBytes(DST);
|
||||||
|
isNum(lenInBytes);
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-16#section-5.3.3
|
||||||
|
if (DST.length > 255) DST = H(concatBytes(utf8ToBytes('H2C-OVERSIZE-DST-'), DST));
|
||||||
|
const { outputLen: b_in_bytes, blockLen: r_in_bytes } = H;
|
||||||
|
const ell = Math.ceil(lenInBytes / b_in_bytes);
|
||||||
|
if (ell > 255) throw new Error('Invalid xmd length');
|
||||||
|
const DST_prime = concatBytes(DST, i2osp(DST.length, 1));
|
||||||
|
const Z_pad = i2osp(0, r_in_bytes);
|
||||||
|
const l_i_b_str = i2osp(lenInBytes, 2); // len_in_bytes_str
|
||||||
|
const b = new Array<Uint8Array>(ell);
|
||||||
|
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));
|
||||||
|
for (let i = 1; i <= ell; i++) {
|
||||||
|
const args = [strxor(b_0, b[i - 1]), i2osp(i + 1, 1), DST_prime];
|
||||||
|
b[i] = H(concatBytes(...args));
|
||||||
|
}
|
||||||
|
const pseudo_random_bytes = concatBytes(...b);
|
||||||
|
return pseudo_random_bytes.slice(0, lenInBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expand_message_xof(
|
||||||
|
msg: Uint8Array,
|
||||||
|
DST: Uint8Array,
|
||||||
|
lenInBytes: number,
|
||||||
|
k: number,
|
||||||
|
H: CHash
|
||||||
|
): Uint8Array {
|
||||||
|
isBytes(msg);
|
||||||
|
isBytes(DST);
|
||||||
|
isNum(lenInBytes);
|
||||||
|
// 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));
|
||||||
|
if (DST.length > 255) {
|
||||||
|
const dkLen = Math.ceil((2 * k) / 8);
|
||||||
|
DST = H.create({ dkLen }).update(utf8ToBytes('H2C-OVERSIZE-DST-')).update(DST).digest();
|
||||||
|
}
|
||||||
|
if (lenInBytes > 65535 || DST.length > 255)
|
||||||
|
throw new Error('expand_message_xof: invalid lenInBytes');
|
||||||
|
return (
|
||||||
|
H.create({ dkLen: lenInBytes })
|
||||||
|
.update(msg)
|
||||||
|
.update(i2osp(lenInBytes, 2))
|
||||||
|
// 2. DST_prime = DST || I2OSP(len(DST), 1)
|
||||||
|
.update(DST)
|
||||||
|
.update(i2osp(DST.length, 1))
|
||||||
|
.digest()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hashes arbitrary-length byte strings to a list of one or more elements of a finite field F
|
||||||
|
* 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 count the number of elements of F to output
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
export function hash_to_field(msg: Uint8Array, count: number, options: Opts): bigint[][] {
|
||||||
|
validateObject(options, {
|
||||||
|
DST: 'string',
|
||||||
|
p: 'bigint',
|
||||||
|
m: 'isSafeInteger',
|
||||||
|
k: 'isSafeInteger',
|
||||||
|
hash: 'hash',
|
||||||
|
});
|
||||||
|
const { p, k, m, hash, expand, DST: _DST } = options;
|
||||||
|
isBytes(msg);
|
||||||
|
isNum(count);
|
||||||
|
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);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const e = new Array(m);
|
||||||
|
for (let j = 0; j < m; j++) {
|
||||||
|
const elm_offset = L * (j + i * m);
|
||||||
|
const tv = prb.subarray(elm_offset, elm_offset + L);
|
||||||
|
e[j] = mod(os2ip(tv), p);
|
||||||
|
}
|
||||||
|
u[i] = e;
|
||||||
|
}
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isogenyMap<T, F extends IField<T>>(field: F, map: [T[], T[], T[], T[]]) {
|
||||||
|
// Make same order as in spec
|
||||||
|
const COEFF = map.map((i) => Array.from(i).reverse());
|
||||||
|
return (x: T, y: T) => {
|
||||||
|
const [xNum, xDen, yNum, yDen] = COEFF.map((val) =>
|
||||||
|
val.reduce((acc, i) => field.add(field.mul(acc, x), i))
|
||||||
|
);
|
||||||
|
x = field.div(xNum, xDen); // xNum / xDen
|
||||||
|
y = field.mul(y, field.div(yNum, yDen)); // y * (yNum / yDev)
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
427
src/abstract/modular.ts
Normal file
427
src/abstract/modular.ts
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
|
// Utilities for modular arithmetics and finite fields
|
||||||
|
import {
|
||||||
|
bitMask,
|
||||||
|
numberToBytesBE,
|
||||||
|
numberToBytesLE,
|
||||||
|
bytesToNumberBE,
|
||||||
|
bytesToNumberLE,
|
||||||
|
ensureBytes,
|
||||||
|
validateObject,
|
||||||
|
} from './utils.js';
|
||||||
|
// prettier-ignore
|
||||||
|
const _0n = BigInt(0), _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3);
|
||||||
|
// prettier-ignore
|
||||||
|
const _4n = BigInt(4), _5n = BigInt(5), _8n = BigInt(8);
|
||||||
|
// prettier-ignore
|
||||||
|
const _9n = BigInt(9), _16n = BigInt(16);
|
||||||
|
|
||||||
|
// Calculates a modulo b
|
||||||
|
export function mod(a: bigint, b: bigint): bigint {
|
||||||
|
const result = a % b;
|
||||||
|
return result >= _0n ? result : b + result;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Efficiently exponentiate num to power and do modular division.
|
||||||
|
* Unsafe in some contexts: uses ladder, so can expose bigint bits.
|
||||||
|
* @example
|
||||||
|
* powMod(2n, 6n, 11n) // 64n % 11n == 9n
|
||||||
|
*/
|
||||||
|
// TODO: use field version && remove
|
||||||
|
export function pow(num: bigint, power: bigint, modulo: bigint): bigint {
|
||||||
|
if (modulo <= _0n || power < _0n) throw new Error('Expected power/modulo > 0');
|
||||||
|
if (modulo === _1n) return _0n;
|
||||||
|
let res = _1n;
|
||||||
|
while (power > _0n) {
|
||||||
|
if (power & _1n) res = (res * num) % modulo;
|
||||||
|
num = (num * num) % modulo;
|
||||||
|
power >>= _1n;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does x ^ (2 ^ power) mod p. pow2(30, 4) == 30 ^ (2 ^ 4)
|
||||||
|
export function pow2(x: bigint, power: bigint, modulo: bigint): bigint {
|
||||||
|
let res = x;
|
||||||
|
while (power-- > _0n) {
|
||||||
|
res *= res;
|
||||||
|
res %= modulo;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverses number over modulo
|
||||||
|
export function invert(number: bigint, modulo: bigint): bigint {
|
||||||
|
if (number === _0n || modulo <= _0n) {
|
||||||
|
throw new Error(`invert: expected positive integers, got n=${number} mod=${modulo}`);
|
||||||
|
}
|
||||||
|
// 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 b = modulo;
|
||||||
|
// prettier-ignore
|
||||||
|
let x = _0n, y = _1n, u = _1n, v = _0n;
|
||||||
|
while (a !== _0n) {
|
||||||
|
// JIT applies optimization if those two lines follow each other
|
||||||
|
const q = b / a;
|
||||||
|
const r = b % a;
|
||||||
|
const m = x - u * q;
|
||||||
|
const n = y - v * q;
|
||||||
|
// prettier-ignore
|
||||||
|
b = a, a = r, x = u, y = v, u = m, v = n;
|
||||||
|
}
|
||||||
|
const gcd = b;
|
||||||
|
if (gcd !== _1n) throw new Error('invert: does not exist');
|
||||||
|
return mod(x, modulo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tonelli-Shanks algorithm
|
||||||
|
// Paper 1: https://eprint.iacr.org/2012/685.pdf (page 12)
|
||||||
|
// Paper 2: Square Roots from 1; 24, 51, 10 to Dan Shanks
|
||||||
|
export function tonelliShanks(P: bigint) {
|
||||||
|
// Legendre constant: used to calculate Legendre symbol (a | p),
|
||||||
|
// which denotes the value of a^((p-1)/2) (mod p).
|
||||||
|
// (a | p) ≡ 1 if a is a square (mod p)
|
||||||
|
// (a | p) ≡ -1 if a is not a square (mod p)
|
||||||
|
// (a | p) ≡ 0 if a ≡ 0 (mod p)
|
||||||
|
const legendreC = (P - _1n) / _2n;
|
||||||
|
|
||||||
|
let Q: bigint, S: number, Z: bigint;
|
||||||
|
// Step 1: By factoring out powers of 2 from p - 1,
|
||||||
|
// find q and s such that p - 1 = q*(2^s) with q odd
|
||||||
|
for (Q = P - _1n, S = 0; Q % _2n === _0n; Q /= _2n, S++);
|
||||||
|
|
||||||
|
// Step 2: Select a non-square z such that (z | p) ≡ -1 and set c ≡ zq
|
||||||
|
for (Z = _2n; Z < P && pow(Z, legendreC, P) !== P - _1n; Z++);
|
||||||
|
|
||||||
|
// Fast-path
|
||||||
|
if (S === 1) {
|
||||||
|
const p1div4 = (P + _1n) / _4n;
|
||||||
|
return function tonelliFast<T>(Fp: IField<T>, n: T) {
|
||||||
|
const root = Fp.pow(n, p1div4);
|
||||||
|
if (!Fp.eql(Fp.sqr(root), n)) throw new Error('Cannot find square root');
|
||||||
|
return root;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow-path
|
||||||
|
const Q1div2 = (Q + _1n) / _2n;
|
||||||
|
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
|
||||||
|
if (Fp.pow(n, legendreC) === Fp.neg(Fp.ONE)) throw new Error('Cannot find square root');
|
||||||
|
let r = S;
|
||||||
|
// TODO: will fail at Fp2/etc
|
||||||
|
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 b = Fp.pow(n, Q); // first guess at the fudge factor
|
||||||
|
|
||||||
|
while (!Fp.eql(b, Fp.ONE)) {
|
||||||
|
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
|
||||||
|
let m = 1;
|
||||||
|
for (let t2 = Fp.sqr(b); m < r; m++) {
|
||||||
|
if (Fp.eql(t2, Fp.ONE)) break;
|
||||||
|
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
|
||||||
|
const ge = Fp.pow(g, _1n << BigInt(r - m - 1)); // ge = 2^(r-m-1)
|
||||||
|
g = Fp.sqr(ge); // g = ge * ge
|
||||||
|
x = Fp.mul(x, ge); // x *= ge
|
||||||
|
b = Fp.mul(b, g); // b *= g
|
||||||
|
r = m;
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FpSqrt(P: bigint) {
|
||||||
|
// NOTE: different algorithms can give different roots, it is up to user to decide which one they want.
|
||||||
|
// For example there is FpSqrtOdd/FpSqrtEven to choice root based on oddness (used for hash-to-curve).
|
||||||
|
|
||||||
|
// P ≡ 3 (mod 4)
|
||||||
|
// √n = n^((P+1)/4)
|
||||||
|
if (P % _4n === _3n) {
|
||||||
|
// Not all roots possible!
|
||||||
|
// const ORDER =
|
||||||
|
// 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaabn;
|
||||||
|
// const NUM = 72057594037927816n;
|
||||||
|
const p1div4 = (P + _1n) / _4n;
|
||||||
|
return function sqrt3mod4<T>(Fp: IField<T>, n: T) {
|
||||||
|
const root = Fp.pow(n, p1div4);
|
||||||
|
// Throw if root**2 != n
|
||||||
|
if (!Fp.eql(Fp.sqr(root), n)) throw new Error('Cannot find square root');
|
||||||
|
return root;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atkin algorithm for q ≡ 5 (mod 8), https://eprint.iacr.org/2012/685.pdf (page 10)
|
||||||
|
if (P % _8n === _5n) {
|
||||||
|
const c1 = (P - _5n) / _8n;
|
||||||
|
return function sqrt5mod8<T>(Fp: IField<T>, n: T) {
|
||||||
|
const n2 = Fp.mul(n, _2n);
|
||||||
|
const v = Fp.pow(n2, c1);
|
||||||
|
const nv = Fp.mul(n, v);
|
||||||
|
const i = Fp.mul(Fp.mul(nv, _2n), v);
|
||||||
|
const root = Fp.mul(nv, Fp.sub(i, Fp.ONE));
|
||||||
|
if (!Fp.eql(Fp.sqr(root), n)) throw new Error('Cannot find square root');
|
||||||
|
return root;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// P ≡ 9 (mod 16)
|
||||||
|
if (P % _16n === _9n) {
|
||||||
|
// NOTE: tonelli is too slow for bls-Fp2 calculations even on start
|
||||||
|
// Means we cannot use sqrt for constants at all!
|
||||||
|
//
|
||||||
|
// const c1 = Fp.sqrt(Fp.negate(Fp.ONE)); // 1. c1 = sqrt(-1) in F, i.e., (c1^2) == -1 in F
|
||||||
|
// const c2 = Fp.sqrt(c1); // 2. c2 = sqrt(c1) in F, i.e., (c2^2) == c1 in F
|
||||||
|
// const c3 = Fp.sqrt(Fp.negate(c1)); // 3. c3 = sqrt(-c1) in F, i.e., (c3^2) == -c1 in F
|
||||||
|
// const c4 = (P + _7n) / _16n; // 4. c4 = (q + 7) / 16 # Integer arithmetic
|
||||||
|
// sqrt = (x) => {
|
||||||
|
// let tv1 = Fp.pow(x, c4); // 1. tv1 = x^c4
|
||||||
|
// let tv2 = Fp.mul(c1, tv1); // 2. tv2 = c1 * tv1
|
||||||
|
// const tv3 = Fp.mul(c2, tv1); // 3. tv3 = c2 * tv1
|
||||||
|
// let tv4 = Fp.mul(c3, tv1); // 4. tv4 = c3 * tv1
|
||||||
|
// const e1 = Fp.equals(Fp.square(tv2), x); // 5. e1 = (tv2^2) == x
|
||||||
|
// const e2 = Fp.equals(Fp.square(tv3), x); // 6. e2 = (tv3^2) == x
|
||||||
|
// tv1 = Fp.cmov(tv1, tv2, e1); // 7. tv1 = CMOV(tv1, tv2, e1) # Select tv2 if (tv2^2) == x
|
||||||
|
// tv2 = Fp.cmov(tv4, tv3, e2); // 8. tv2 = CMOV(tv4, tv3, e2) # Select tv3 if (tv3^2) == x
|
||||||
|
// const e3 = Fp.equals(Fp.square(tv2), x); // 9. e3 = (tv2^2) == x
|
||||||
|
// return Fp.cmov(tv1, tv2, e3); // 10. z = CMOV(tv1, tv2, e3) # Select the sqrt from tv1 and tv2
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other cases: Tonelli-Shanks algorithm
|
||||||
|
return tonelliShanks(P);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Little-endian check for first LE bit (last BE bit);
|
||||||
|
export const isNegativeLE = (num: bigint, modulo: bigint) => (mod(num, modulo) & _1n) === _1n;
|
||||||
|
|
||||||
|
// Currently completly inconsistent naming:
|
||||||
|
// - readable: add, mul, sqr, sqrt, inv, div, pow, eq, sub
|
||||||
|
// - 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
|
||||||
|
export interface IField<T> {
|
||||||
|
ORDER: bigint;
|
||||||
|
BYTES: number;
|
||||||
|
BITS: number;
|
||||||
|
MASK: bigint;
|
||||||
|
ZERO: T;
|
||||||
|
ONE: T;
|
||||||
|
// 1-arg
|
||||||
|
create: (num: T) => T;
|
||||||
|
isValid: (num: T) => boolean;
|
||||||
|
is0: (num: T) => boolean;
|
||||||
|
neg(num: T): T;
|
||||||
|
inv(num: T): T;
|
||||||
|
sqrt(num: T): T;
|
||||||
|
sqr(num: T): T;
|
||||||
|
// 2-args
|
||||||
|
eql(lhs: T, rhs: T): boolean;
|
||||||
|
add(lhs: T, rhs: T): T;
|
||||||
|
sub(lhs: T, rhs: T): T;
|
||||||
|
mul(lhs: T, rhs: T | bigint): T;
|
||||||
|
pow(lhs: T, power: bigint): T;
|
||||||
|
div(lhs: T, rhs: T | bigint): T;
|
||||||
|
// N for NonNormalized (for now)
|
||||||
|
addN(lhs: T, rhs: T): T;
|
||||||
|
subN(lhs: T, rhs: T): T;
|
||||||
|
mulN(lhs: T, rhs: T | bigint): T;
|
||||||
|
sqrN(num: T): T;
|
||||||
|
|
||||||
|
// Optional
|
||||||
|
// 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.
|
||||||
|
isOdd?(num: T): boolean; // Odd instead of even since we have it for Fp2
|
||||||
|
// legendre?(num: T): T;
|
||||||
|
pow(lhs: T, power: bigint): T;
|
||||||
|
invertBatch: (lst: T[]) => T[];
|
||||||
|
toBytes(num: T): Uint8Array;
|
||||||
|
fromBytes(bytes: Uint8Array): T;
|
||||||
|
// If c is False, CMOV returns a, otherwise it returns b.
|
||||||
|
cmov(a: T, b: T, c: boolean): T;
|
||||||
|
}
|
||||||
|
// prettier-ignore
|
||||||
|
const FIELD_FIELDS = [
|
||||||
|
'create', 'isValid', 'is0', 'neg', 'inv', 'sqrt', 'sqr',
|
||||||
|
'eql', 'add', 'sub', 'mul', 'pow', 'div',
|
||||||
|
'addN', 'subN', 'mulN', 'sqrN'
|
||||||
|
] as const;
|
||||||
|
export function validateField<T>(field: IField<T>) {
|
||||||
|
const initial = {
|
||||||
|
ORDER: 'bigint',
|
||||||
|
MASK: 'bigint',
|
||||||
|
BYTES: 'isSafeInteger',
|
||||||
|
BITS: 'isSafeInteger',
|
||||||
|
} as Record<string, string>;
|
||||||
|
const opts = FIELD_FIELDS.reduce((map, val: string) => {
|
||||||
|
map[val] = 'function';
|
||||||
|
return map;
|
||||||
|
}, initial);
|
||||||
|
return validateObject(field, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic field functions
|
||||||
|
export function FpPow<T>(f: IField<T>, num: T, power: bigint): T {
|
||||||
|
// Should have same speed as pow for bigints
|
||||||
|
// TODO: benchmark!
|
||||||
|
if (power < _0n) throw new Error('Expected power > 0');
|
||||||
|
if (power === _0n) return f.ONE;
|
||||||
|
if (power === _1n) return num;
|
||||||
|
let p = f.ONE;
|
||||||
|
let d = num;
|
||||||
|
while (power > _0n) {
|
||||||
|
if (power & _1n) p = f.mul(p, d);
|
||||||
|
d = f.sqr(d);
|
||||||
|
power >>= 1n;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
// Walk from first to last, multiply them by each other MOD p
|
||||||
|
const lastMultiplied = nums.reduce((acc, num, i) => {
|
||||||
|
if (f.is0(num)) return acc;
|
||||||
|
tmp[i] = acc;
|
||||||
|
return f.mul(acc, num);
|
||||||
|
}, f.ONE);
|
||||||
|
// Invert last element
|
||||||
|
const inverted = f.inv(lastMultiplied);
|
||||||
|
// Walk from last to first, multiply them by inverted each other MOD p
|
||||||
|
nums.reduceRight((acc, num, i) => {
|
||||||
|
if (f.is0(num)) return acc;
|
||||||
|
tmp[i] = f.mul(acc, tmp[i]);
|
||||||
|
return f.mul(acc, num);
|
||||||
|
}, inverted);
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.inv(rhs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function returns True whenever the value x is a square in the field F.
|
||||||
|
export function FpIsSquare<T>(f: IField<T>) {
|
||||||
|
const legendreConst = (f.ORDER - _1n) / _2n; // Integer arithmetic
|
||||||
|
return (x: T): boolean => {
|
||||||
|
const p = f.pow(x, legendreConst);
|
||||||
|
return f.eql(p, f.ZERO) || f.eql(p, f.ONE);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
bitLen?: number,
|
||||||
|
isLE = false,
|
||||||
|
redef: Partial<IField<bigint>> = {}
|
||||||
|
): Readonly<FpField> {
|
||||||
|
if (ORDER <= _0n) throw new Error(`Expected Fp ORDER > 0, got ${ORDER}`);
|
||||||
|
const { nBitLength: BITS, nByteLength: BYTES } = nLength(ORDER, bitLen);
|
||||||
|
if (BYTES > 2048) throw new Error('Field lengths over 2048 bytes are not supported');
|
||||||
|
const sqrtP = FpSqrt(ORDER);
|
||||||
|
const f: Readonly<FpField> = Object.freeze({
|
||||||
|
ORDER,
|
||||||
|
BITS,
|
||||||
|
BYTES,
|
||||||
|
MASK: bitMask(BITS),
|
||||||
|
ZERO: _0n,
|
||||||
|
ONE: _1n,
|
||||||
|
create: (num) => mod(num, ORDER),
|
||||||
|
isValid: (num) => {
|
||||||
|
if (typeof num !== 'bigint')
|
||||||
|
throw new Error(`Invalid field element: expected bigint, got ${typeof num}`);
|
||||||
|
return _0n <= num && num < ORDER; // 0 is valid element, but it's not invertible
|
||||||
|
},
|
||||||
|
is0: (num) => num === _0n,
|
||||||
|
isOdd: (num) => (num & _1n) === _1n,
|
||||||
|
neg: (num) => mod(-num, ORDER),
|
||||||
|
eql: (lhs, rhs) => lhs === rhs,
|
||||||
|
|
||||||
|
sqr: (num) => mod(num * num, ORDER),
|
||||||
|
add: (lhs, rhs) => mod(lhs + rhs, ORDER),
|
||||||
|
sub: (lhs, rhs) => mod(lhs - rhs, ORDER),
|
||||||
|
mul: (lhs, rhs) => mod(lhs * rhs, ORDER),
|
||||||
|
pow: (num, power) => FpPow(f, num, power),
|
||||||
|
div: (lhs, rhs) => mod(lhs * invert(rhs, ORDER), ORDER),
|
||||||
|
|
||||||
|
// Same as above, but doesn't normalize
|
||||||
|
sqrN: (num) => num * num,
|
||||||
|
addN: (lhs, rhs) => lhs + rhs,
|
||||||
|
subN: (lhs, rhs) => lhs - rhs,
|
||||||
|
mulN: (lhs, rhs) => lhs * rhs,
|
||||||
|
|
||||||
|
inv: (num) => invert(num, ORDER),
|
||||||
|
sqrt: redef.sqrt || ((n) => sqrtP(f, n)),
|
||||||
|
invertBatch: (lst) => FpInvertBatch(f, lst),
|
||||||
|
// TODO: do we really need constant cmov?
|
||||||
|
// We don't have const-time bigints anyway, so probably will be not very useful
|
||||||
|
cmov: (a, b, c) => (c ? b : a),
|
||||||
|
toBytes: (num) => (isLE ? numberToBytesLE(num, BYTES) : numberToBytesBE(num, BYTES)),
|
||||||
|
fromBytes: (bytes) => {
|
||||||
|
if (bytes.length !== BYTES)
|
||||||
|
throw new Error(`Fp.fromBytes: expected ${BYTES}, got ${bytes.length}`);
|
||||||
|
return isLE ? bytesToNumberLE(bytes) : bytesToNumberBE(bytes);
|
||||||
|
},
|
||||||
|
} as FpField);
|
||||||
|
return Object.freeze(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FpSqrtOdd<T>(Fp: IField<T>, elm: T) {
|
||||||
|
if (!Fp.isOdd) throw new Error(`Field doesn't have isOdd`);
|
||||||
|
const root = Fp.sqrt(elm);
|
||||||
|
return Fp.isOdd(root) ? root : Fp.neg(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FpSqrtEven<T>(Fp: IField<T>, elm: T) {
|
||||||
|
if (!Fp.isOdd) throw new Error(`Field doesn't have isOdd`);
|
||||||
|
const root = Fp.sqrt(elm);
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,55 +1,48 @@
|
|||||||
import * as mod from './modular.js';
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
import {
|
import { mod, pow } from './modular.js';
|
||||||
ensureBytes,
|
import { bytesToNumberLE, ensureBytes, numberToBytesLE, validateObject } from './utils.js';
|
||||||
numberToBytesLE,
|
|
||||||
bytesToNumberLE,
|
|
||||||
// nLength,
|
|
||||||
} 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: (u: Hex, scalar: Hex) => Uint8Array;
|
scalarMult: (scalar: Hex, u: Hex) => Uint8Array;
|
||||||
scalarMultBase: (scalar: Hex) => Uint8Array;
|
scalarMultBase: (scalar: 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 (!Number.isSafeInteger(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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,35 +51,15 @@ 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)
|
||||||
x_2 = x_2 XOR dummy
|
x_2 = x_2 XOR dummy
|
||||||
@@ -102,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
|
||||||
@@ -110,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;
|
||||||
@@ -170,23 +148,22 @@ 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));
|
||||||
}
|
}
|
||||||
// Multiply point u by scalar
|
function scalarMult(scalar: Hex, u: Hex): Uint8Array {
|
||||||
function scalarMult(u: Hex, scalar: Hex): Uint8Array {
|
|
||||||
const pointU = decodeUCoordinate(u);
|
const pointU = decodeUCoordinate(u);
|
||||||
const _scalar = decodeScalar(scalar);
|
const _scalar = decodeScalar(scalar);
|
||||||
const pu = montgomeryLadder(pointU, _scalar);
|
const pu = montgomeryLadder(pointU, _scalar);
|
||||||
@@ -195,19 +172,18 @@ 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);
|
||||||
}
|
}
|
||||||
// Multiply base point by scalar
|
// Computes public key from private. By doing scalar multiplication of base point.
|
||||||
|
const GuBytes = encodeUCoordinate(CURVE.Gu);
|
||||||
function scalarMultBase(scalar: Hex): Uint8Array {
|
function scalarMultBase(scalar: Hex): Uint8Array {
|
||||||
return scalarMult(CURVE.Gu, scalar);
|
return scalarMult(scalar, GuBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// NOTE: 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
|
|
||||||
scalarMult,
|
scalarMult,
|
||||||
scalarMultBase,
|
scalarMultBase,
|
||||||
// NOTE: these function work on complimentary montgomery curve
|
getSharedSecret: (privateKey: Hex, publicKey: Hex) => scalarMult(privateKey, publicKey),
|
||||||
// getSharedSecret: (privateKey: Hex, publicKey: Hex) => scalarMult(publicKey, privateKey),
|
|
||||||
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
119
src/abstract/poseidon.ts
Normal 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;
|
||||||
|
}
|
||||||
246
src/abstract/utils.ts
Normal file
246
src/abstract/utils.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
|
const _0n = BigInt(0);
|
||||||
|
const _1n = BigInt(1);
|
||||||
|
const _2n = BigInt(2);
|
||||||
|
const u8a = (a: any): a is Uint8Array => a instanceof Uint8Array;
|
||||||
|
|
||||||
|
// We accept hex strings besides Uint8Array for simplicity
|
||||||
|
export type Hex = Uint8Array | string;
|
||||||
|
// Very few implementations accept numbers, we do it to ease learning curve
|
||||||
|
export type PrivKey = Hex | bigint;
|
||||||
|
export type CHash = {
|
||||||
|
(message: Uint8Array | string): Uint8Array;
|
||||||
|
blockLen: number;
|
||||||
|
outputLen: number;
|
||||||
|
create(opts?: { dkLen?: number }): any; // For shake
|
||||||
|
};
|
||||||
|
export type FHash = (message: Uint8Array | string) => Uint8Array;
|
||||||
|
|
||||||
|
const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0'));
|
||||||
|
export function bytesToHex(bytes: Uint8Array): string {
|
||||||
|
if (!u8a(bytes)) throw new Error('Uint8Array expected');
|
||||||
|
// pre-caching improves the speed 6x
|
||||||
|
let hex = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
hex += hexes[bytes[i]];
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numberToHexUnpadded(num: number | bigint): string {
|
||||||
|
const hex = num.toString(16);
|
||||||
|
return hex.length & 1 ? `0${hex}` : hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hexToNumber(hex: string): bigint {
|
||||||
|
if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex);
|
||||||
|
// Big Endian
|
||||||
|
return BigInt(hex === '' ? '0' : `0x${hex}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caching slows it down 2-3x
|
||||||
|
export function hexToBytes(hex: string): Uint8Array {
|
||||||
|
if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex);
|
||||||
|
if (hex.length % 2) throw new Error('hex string is invalid: unpadded ' + 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Big Endian
|
||||||
|
export function bytesToNumberBE(bytes: Uint8Array): bigint {
|
||||||
|
return hexToNumber(bytesToHex(bytes));
|
||||||
|
}
|
||||||
|
export function bytesToNumberLE(bytes: Uint8Array): bigint {
|
||||||
|
if (!u8a(bytes)) throw new Error('Uint8Array expected');
|
||||||
|
return hexToNumber(bytesToHex(Uint8Array.from(bytes).reverse()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const numberToBytesBE = (n: bigint, len: number) =>
|
||||||
|
hexToBytes(n.toString(16).padStart(len * 2, '0'));
|
||||||
|
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(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
|
||||||
|
// is instance of Uint8Array, and its slice() creates **mutable** copy
|
||||||
|
res = Uint8Array.from(hex);
|
||||||
|
} else {
|
||||||
|
throw new Error(`${title} must be hex string or Uint8Array`);
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
export function concatBytes(...arrs: Uint8Array[]): Uint8Array {
|
||||||
|
const r = new Uint8Array(arrs.reduce((sum, a) => sum + a.length, 0));
|
||||||
|
let pad = 0; // walk through each item, ensure they have proper type
|
||||||
|
arrs.forEach((a) => {
|
||||||
|
if (!u8a(a)) throw new Error('Uint8Array expected');
|
||||||
|
r.set(a, pad);
|
||||||
|
pad += a.length;
|
||||||
|
});
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function equalBytes(b1: Uint8Array, b2: Uint8Array) {
|
||||||
|
// We don't care about timing attacks here
|
||||||
|
if (b1.length !== b2.length) return false;
|
||||||
|
for (let i = 0; i < b1.length; i++) if (b1[i] !== b2[i]) return false;
|
||||||
|
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
|
||||||
|
|
||||||
|
// Amount of bits inside bigint (Same as n.toString(2).length)
|
||||||
|
export function bitLen(n: bigint) {
|
||||||
|
let len;
|
||||||
|
for (len = 0; n > 0n; n >>= _1n, len += 1);
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
// Gets single bit at position. NOTE: first bit position is 0 (same as arrays)
|
||||||
|
// Same as !!+Array.from(n.toString(2)).reverse()[pos]
|
||||||
|
export const bitGet = (n: bigint, pos: number) => (n >> BigInt(pos)) & 1n;
|
||||||
|
// Sets single bit at position
|
||||||
|
export const bitSet = (n: bigint, pos: number, value: boolean) =>
|
||||||
|
n | ((value ? _1n : _0n) << BigInt(pos));
|
||||||
|
// Return mask for N bits (Same as BigInt(`0b${Array(i).fill('1').join('')}`))
|
||||||
|
// Not using ** operator with bigints for old engines.
|
||||||
|
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' });
|
||||||
1187
src/abstract/weierstrass.ts
Normal file
1187
src/abstract/weierstrass.ts
Normal file
File diff suppressed because it is too large
Load Diff
418
src/bls.ts
418
src/bls.ts
@@ -1,418 +0,0 @@
|
|||||||
// Barreto-Lynn-Scott Curves. A family of pairing friendly curves, with embedding degree = 12 or 24
|
|
||||||
// NOTE: only 12 supported for now
|
|
||||||
// Constructed from pair of weierstrass curves, based pairing logic
|
|
||||||
import * as mod from './modular.js';
|
|
||||||
import { ensureBytes, numberToBytesBE, bytesToNumberBE, bitLen, bitGet } from './utils.js';
|
|
||||||
import * as utils from './utils.js';
|
|
||||||
// Types
|
|
||||||
import { hexToBytes, bytesToHex, Hex, PrivKey } from './utils.js';
|
|
||||||
import { htfOpts, stringToBytes, hash_to_field, expand_message_xmd } from './hashToCurve.js';
|
|
||||||
import { CurvePointsType, PointType, CurvePointsRes, weierstrassPoints } from './weierstrass.js';
|
|
||||||
|
|
||||||
type Fp = bigint; // Can be different field?
|
|
||||||
|
|
||||||
export type SignatureCoder<Fp2> = {
|
|
||||||
decode(hex: Hex): PointType<Fp2>;
|
|
||||||
encode(point: PointType<Fp2>): Uint8Array;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CurveType<Fp, Fp2, Fp6, Fp12> = {
|
|
||||||
r: bigint;
|
|
||||||
G1: Omit<CurvePointsType<Fp>, 'n'>;
|
|
||||||
G2: Omit<CurvePointsType<Fp2>, 'n'> & {
|
|
||||||
Signature: SignatureCoder<Fp2>;
|
|
||||||
};
|
|
||||||
x: bigint;
|
|
||||||
Fp: mod.Field<Fp>;
|
|
||||||
Fr: mod.Field<bigint>;
|
|
||||||
Fp2: mod.Field<Fp2> & {
|
|
||||||
reim: (num: Fp2) => { re: bigint; im: bigint };
|
|
||||||
multiplyByB: (num: Fp2) => Fp2;
|
|
||||||
frobeniusMap(num: Fp2, power: number): Fp2;
|
|
||||||
};
|
|
||||||
Fp6: mod.Field<Fp6>;
|
|
||||||
Fp12: mod.Field<Fp12> & {
|
|
||||||
frobeniusMap(num: Fp12, power: number): Fp12;
|
|
||||||
multiplyBy014(num: Fp12, o0: Fp2, o1: Fp2, o4: Fp2): Fp12;
|
|
||||||
conjugate(num: Fp12): Fp12;
|
|
||||||
finalExponentiate(num: Fp12): Fp12;
|
|
||||||
};
|
|
||||||
htfDefaults: htfOpts;
|
|
||||||
hash: utils.CHash; // Because we need outputLen for DRBG
|
|
||||||
randomBytes: (bytesLength?: number) => Uint8Array;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CurveFn<Fp, Fp2, Fp6, Fp12> = {
|
|
||||||
CURVE: CurveType<Fp, Fp2, Fp6, Fp12>;
|
|
||||||
Fr: mod.Field<bigint>;
|
|
||||||
Fp: mod.Field<Fp>;
|
|
||||||
Fp2: mod.Field<Fp2>;
|
|
||||||
Fp6: mod.Field<Fp6>;
|
|
||||||
Fp12: mod.Field<Fp12>;
|
|
||||||
G1: CurvePointsRes<Fp>;
|
|
||||||
G2: CurvePointsRes<Fp2>;
|
|
||||||
Signature: SignatureCoder<Fp2>;
|
|
||||||
millerLoop: (ell: [Fp2, Fp2, Fp2][], g1: [Fp, Fp]) => Fp12;
|
|
||||||
calcPairingPrecomputes: (x: Fp2, y: Fp2) => [Fp2, Fp2, Fp2][];
|
|
||||||
pairing: (P: PointType<Fp>, Q: PointType<Fp2>, withFinalExponent?: boolean) => Fp12;
|
|
||||||
getPublicKey: (privateKey: PrivKey) => Uint8Array;
|
|
||||||
sign: {
|
|
||||||
(message: Hex, privateKey: PrivKey): Uint8Array;
|
|
||||||
(message: PointType<Fp2>, privateKey: PrivKey): PointType<Fp2>;
|
|
||||||
};
|
|
||||||
verify: (
|
|
||||||
signature: Hex | PointType<Fp2>,
|
|
||||||
message: Hex | PointType<Fp2>,
|
|
||||||
publicKey: Hex | PointType<Fp>
|
|
||||||
) => boolean;
|
|
||||||
aggregatePublicKeys: {
|
|
||||||
(publicKeys: Hex[]): Uint8Array;
|
|
||||||
(publicKeys: PointType<Fp>[]): PointType<Fp>;
|
|
||||||
};
|
|
||||||
aggregateSignatures: {
|
|
||||||
(signatures: Hex[]): Uint8Array;
|
|
||||||
(signatures: PointType<Fp2>[]): PointType<Fp2>;
|
|
||||||
};
|
|
||||||
verifyBatch: (
|
|
||||||
signature: Hex | PointType<Fp2>,
|
|
||||||
messages: (Hex | PointType<Fp2>)[],
|
|
||||||
publicKeys: (Hex | PointType<Fp>)[]
|
|
||||||
) => boolean;
|
|
||||||
utils: {
|
|
||||||
bytesToHex: typeof utils.bytesToHex;
|
|
||||||
hexToBytes: typeof utils.hexToBytes;
|
|
||||||
stringToBytes: typeof stringToBytes;
|
|
||||||
hashToField: typeof hash_to_field;
|
|
||||||
expandMessageXMD: typeof expand_message_xmd;
|
|
||||||
mod: typeof mod.mod;
|
|
||||||
getDSTLabel: () => string;
|
|
||||||
setDSTLabel(newLabel: string): void;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function bls<Fp2, Fp6, Fp12>(
|
|
||||||
CURVE: CurveType<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
|
|
||||||
const Fp = CURVE.Fp;
|
|
||||||
const Fr = CURVE.Fr;
|
|
||||||
const Fp2 = CURVE.Fp2;
|
|
||||||
const Fp6 = CURVE.Fp6;
|
|
||||||
const Fp12 = CURVE.Fp12;
|
|
||||||
const BLS_X_LEN = bitLen(CURVE.x);
|
|
||||||
|
|
||||||
// Pre-compute coefficients for sparse multiplication
|
|
||||||
// Point addition and point double calculations is reused for coefficients
|
|
||||||
function calcPairingPrecomputes(x: Fp2, y: Fp2) {
|
|
||||||
// prettier-ignore
|
|
||||||
const Qx = x, Qy = y, Qz = Fp2.ONE;
|
|
||||||
// prettier-ignore
|
|
||||||
let Rx = Qx, Ry = Qy, Rz = Qz;
|
|
||||||
let ell_coeff: [Fp2, Fp2, Fp2][] = [];
|
|
||||||
for (let i = BLS_X_LEN - 2; i >= 0; i--) {
|
|
||||||
// Double
|
|
||||||
let t0 = Fp2.square(Ry); // Ry²
|
|
||||||
let t1 = Fp2.square(Rz); // Rz²
|
|
||||||
let t2 = Fp2.multiplyByB(Fp2.multiply(t1, 3n)); // 3 * T1 * B
|
|
||||||
let t3 = Fp2.multiply(t2, 3n); // 3 * T2
|
|
||||||
let t4 = Fp2.subtract(Fp2.subtract(Fp2.square(Fp2.add(Ry, Rz)), t1), t0); // (Ry + Rz)² - T1 - T0
|
|
||||||
ell_coeff.push([
|
|
||||||
Fp2.subtract(t2, t0), // T2 - T0
|
|
||||||
Fp2.multiply(Fp2.square(Rx), 3n), // 3 * Rx²
|
|
||||||
Fp2.negate(t4), // -T4
|
|
||||||
]);
|
|
||||||
Rx = Fp2.div(Fp2.multiply(Fp2.multiply(Fp2.subtract(t0, t3), Rx), Ry), 2n); // ((T0 - T3) * Rx * Ry) / 2
|
|
||||||
Ry = Fp2.subtract(Fp2.square(Fp2.div(Fp2.add(t0, t3), 2n)), Fp2.multiply(Fp2.square(t2), 3n)); // ((T0 + T3) / 2)² - 3 * T2²
|
|
||||||
Rz = Fp2.multiply(t0, t4); // T0 * T4
|
|
||||||
if (bitGet(CURVE.x, i)) {
|
|
||||||
// Addition
|
|
||||||
let t0 = Fp2.subtract(Ry, Fp2.multiply(Qy, Rz)); // Ry - Qy * Rz
|
|
||||||
let t1 = Fp2.subtract(Rx, Fp2.multiply(Qx, Rz)); // Rx - Qx * Rz
|
|
||||||
ell_coeff.push([
|
|
||||||
Fp2.subtract(Fp2.multiply(t0, Qx), Fp2.multiply(t1, Qy)), // T0 * Qx - T1 * Qy
|
|
||||||
Fp2.negate(t0), // -T0
|
|
||||||
t1, // T1
|
|
||||||
]);
|
|
||||||
let t2 = Fp2.square(t1); // T1²
|
|
||||||
let t3 = Fp2.multiply(t2, t1); // T2 * T1
|
|
||||||
let t4 = Fp2.multiply(t2, Rx); // T2 * Rx
|
|
||||||
let t5 = Fp2.add(Fp2.subtract(t3, Fp2.multiply(t4, 2n)), Fp2.multiply(Fp2.square(t0), Rz)); // T3 - 2 * T4 + T0² * Rz
|
|
||||||
Rx = Fp2.multiply(t1, t5); // T1 * T5
|
|
||||||
Ry = Fp2.subtract(Fp2.multiply(Fp2.subtract(t4, t5), t0), Fp2.multiply(t3, Ry)); // (T4 - T5) * T0 - T3 * Ry
|
|
||||||
Rz = Fp2.multiply(Rz, t3); // Rz * T3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ell_coeff;
|
|
||||||
}
|
|
||||||
|
|
||||||
function millerLoop(ell: [Fp2, Fp2, Fp2][], g1: [Fp, Fp]): Fp12 {
|
|
||||||
const Px = g1[0];
|
|
||||||
const Py = g1[1];
|
|
||||||
let f12 = Fp12.ONE;
|
|
||||||
for (let j = 0, i = BLS_X_LEN - 2; i >= 0; i--, j++) {
|
|
||||||
const E = ell[j];
|
|
||||||
f12 = Fp12.multiplyBy014(f12, E[0], Fp2.multiply(E[1], Px), Fp2.multiply(E[2], Py));
|
|
||||||
if (bitGet(CURVE.x, i)) {
|
|
||||||
j += 1;
|
|
||||||
const F = ell[j];
|
|
||||||
f12 = Fp12.multiplyBy014(f12, F[0], Fp2.multiply(F[1], Px), Fp2.multiply(F[2], Py));
|
|
||||||
}
|
|
||||||
if (i !== 0) f12 = Fp12.square(f12);
|
|
||||||
}
|
|
||||||
return Fp12.conjugate(f12);
|
|
||||||
}
|
|
||||||
|
|
||||||
// bls12-381 is a construction of two curves:
|
|
||||||
// 1. Fp: (x, y)
|
|
||||||
// 2. Fp₂: ((x₁, x₂+i), (y₁, y₂+i)) - (complex numbers)
|
|
||||||
//
|
|
||||||
// Bilinear Pairing (ate pairing) is used to combine both elements into a paired one:
|
|
||||||
// Fp₁₂ = e(Fp, Fp2)
|
|
||||||
// where Fp₁₂ = 12-degree polynomial
|
|
||||||
// Pairing is used to verify signatures.
|
|
||||||
//
|
|
||||||
// We are using Fp for private keys (shorter) and Fp2 for signatures (longer).
|
|
||||||
// Some projects may prefer to swap this relation, it is not supported for now.
|
|
||||||
|
|
||||||
const htfDefaults = { ...CURVE.htfDefaults };
|
|
||||||
|
|
||||||
function isWithinCurveOrder(num: bigint): boolean {
|
|
||||||
return 0 < num && num < CURVE.r;
|
|
||||||
}
|
|
||||||
|
|
||||||
const utils = {
|
|
||||||
hexToBytes: hexToBytes,
|
|
||||||
bytesToHex: bytesToHex,
|
|
||||||
mod: mod.mod,
|
|
||||||
stringToBytes,
|
|
||||||
// TODO: do we need to export it here?
|
|
||||||
hashToField: (msg: Uint8Array, count: number, options: Partial<typeof htfDefaults> = {}) =>
|
|
||||||
hash_to_field(msg, count, { ...CURVE.htfDefaults, ...options }),
|
|
||||||
expandMessageXMD: (msg: Uint8Array, DST: Uint8Array, lenInBytes: number, H = CURVE.hash) =>
|
|
||||||
expand_message_xmd(msg, DST, lenInBytes, H),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can take 40 or more bytes of uniform input e.g. from CSPRNG or KDF
|
|
||||||
* and convert them into private key, with the modulo bias being negligible.
|
|
||||||
* As per FIPS 186 B.1.1.
|
|
||||||
* https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
|
|
||||||
* @param hash hash output from sha512, or a similar function
|
|
||||||
* @returns valid private key
|
|
||||||
*/
|
|
||||||
hashToPrivateKey: (hash: Hex): Uint8Array => {
|
|
||||||
hash = ensureBytes(hash);
|
|
||||||
if (hash.length < 40 || hash.length > 1024)
|
|
||||||
throw new Error('Expected 40-1024 bytes of private key as per FIPS 186');
|
|
||||||
// hashToPrivateScalar(hash, CURVE.r)
|
|
||||||
// NOTE: doesn't add +/-1
|
|
||||||
const num = mod.mod(bytesToNumberBE(hash), CURVE.r);
|
|
||||||
// This should never happen
|
|
||||||
if (num === 0n || num === 1n) throw new Error('Invalid private key');
|
|
||||||
return numberToBytesBE(num, 32);
|
|
||||||
},
|
|
||||||
|
|
||||||
randomBytes: (bytesLength: number = 32): Uint8Array => CURVE.randomBytes(bytesLength),
|
|
||||||
// NIST SP 800-56A rev 3, section 5.6.1.2.2
|
|
||||||
// https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
|
|
||||||
randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(utils.randomBytes(40)),
|
|
||||||
getDSTLabel: () => 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');
|
|
||||||
}
|
|
||||||
htfDefaults.DST = newLabel;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizePrivKey(key: PrivKey): bigint {
|
|
||||||
let int: bigint;
|
|
||||||
if (key instanceof Uint8Array && key.length === 32) int = bytesToNumberBE(key);
|
|
||||||
else if (typeof key === 'string' && key.length === 64) int = BigInt(`0x${key}`);
|
|
||||||
else if (typeof key === 'number' && key > 0 && Number.isSafeInteger(key)) int = BigInt(key);
|
|
||||||
else if (typeof key === 'bigint' && key > 0n) int = key;
|
|
||||||
else throw new TypeError('Expected valid private key');
|
|
||||||
int = mod.mod(int, CURVE.r);
|
|
||||||
if (!isWithinCurveOrder(int)) throw new Error('Private key must be 0 < key < CURVE.r');
|
|
||||||
return int;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Point on G1 curve: (x, y)
|
|
||||||
const G1 = weierstrassPoints({
|
|
||||||
n: Fr.ORDER,
|
|
||||||
...CURVE.G1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sparse multiplication against precomputed coefficients
|
|
||||||
// TODO: replace with weakmap?
|
|
||||||
type withPairingPrecomputes = { _PPRECOMPUTES: [Fp2, Fp2, Fp2][] | undefined };
|
|
||||||
function pairingPrecomputes(point: G2): [Fp2, Fp2, Fp2][] {
|
|
||||||
const p = point as G2 & withPairingPrecomputes;
|
|
||||||
if (p._PPRECOMPUTES) return p._PPRECOMPUTES;
|
|
||||||
p._PPRECOMPUTES = calcPairingPrecomputes(p.x, p.y);
|
|
||||||
return p._PPRECOMPUTES;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearPairingPrecomputes(point: G2) {
|
|
||||||
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)
|
|
||||||
const G2 = weierstrassPoints({
|
|
||||||
n: Fr.ORDER,
|
|
||||||
...CURVE.G2,
|
|
||||||
});
|
|
||||||
const { Signature } = CURVE.G2;
|
|
||||||
|
|
||||||
// Calculates bilinear pairing
|
|
||||||
function pairing(P: G1, Q: G2, withFinalExponent: boolean = true): Fp12 {
|
|
||||||
if (P.equals(G1.Point.ZERO) || Q.equals(G2.Point.ZERO))
|
|
||||||
throw new Error('No pairings at point of Infinity');
|
|
||||||
P.assertValidity();
|
|
||||||
Q.assertValidity();
|
|
||||||
// Performance: 9ms for millerLoop and ~14ms for exp.
|
|
||||||
const looped = millerLoopG1(P, Q);
|
|
||||||
return withFinalExponent ? Fp12.finalExponentiate(looped) : looped;
|
|
||||||
}
|
|
||||||
type G1 = typeof G1.Point.BASE;
|
|
||||||
type G2 = typeof G2.Point.BASE;
|
|
||||||
|
|
||||||
type G1Hex = Hex | G1;
|
|
||||||
type G2Hex = Hex | G2;
|
|
||||||
function normP1(point: G1Hex): G1 {
|
|
||||||
return point instanceof G1.Point ? (point as G1) : G1.Point.fromHex(point);
|
|
||||||
}
|
|
||||||
function normP2(point: G2Hex): G2 {
|
|
||||||
return point instanceof G2.Point ? point : Signature.decode(point);
|
|
||||||
}
|
|
||||||
function normP2Hash(point: G2Hex): G2 {
|
|
||||||
return point instanceof G2.Point ? point : G2.Point.hashToCurve(point);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiplies generator by private key.
|
|
||||||
// P = pk x G
|
|
||||||
function getPublicKey(privateKey: PrivKey): Uint8Array {
|
|
||||||
return G1.Point.fromPrivateKey(privateKey).toRawBytes(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executes `hashToCurve` on the message and then multiplies the result by private key.
|
|
||||||
// S = pk x H(m)
|
|
||||||
function sign(message: Hex, privateKey: PrivKey): Uint8Array;
|
|
||||||
function sign(message: G2, privateKey: PrivKey): G2;
|
|
||||||
function sign(message: G2Hex, privateKey: PrivKey): Uint8Array | G2 {
|
|
||||||
const msgPoint = normP2Hash(message);
|
|
||||||
msgPoint.assertValidity();
|
|
||||||
const sigPoint = msgPoint.multiply(normalizePrivKey(privateKey));
|
|
||||||
if (message instanceof G2.Point) return sigPoint;
|
|
||||||
return Signature.encode(sigPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if pairing of public key & hash is equal to pairing of generator & signature.
|
|
||||||
// e(P, H(m)) == e(G, S)
|
|
||||||
function verify(signature: G2Hex, message: G2Hex, publicKey: G1Hex): boolean {
|
|
||||||
const P = normP1(publicKey);
|
|
||||||
const Hm = normP2Hash(message);
|
|
||||||
const G = G1.Point.BASE;
|
|
||||||
const S = normP2(signature);
|
|
||||||
// Instead of doing 2 exponentiations, we use property of billinear maps
|
|
||||||
// and do one exp after multiplying 2 points.
|
|
||||||
const ePHm = pairing(P.negate(), Hm, false);
|
|
||||||
const eGS = pairing(G, S, false);
|
|
||||||
const exp = Fp12.finalExponentiate(Fp12.multiply(eGS, ePHm));
|
|
||||||
return Fp12.equals(exp, Fp12.ONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds a bunch of public key points together.
|
|
||||||
// pk1 + pk2 + pk3 = pkA
|
|
||||||
function aggregatePublicKeys(publicKeys: Hex[]): Uint8Array;
|
|
||||||
function aggregatePublicKeys(publicKeys: G1[]): G1;
|
|
||||||
function aggregatePublicKeys(publicKeys: G1Hex[]): Uint8Array | G1 {
|
|
||||||
if (!publicKeys.length) throw new Error('Expected non-empty array');
|
|
||||||
const agg = publicKeys
|
|
||||||
.map(normP1)
|
|
||||||
.reduce((sum, p) => sum.add(G1.JacobianPoint.fromAffine(p)), G1.JacobianPoint.ZERO);
|
|
||||||
const aggAffine = agg.toAffine();
|
|
||||||
if (publicKeys[0] instanceof G1.Point) {
|
|
||||||
aggAffine.assertValidity();
|
|
||||||
return aggAffine;
|
|
||||||
}
|
|
||||||
// toRawBytes ensures point validity
|
|
||||||
return aggAffine.toRawBytes(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds a bunch of signature points together.
|
|
||||||
function aggregateSignatures(signatures: Hex[]): Uint8Array;
|
|
||||||
function aggregateSignatures(signatures: G2[]): G2;
|
|
||||||
function aggregateSignatures(signatures: G2Hex[]): Uint8Array | G2 {
|
|
||||||
if (!signatures.length) throw new Error('Expected non-empty array');
|
|
||||||
const agg = signatures
|
|
||||||
.map(normP2)
|
|
||||||
.reduce((sum, s) => sum.add(G2.JacobianPoint.fromAffine(s)), G2.JacobianPoint.ZERO);
|
|
||||||
const aggAffine = agg.toAffine();
|
|
||||||
if (signatures[0] instanceof G2.Point) {
|
|
||||||
aggAffine.assertValidity();
|
|
||||||
return aggAffine;
|
|
||||||
}
|
|
||||||
return Signature.encode(aggAffine);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407
|
|
||||||
// e(G, S) = e(G, SUM(n)(Si)) = MUL(n)(e(G, Si))
|
|
||||||
function verifyBatch(signature: G2Hex, messages: G2Hex[], publicKeys: G1Hex[]): boolean {
|
|
||||||
if (!messages.length) throw new Error('Expected non-empty messages array');
|
|
||||||
if (publicKeys.length !== messages.length)
|
|
||||||
throw new Error('Pubkey count should equal msg count');
|
|
||||||
const sig = normP2(signature);
|
|
||||||
const nMessages = messages.map(normP2Hash);
|
|
||||||
const nPublicKeys = publicKeys.map(normP1);
|
|
||||||
try {
|
|
||||||
const paired = [];
|
|
||||||
for (const message of new Set(nMessages)) {
|
|
||||||
const groupPublicKey = nMessages.reduce(
|
|
||||||
(groupPublicKey, subMessage, i) =>
|
|
||||||
subMessage === message ? groupPublicKey.add(nPublicKeys[i]) : groupPublicKey,
|
|
||||||
G1.Point.ZERO
|
|
||||||
);
|
|
||||||
// const msg = message instanceof PointG2 ? message : await PointG2.hashToCurve(message);
|
|
||||||
// Possible to batch pairing for same msg with different groupPublicKey here
|
|
||||||
paired.push(pairing(groupPublicKey, message, false));
|
|
||||||
}
|
|
||||||
paired.push(pairing(G1.Point.BASE.negate(), sig, false));
|
|
||||||
const product = paired.reduce((a, b) => Fp12.multiply(a, b), Fp12.ONE);
|
|
||||||
const exp = Fp12.finalExponentiate(product);
|
|
||||||
return Fp12.equals(exp, Fp12.ONE);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-compute points. Refer to README.
|
|
||||||
G1.Point.BASE._setWindowSize(4);
|
|
||||||
return {
|
|
||||||
CURVE,
|
|
||||||
Fr,
|
|
||||||
Fp,
|
|
||||||
Fp2,
|
|
||||||
Fp6,
|
|
||||||
Fp12,
|
|
||||||
G1,
|
|
||||||
G2,
|
|
||||||
Signature,
|
|
||||||
millerLoop,
|
|
||||||
calcPairingPrecomputes,
|
|
||||||
pairing,
|
|
||||||
getPublicKey,
|
|
||||||
sign,
|
|
||||||
verify,
|
|
||||||
aggregatePublicKeys,
|
|
||||||
aggregateSignatures,
|
|
||||||
verifyBatch,
|
|
||||||
utils,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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 '@noble/curves/weierstrass';
|
|
||||||
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 '@noble/curves/modular';
|
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 '@noble/curves/modular';
|
|||||||
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),
|
||||||
@@ -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 '@noble/curves/edwards';
|
import { twistedEdwards, ExtPointType } from './abstract/edwards.js';
|
||||||
import { montgomery } from '@noble/curves/montgomery';
|
import { montgomery } from './abstract/montgomery.js';
|
||||||
import { mod, pow2, isNegativeLE, Fp as FpFn } from '@noble/curves/modular';
|
import { mod, pow2, isNegativeLE, Field, FpSqrtEven } from './abstract/modular.js';
|
||||||
import {
|
import {
|
||||||
ensureBytes,
|
|
||||||
equalBytes,
|
equalBytes,
|
||||||
bytesToHex,
|
bytesToHex,
|
||||||
bytesToNumberLE,
|
bytesToNumberLE,
|
||||||
numberToBytesLE,
|
numberToBytesLE,
|
||||||
Hex,
|
Hex,
|
||||||
} from '@noble/curves/utils';
|
ensureBytes,
|
||||||
|
} 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:
|
||||||
@@ -91,6 +93,8 @@ export const ED25519_TORSION_SUBGROUP = [
|
|||||||
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
|
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const Fp = Field(ED25519_P, undefined, true);
|
||||||
|
|
||||||
const ED25519_DEF = {
|
const ED25519_DEF = {
|
||||||
// Param: a
|
// Param: a
|
||||||
a: BigInt(-1),
|
a: BigInt(-1),
|
||||||
@@ -98,7 +102,7 @@ const ED25519_DEF = {
|
|||||||
// Negative number is P - number, and division is invert(number, P)
|
// Negative number is P - number, and division is invert(number, P)
|
||||||
d: BigInt('37095705934669439343138083508754565189542113879843219016388785533085940283555'),
|
d: BigInt('37095705934669439343138083508754565189542113879843219016388785533085940283555'),
|
||||||
// Finite field 𝔽p over which we'll do calculations; 2n ** 255n - 19n
|
// Finite field 𝔽p over which we'll do calculations; 2n ** 255n - 19n
|
||||||
Fp: FpFn(ED25519_P),
|
Fp,
|
||||||
// Subgroup order: how many points ed25519 has
|
// Subgroup order: how many points ed25519 has
|
||||||
// 2n ** 252n + 27742317777372353535851937790883648493n;
|
// 2n ** 252n + 27742317777372353535851937790883648493n;
|
||||||
n: BigInt('7237005577332262213973186563042994240857116359379907606001950938285454250989'),
|
n: BigInt('7237005577332262213973186563042994240857116359379907606001950938285454250989'),
|
||||||
@@ -135,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)
|
||||||
@@ -146,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(
|
||||||
@@ -176,9 +268,33 @@ const invertSqrt = (number: bigint) => uvRatio(_1n, number);
|
|||||||
|
|
||||||
const MAX_255B = BigInt('0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
|
const MAX_255B = BigInt('0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
|
||||||
const bytes255ToNumberLE = (bytes: Uint8Array) =>
|
const bytes255ToNumberLE = (bytes: Uint8Array) =>
|
||||||
ed25519.utils.mod(bytesToNumberLE(bytes) & MAX_255B);
|
ed25519.CURVE.Fp.create(bytesToNumberLE(bytes) & MAX_255B);
|
||||||
|
|
||||||
type ExtendedPoint = ExtendedPointType;
|
type ExtendedPoint = ExtPointType;
|
||||||
|
|
||||||
|
// Computes Elligator map for Ristretto
|
||||||
|
// https://ristretto.group/formulas/elligator.html
|
||||||
|
function calcElligatorRistrettoMap(r0: bigint): ExtendedPoint {
|
||||||
|
const { d } = ed25519.CURVE;
|
||||||
|
const P = ed25519.CURVE.Fp.ORDER;
|
||||||
|
const mod = ed25519.CURVE.Fp.create;
|
||||||
|
const r = mod(SQRT_M1 * r0 * r0); // 1
|
||||||
|
const Ns = mod((r + _1n) * ONE_MINUS_D_SQ); // 2
|
||||||
|
let c = BigInt(-1); // 3
|
||||||
|
const D = mod((c - d * r) * mod(r + d)); // 4
|
||||||
|
let { isValid: Ns_D_is_sq, value: s } = uvRatio(Ns, D); // 5
|
||||||
|
let s_ = mod(s * r0); // 6
|
||||||
|
if (!isNegativeLE(s_, P)) s_ = mod(-s_);
|
||||||
|
if (!Ns_D_is_sq) s = s_; // 7
|
||||||
|
if (!Ns_D_is_sq) c = r; // 8
|
||||||
|
const Nt = mod(c * (r - _1n) * D_MINUS_ONE_SQ - D); // 9
|
||||||
|
const s2 = s * s;
|
||||||
|
const W0 = mod((s + s) * D); // 10
|
||||||
|
const W1 = mod(Nt * SQRT_AD_MINUS_ONE); // 11
|
||||||
|
const W2 = mod(_1n - s2); // 12
|
||||||
|
const W3 = mod(_1n + s2); // 13
|
||||||
|
return new ed25519.ExtendedPoint(mod(W0 * W3), mod(W2 * W1), mod(W1 * W3), mod(W0 * W2));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Each ed25519/ExtendedPoint has 8 different equivalent points. This can be
|
* Each ed25519/ExtendedPoint has 8 different equivalent points. This can be
|
||||||
@@ -195,28 +311,8 @@ export class RistrettoPoint {
|
|||||||
// Always use Ristretto encoding/decoding instead.
|
// Always use Ristretto encoding/decoding instead.
|
||||||
constructor(private readonly ep: ExtendedPoint) {}
|
constructor(private readonly ep: ExtendedPoint) {}
|
||||||
|
|
||||||
// Computes Elligator map for Ristretto
|
static fromAffine(ap: AffinePoint<bigint>) {
|
||||||
// https://ristretto.group/formulas/elligator.html
|
return new RistrettoPoint(ed25519.ExtendedPoint.fromAffine(ap));
|
||||||
private static calcElligatorRistrettoMap(r0: bigint): ExtendedPoint {
|
|
||||||
const { d } = ed25519.CURVE;
|
|
||||||
const P = ed25519.CURVE.Fp.ORDER;
|
|
||||||
const { mod } = ed25519.utils;
|
|
||||||
const r = mod(SQRT_M1 * r0 * r0); // 1
|
|
||||||
const Ns = mod((r + _1n) * ONE_MINUS_D_SQ); // 2
|
|
||||||
let c = BigInt(-1); // 3
|
|
||||||
const D = mod((c - d * r) * mod(r + d)); // 4
|
|
||||||
let { isValid: Ns_D_is_sq, value: s } = uvRatio(Ns, D); // 5
|
|
||||||
let s_ = mod(s * r0); // 6
|
|
||||||
if (!isNegativeLE(s_, P)) s_ = mod(-s_);
|
|
||||||
if (!Ns_D_is_sq) s = s_; // 7
|
|
||||||
if (!Ns_D_is_sq) c = r; // 8
|
|
||||||
const Nt = mod(c * (r - _1n) * D_MINUS_ONE_SQ - D); // 9
|
|
||||||
const s2 = s * s;
|
|
||||||
const W0 = mod((s + s) * D); // 10
|
|
||||||
const W1 = mod(Nt * SQRT_AD_MINUS_ONE); // 11
|
|
||||||
const W2 = mod(_1n - s2); // 12
|
|
||||||
const W3 = mod(_1n + s2); // 13
|
|
||||||
return new ed25519.ExtendedPoint(mod(W0 * W3), mod(W2 * W1), mod(W1 * W3), mod(W0 * W2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -227,11 +323,11 @@ 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 = this.calcElligatorRistrettoMap(r1);
|
const R1 = calcElligatorRistrettoMap(r1);
|
||||||
const r2 = bytes255ToNumberLE(hex.slice(32, 64));
|
const r2 = bytes255ToNumberLE(hex.slice(32, 64));
|
||||||
const R2 = this.calcElligatorRistrettoMap(r2);
|
const R2 = calcElligatorRistrettoMap(r2);
|
||||||
return new RistrettoPoint(R1.add(R2));
|
return new RistrettoPoint(R1.add(R2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,10 +337,10 @@ 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.utils;
|
const mod = ed25519.CURVE.Fp.create;
|
||||||
const emsg = 'RistrettoPoint.fromHex: the hex is not valid encoding of RistrettoPoint';
|
const emsg = 'RistrettoPoint.fromHex: the hex is not valid encoding of RistrettoPoint';
|
||||||
const s = bytes255ToNumberLE(hex);
|
const s = bytes255ToNumberLE(hex);
|
||||||
// 1. Check that s_bytes is the canonical encoding of a field element, or else abort.
|
// 1. Check that s_bytes is the canonical encoding of a field element, or else abort.
|
||||||
@@ -272,9 +368,9 @@ 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.utils;
|
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
|
||||||
const u2 = mod(x * y); // 2
|
const u2 = mod(x * y); // 2
|
||||||
// Square root always exists
|
// Square root always exists
|
||||||
@@ -310,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.utils;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,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;
|
||||||
|
};
|
||||||
242
src/ed448.ts
Normal file
242
src/ed448.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
|
import { shake256 } from '@noble/hashes/sha3';
|
||||||
|
import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/hashes/utils';
|
||||||
|
import { twistedEdwards } from './abstract/edwards.js';
|
||||||
|
import { mod, pow2, Field } from './abstract/modular.js';
|
||||||
|
import { montgomery } from './abstract/montgomery.js';
|
||||||
|
import * as htf from './abstract/hash-to-curve.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edwards448 (not Ed448-Goldilocks) curve with following addons:
|
||||||
|
* * X448 ECDH
|
||||||
|
* Conforms to RFC 8032 https://www.rfc-editor.org/rfc/rfc8032.html#section-5.2
|
||||||
|
*/
|
||||||
|
|
||||||
|
const shake256_114 = wrapConstructor(() => shake256.create({ dkLen: 114 }));
|
||||||
|
const shake256_64 = wrapConstructor(() => shake256.create({ dkLen: 64 }));
|
||||||
|
const ed448P = BigInt(
|
||||||
|
'726838724295606890549323807888004534353641360687318060281490199180612328166730772686396383698676545930088884461843637361053498018365439'
|
||||||
|
);
|
||||||
|
|
||||||
|
// powPminus3div4 calculates z = x^k mod p, where k = (p-3)/4.
|
||||||
|
// Used for efficient square root calculation.
|
||||||
|
// ((P-3)/4).toString(2) would produce bits [223x 1, 0, 222x 1]
|
||||||
|
function ed448_pow_Pminus3div4(x: bigint): bigint {
|
||||||
|
const P = ed448P;
|
||||||
|
// prettier-ignore
|
||||||
|
const _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3), _11n = BigInt(11);
|
||||||
|
// prettier-ignore
|
||||||
|
const _22n = BigInt(22), _44n = BigInt(44), _88n = BigInt(88), _223n = BigInt(223);
|
||||||
|
const b2 = (x * x * x) % P;
|
||||||
|
const b3 = (b2 * b2 * x) % P;
|
||||||
|
const b6 = (pow2(b3, _3n, P) * b3) % P;
|
||||||
|
const b9 = (pow2(b6, _3n, P) * b3) % P;
|
||||||
|
const b11 = (pow2(b9, _2n, P) * b2) % P;
|
||||||
|
const b22 = (pow2(b11, _11n, P) * b11) % P;
|
||||||
|
const b44 = (pow2(b22, _22n, P) * b22) % P;
|
||||||
|
const b88 = (pow2(b44, _44n, P) * b44) % P;
|
||||||
|
const b176 = (pow2(b88, _88n, P) * b88) % P;
|
||||||
|
const b220 = (pow2(b176, _44n, P) * b44) % P;
|
||||||
|
const b222 = (pow2(b220, _2n, P) * b2) % P;
|
||||||
|
const b223 = (pow2(b222, _1n, P) * x) % P;
|
||||||
|
return (pow2(b223, _223n, P) * b222) % P;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustScalarBytes(bytes: Uint8Array): Uint8Array {
|
||||||
|
// Section 5: Likewise, for X448, set the two least significant bits of the first byte to 0, and the most
|
||||||
|
// significant bit of the last byte to 1.
|
||||||
|
bytes[0] &= 252; // 0b11111100
|
||||||
|
// and the most significant bit of the last byte to 1.
|
||||||
|
bytes[55] |= 128; // 0b10000000
|
||||||
|
// NOTE: is is NOOP for 56 bytes scalars (X25519/X448)
|
||||||
|
bytes[56] = 0; // Byte outside of group (456 buts vs 448 bits)
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Fp = Field(ed448P, 456, true);
|
||||||
|
|
||||||
|
const ED448_DEF = {
|
||||||
|
// Param: a
|
||||||
|
a: BigInt(1),
|
||||||
|
// -39081. Negative number is P - number
|
||||||
|
d: BigInt(
|
||||||
|
'726838724295606890549323807888004534353641360687318060281490199180612328166730772686396383698676545930088884461843637361053498018326358'
|
||||||
|
),
|
||||||
|
// Finite field 𝔽p over which we'll do calculations; 2n ** 448n - 2n ** 224n - 1n
|
||||||
|
Fp,
|
||||||
|
// Subgroup order: how many points curve has;
|
||||||
|
// 2n**446n - 13818066809895115352007386748515426880336692474882178609894547503885n
|
||||||
|
n: BigInt(
|
||||||
|
'181709681073901722637330951972001133588410340171829515070372549795146003961539585716195755291692375963310293709091662304773755859649779'
|
||||||
|
),
|
||||||
|
nBitLength: 456,
|
||||||
|
// Cofactor
|
||||||
|
h: BigInt(4),
|
||||||
|
// Base point (x, y) aka generator point
|
||||||
|
Gx: BigInt(
|
||||||
|
'224580040295924300187604334099896036246789641632564134246125461686950415467406032909029192869357953282578032075146446173674602635247710'
|
||||||
|
),
|
||||||
|
Gy: BigInt(
|
||||||
|
'298819210078481492676017930443930673437544040154080242095928241372331506189835876003536878655418784733982303233503462500531545062832660'
|
||||||
|
),
|
||||||
|
// SHAKE256(dom4(phflag,context)||x, 114)
|
||||||
|
hash: shake256_114,
|
||||||
|
randomBytes,
|
||||||
|
adjustScalarBytes,
|
||||||
|
// dom4
|
||||||
|
domain: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => {
|
||||||
|
if (ctx.length > 255) throw new Error(`Context is too big: ${ctx.length}`);
|
||||||
|
return concatBytes(
|
||||||
|
utf8ToBytes('SigEd448'),
|
||||||
|
new Uint8Array([phflag ? 1 : 0, ctx.length]),
|
||||||
|
ctx,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Constant-time ratio of u to v. Allows to combine inversion and square root u/√v.
|
||||||
|
// Uses algo from RFC8032 5.1.3.
|
||||||
|
uvRatio: (u: bigint, v: bigint): { isValid: boolean; value: bigint } => {
|
||||||
|
const P = ed448P;
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8032#section-5.2.3
|
||||||
|
// To compute the square root of (u/v), the first step is to compute the
|
||||||
|
// candidate root x = (u/v)^((p+1)/4). This can be done using the
|
||||||
|
// following trick, to use a single modular powering for both the
|
||||||
|
// inversion of v and the square root:
|
||||||
|
// x = (u/v)^((p+1)/4) = u³v(u⁵v³)^((p-3)/4) (mod p)
|
||||||
|
const u2v = mod(u * u * v, P); // u²v
|
||||||
|
const u3v = mod(u2v * u, P); // u³v
|
||||||
|
const u5v3 = mod(u3v * u2v * v, P); // u⁵v³
|
||||||
|
const root = ed448_pow_Pminus3div4(u5v3);
|
||||||
|
const x = mod(u3v * root, P);
|
||||||
|
// Verify that root is exists
|
||||||
|
const x2 = mod(x * x, P); // x²
|
||||||
|
// If vx² = u, the recovered x-coordinate is x. Otherwise, no
|
||||||
|
// square root exists, and the decoding fails.
|
||||||
|
return { isValid: mod(x2 * v, P) === u, value: x };
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ed448 = twistedEdwards(ED448_DEF);
|
||||||
|
// NOTE: there is no ed448ctx, since ed448 supports ctx by default
|
||||||
|
export const ed448ph = twistedEdwards({ ...ED448_DEF, preHash: shake256_64 });
|
||||||
|
|
||||||
|
export const x448 = montgomery({
|
||||||
|
a: BigInt(156326),
|
||||||
|
montgomeryBits: 448,
|
||||||
|
nByteLength: 57,
|
||||||
|
P: ed448P,
|
||||||
|
Gu: BigInt(5),
|
||||||
|
powPminus2: (x: bigint): bigint => {
|
||||||
|
const P = ed448P;
|
||||||
|
const Pminus3div4 = ed448_pow_Pminus3div4(x);
|
||||||
|
const Pminus3 = pow2(Pminus3div4, BigInt(2), P);
|
||||||
|
return mod(Pminus3 * x, P); // Pminus3 * x = Pminus2
|
||||||
|
},
|
||||||
|
adjustScalarBytes,
|
||||||
|
randomBytes,
|
||||||
|
// The 4-isogeny maps between the Montgomery curve and this Edwards
|
||||||
|
// curve are:
|
||||||
|
// (u, v) = (y^2/x^2, (2 - x^2 - y^2)*y/x^3)
|
||||||
|
// (x, y) = (4*v*(u^2 - 1)/(u^4 - 2*u^2 + 4*v^2 + 1),
|
||||||
|
// -(u^5 - 2*u^3 - 4*u*v^2 + u)/
|
||||||
|
// (u^5 - 2*u^2*v^2 - 2*u^3 - 2*v^2 + u))
|
||||||
|
// xyToU: (p: PointType) => {
|
||||||
|
// const P = ed448P;
|
||||||
|
// const { x, y } = p;
|
||||||
|
// if (x === _0n) throw new Error(`Point with x=0 doesn't have mapping`);
|
||||||
|
// const invX = invert(x * x, P); // x^2
|
||||||
|
// const u = mod(y * y * invX, P); // (y^2/x^2)
|
||||||
|
// 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 };
|
||||||
682
src/edwards.ts
682
src/edwards.ts
@@ -1,682 +0,0 @@
|
|||||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
||||||
// Implementation of Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y²
|
|
||||||
|
|
||||||
// Differences from @noble/ed25519 1.7:
|
|
||||||
// 1. Different field element lengths in ed448:
|
|
||||||
// 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 too (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 {
|
|
||||||
bytesToHex,
|
|
||||||
concatBytes,
|
|
||||||
ensureBytes,
|
|
||||||
numberToBytesLE,
|
|
||||||
bytesToNumberLE,
|
|
||||||
hashToPrivateScalar,
|
|
||||||
BasicCurve,
|
|
||||||
validateOpts as utilOpts,
|
|
||||||
Hex,
|
|
||||||
PrivKey,
|
|
||||||
} from './utils.js'; // TODO: import * as u from './utils.js'?
|
|
||||||
import { Group, GroupConstructor, wNAF } from './group.js';
|
|
||||||
|
|
||||||
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
|
|
||||||
const _0n = BigInt(0);
|
|
||||||
const _1n = BigInt(1);
|
|
||||||
const _2n = BigInt(2);
|
|
||||||
const _8n = BigInt(8);
|
|
||||||
|
|
||||||
export type CHash = {
|
|
||||||
(message: Uint8Array | string): Uint8Array;
|
|
||||||
blockLen: number;
|
|
||||||
outputLen: number;
|
|
||||||
create(): any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CurveType = BasicCurve<bigint> & {
|
|
||||||
// Params: a, d
|
|
||||||
a: bigint;
|
|
||||||
d: bigint;
|
|
||||||
// Hashes
|
|
||||||
hash: CHash; // Because we need outputLen for DRBG
|
|
||||||
randomBytes: (bytesLength?: number) => Uint8Array;
|
|
||||||
adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array;
|
|
||||||
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
|
|
||||||
uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint };
|
|
||||||
preHash?: CHash;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Should be separate from overrides, since overrides can use information about curve (for example nBits)
|
|
||||||
function validateOpts(curve: CurveType) {
|
|
||||||
const opts = utilOpts(curve);
|
|
||||||
if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen))
|
|
||||||
throw new Error('Invalid hash function');
|
|
||||||
for (const i of ['a', 'd'] as const) {
|
|
||||||
if (typeof opts[i] !== 'bigint')
|
|
||||||
throw new Error(`Invalid curve param ${i}=${opts[i]} (${typeof opts[i]})`);
|
|
||||||
}
|
|
||||||
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'] as const) {
|
|
||||||
if (opts[fn] === undefined) continue; // Optional
|
|
||||||
if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
|
||||||
}
|
|
||||||
// Set defaults
|
|
||||||
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
|
|
||||||
export interface ExtendedPointType extends Group<ExtendedPointType> {
|
|
||||||
readonly x: bigint;
|
|
||||||
readonly y: bigint;
|
|
||||||
readonly z: bigint;
|
|
||||||
readonly t: bigint;
|
|
||||||
multiply(scalar: number | bigint, affinePoint?: PointType): ExtendedPointType;
|
|
||||||
multiplyUnsafe(scalar: number | bigint): ExtendedPointType;
|
|
||||||
isSmallOrder(): boolean;
|
|
||||||
isTorsionFree(): boolean;
|
|
||||||
toAffine(invZ?: bigint): PointType;
|
|
||||||
}
|
|
||||||
// Static methods
|
|
||||||
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
|
|
||||||
export interface PointType extends Group<PointType> {
|
|
||||||
readonly x: bigint;
|
|
||||||
readonly y: bigint;
|
|
||||||
_setWindowSize(windowSize: number): void;
|
|
||||||
toRawBytes(isCompressed?: boolean): Uint8Array;
|
|
||||||
toHex(isCompressed?: boolean): string;
|
|
||||||
isTorsionFree(): boolean;
|
|
||||||
}
|
|
||||||
// Static methods
|
|
||||||
export interface PointConstructor extends GroupConstructor<PointType> {
|
|
||||||
new (x: bigint, y: bigint): PointType;
|
|
||||||
fromHex(hex: Hex): PointType;
|
|
||||||
fromPrivateKey(privateKey: PrivKey): PointType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PubKey = Hex | PointType;
|
|
||||||
export type SigType = Hex | SignatureType;
|
|
||||||
|
|
||||||
export type CurveFn = {
|
|
||||||
CURVE: ReturnType<typeof validateOpts>;
|
|
||||||
getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array;
|
|
||||||
sign: (message: Hex, privateKey: Hex) => Uint8Array;
|
|
||||||
verify: (sig: SigType, message: Hex, publicKey: PubKey) => boolean;
|
|
||||||
Point: PointConstructor;
|
|
||||||
ExtendedPoint: ExtendedPointConstructor;
|
|
||||||
Signature: SignatureConstructor;
|
|
||||||
utils: {
|
|
||||||
mod: (a: bigint) => bigint;
|
|
||||||
invert: (number: bigint) => bigint;
|
|
||||||
randomPrivateKey: () => Uint8Array;
|
|
||||||
getExtendedPublicKey: (key: PrivKey) => {
|
|
||||||
head: Uint8Array;
|
|
||||||
prefix: Uint8Array;
|
|
||||||
scalar: bigint;
|
|
||||||
point: PointType;
|
|
||||||
pointBytes: Uint8Array;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// NOTE: it is not generic twisted curve for now, but ed25519/ed448 generic implementation
|
|
||||||
export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|
||||||
const CURVE = validateOpts(curveDef) as ReturnType<typeof validateOpts>;
|
|
||||||
const Fp = CURVE.Fp as mod.Field<bigint>;
|
|
||||||
const CURVE_ORDER = CURVE.n;
|
|
||||||
const fieldLen = Fp.BYTES; // 32 (length of one field element)
|
|
||||||
if (fieldLen > 2048) throw new Error('Field lengths over 2048 are not supported');
|
|
||||||
const groupLen = CURVE.nByteLength;
|
|
||||||
// (2n ** 256n).toString(16);
|
|
||||||
const maxGroupElement = _2n ** BigInt(groupLen * 8); // previous POW_2_256
|
|
||||||
|
|
||||||
// Function overrides
|
|
||||||
const { randomBytes } = CURVE;
|
|
||||||
const modP = Fp.create;
|
|
||||||
|
|
||||||
// sqrt(u/v)
|
|
||||||
function _uvRatio(u: bigint, v: bigint) {
|
|
||||||
try {
|
|
||||||
const value = Fp.sqrt(u * Fp.invert(v));
|
|
||||||
return { isValid: true, value };
|
|
||||||
} catch (e) {
|
|
||||||
return { isValid: false, value: _0n };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const uvRatio = CURVE.uvRatio || _uvRatio;
|
|
||||||
|
|
||||||
const _adjustScalarBytes = (bytes: Uint8Array) => bytes; // NOOP
|
|
||||||
const adjustScalarBytes = CURVE.adjustScalarBytes || _adjustScalarBytes;
|
|
||||||
function _domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) {
|
|
||||||
if (ctx.length || phflag) throw new Error('Contexts/pre-hash are not supported');
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
const domain = CURVE.domain || _domain; // NOOP
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extended Point works in extended coordinates: (x, y, z, t) ∋ (x=x/z, y=y/z, t=xy).
|
|
||||||
* Default Point works in affine coordinates: (x, y)
|
|
||||||
* https://en.wikipedia.org/wiki/Twisted_Edwards_curve#Extended_coordinates
|
|
||||||
*/
|
|
||||||
class ExtendedPoint implements ExtendedPointType {
|
|
||||||
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;
|
|
||||||
return new ExtendedPoint(p.x, p.y, _1n, modP(p.x * p.y));
|
|
||||||
}
|
|
||||||
// Takes a bunch of Jacobian Points but executes only one
|
|
||||||
// invert on all of them. invert is very slow operation,
|
|
||||||
// so this improves performance massively.
|
|
||||||
static toAffineBatch(points: ExtendedPoint[]): Point[] {
|
|
||||||
const toInv = Fp.invertBatch(points.map((p) => p.z));
|
|
||||||
return points.map((p, i) => p.toAffine(toInv[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
static normalizeZ(points: ExtendedPoint[]): ExtendedPoint[] {
|
|
||||||
return this.toAffineBatch(points).map(this.fromAffine);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare one point to another.
|
|
||||||
equals(other: ExtendedPoint): boolean {
|
|
||||||
assertExtPoint(other);
|
|
||||||
const { x: X1, y: Y1, z: Z1 } = this;
|
|
||||||
const { x: X2, y: Y2, z: Z2 } = other;
|
|
||||||
const X1Z2 = modP(X1 * Z2);
|
|
||||||
const X2Z1 = modP(X2 * Z1);
|
|
||||||
const Y1Z2 = modP(Y1 * Z2);
|
|
||||||
const Y2Z1 = modP(Y2 * Z1);
|
|
||||||
return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inverses point to one corresponding to (x, -y) in Affine coordinates.
|
|
||||||
negate(): ExtendedPoint {
|
|
||||||
return new ExtendedPoint(modP(-this.x), this.y, this.z, modP(-this.t));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fast algo for doubling Extended Point.
|
|
||||||
// https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#doubling-dbl-2008-hwcd
|
|
||||||
// Cost: 4M + 4S + 1*a + 6add + 1*2.
|
|
||||||
double(): ExtendedPoint {
|
|
||||||
const { a } = CURVE;
|
|
||||||
const { x: X1, y: Y1, z: Z1 } = this;
|
|
||||||
const A = modP(X1 * X1); // A = X12
|
|
||||||
const B = modP(Y1 * Y1); // B = Y12
|
|
||||||
const C = modP(_2n * modP(Z1 * Z1)); // C = 2*Z12
|
|
||||||
const D = modP(a * A); // D = a*A
|
|
||||||
const x1y1 = X1 + Y1;
|
|
||||||
const E = modP(modP(x1y1 * x1y1) - A - B); // E = (X1+Y1)2-A-B
|
|
||||||
const G = D + B; // G = D+B
|
|
||||||
const F = G - C; // F = G-C
|
|
||||||
const H = D - B; // H = D-B
|
|
||||||
const X3 = modP(E * F); // X3 = E*F
|
|
||||||
const Y3 = modP(G * H); // Y3 = G*H
|
|
||||||
const T3 = modP(E * H); // T3 = E*H
|
|
||||||
const Z3 = modP(F * G); // Z3 = F*G
|
|
||||||
return new ExtendedPoint(X3, Y3, Z3, T3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fast algo for adding 2 Extended Points.
|
|
||||||
// https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#addition-add-2008-hwcd
|
|
||||||
// Cost: 9M + 1*a + 1*d + 7add.
|
|
||||||
add(other: ExtendedPoint) {
|
|
||||||
assertExtPoint(other);
|
|
||||||
const { a, d } = CURVE;
|
|
||||||
const { x: X1, y: Y1, z: Z1, t: T1 } = this;
|
|
||||||
const { x: X2, y: Y2, z: Z2, t: T2 } = other;
|
|
||||||
// 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
|
|
||||||
// Cost: 8M + 8add + 2*2.
|
|
||||||
// Note: It does not check whether the `other` point is valid.
|
|
||||||
if (a === BigInt(-1)) {
|
|
||||||
const A = modP((Y1 - X1) * (Y2 + X2));
|
|
||||||
const B = modP((Y1 + X1) * (Y2 - X2));
|
|
||||||
const F = modP(B - A);
|
|
||||||
if (F === _0n) return this.double(); // Same point.
|
|
||||||
const C = modP(Z1 * _2n * T2);
|
|
||||||
const D = modP(T1 * _2n * Z2);
|
|
||||||
const E = D + C;
|
|
||||||
const G = B + A;
|
|
||||||
const H = D - C;
|
|
||||||
const X3 = modP(E * F);
|
|
||||||
const Y3 = modP(G * H);
|
|
||||||
const T3 = modP(E * H);
|
|
||||||
const Z3 = modP(F * G);
|
|
||||||
return new ExtendedPoint(X3, Y3, Z3, T3);
|
|
||||||
}
|
|
||||||
const A = modP(X1 * X2); // A = X1*X2
|
|
||||||
const B = modP(Y1 * Y2); // B = Y1*Y2
|
|
||||||
const C = modP(T1 * d * T2); // C = T1*d*T2
|
|
||||||
const D = modP(Z1 * Z2); // D = Z1*Z2
|
|
||||||
const E = modP((X1 + Y1) * (X2 + Y2) - A - B); // E = (X1+Y1)*(X2+Y2)-A-B
|
|
||||||
// TODO: do we need to check for same point here? Looks like working without it
|
|
||||||
const F = D - C; // F = D-C
|
|
||||||
const G = D + C; // G = D+C
|
|
||||||
const H = modP(B - a * A); // H = B-a*A
|
|
||||||
const X3 = modP(E * F); // X3 = E*F
|
|
||||||
const Y3 = modP(G * H); // Y3 = G*H
|
|
||||||
const T3 = modP(E * H); // T3 = E*H
|
|
||||||
const Z3 = modP(F * G); // Z3 = F*G
|
|
||||||
|
|
||||||
return new ExtendedPoint(X3, Y3, Z3, T3);
|
|
||||||
}
|
|
||||||
|
|
||||||
subtract(other: ExtendedPoint): ExtendedPoint {
|
|
||||||
return this.add(other.negate());
|
|
||||||
}
|
|
||||||
|
|
||||||
private wNAF(n: bigint, affinePoint?: Point): ExtendedPoint {
|
|
||||||
if (!affinePoint && this.equals(ExtendedPoint.BASE)) affinePoint = Point.BASE;
|
|
||||||
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.
|
|
||||||
// Uses wNAF method. Windowed method may be 10% faster,
|
|
||||||
// but takes 2x longer to generate and consumes 2x memory.
|
|
||||||
multiply(scalar: number | bigint, affinePoint?: Point): ExtendedPoint {
|
|
||||||
return this.wNAF(normalizeScalar(scalar, CURVE_ORDER), affinePoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-constant-time multiplication. Uses double-and-add algorithm.
|
|
||||||
// It's faster, but should only be used when you don't care about
|
|
||||||
// an exposed private key e.g. sig verification.
|
|
||||||
// Allows scalar bigger than curve order, but less than 2^256
|
|
||||||
multiplyUnsafe(scalar: number | bigint): ExtendedPoint {
|
|
||||||
let n = normalizeScalar(scalar, CURVE_ORDER, false);
|
|
||||||
const G = ExtendedPoint.BASE;
|
|
||||||
const P0 = ExtendedPoint.ZERO;
|
|
||||||
if (n === _0n) return P0;
|
|
||||||
if (this.equals(P0) || n === _1n) return this;
|
|
||||||
if (this.equals(G)) return this.wNAF(n);
|
|
||||||
return wnaf.unsafeLadder(this, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiplies point by cofactor and checks if the result is 0.
|
|
||||||
isSmallOrder(): boolean {
|
|
||||||
return this.multiplyUnsafe(CURVE.h).equals(ExtendedPoint.ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiplies point by a very big scalar n and checks if the result is 0.
|
|
||||||
isTorsionFree(): boolean {
|
|
||||||
return this.multiplyUnsafe(CURVE_ORDER).equals(ExtendedPoint.ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts Extended point to default (x, y) coordinates.
|
|
||||||
// Can accept precomputed Z^-1 - for example, from invertBatch.
|
|
||||||
toAffine(invZ?: bigint): Point {
|
|
||||||
const { x, y, z } = this;
|
|
||||||
const is0 = this.equals(ExtendedPoint.ZERO);
|
|
||||||
if (invZ == null) invZ = is0 ? _8n : (Fp.invert(z) as bigint); // 8 was chosen arbitrarily
|
|
||||||
const ax = modP(x * invZ);
|
|
||||||
const ay = modP(y * invZ);
|
|
||||||
const zz = modP(z * invZ);
|
|
||||||
if (is0) return Point.ZERO;
|
|
||||||
if (zz !== _1n) throw new Error('invZ was invalid');
|
|
||||||
return new Point(ax, ay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const wnaf = wNAF(ExtendedPoint, groupLen * 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.
|
|
||||||
// Uses algo from RFC8032 5.1.3.
|
|
||||||
static fromHex(hex: Hex, strict = true) {
|
|
||||||
const { d, a } = CURVE;
|
|
||||||
hex = ensureBytes(hex, fieldLen);
|
|
||||||
// 1. First, interpret the string as an integer in little-endian
|
|
||||||
// representation. Bit 255 of this number is the least significant
|
|
||||||
// bit of the x-coordinate and denote this value x_0. The
|
|
||||||
// y-coordinate is recovered simply by clearing this bit. If the
|
|
||||||
// resulting value is >= p, decoding fails.
|
|
||||||
const normed = hex.slice();
|
|
||||||
const lastByte = hex[fieldLen - 1];
|
|
||||||
normed[fieldLen - 1] = lastByte & ~0x80;
|
|
||||||
const y = bytesToNumberLE(normed);
|
|
||||||
|
|
||||||
if (strict && y >= Fp.ORDER) throw new Error('Expected 0 < hex < P');
|
|
||||||
if (!strict && y >= maxGroupElement) throw new Error('Expected 0 < hex < 2**256');
|
|
||||||
|
|
||||||
// 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 = numberToBytesLE(this.y, fieldLen);
|
|
||||||
bytes[fieldLen - 1] |= this.x & _1n ? 0x80 : 0;
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same as toRawBytes, but returns string.
|
|
||||||
toHex(): string {
|
|
||||||
return bytesToHex(this.toRawBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EDDSA signature.
|
|
||||||
*/
|
|
||||||
class Signature implements SignatureType {
|
|
||||||
constructor(readonly r: Point, readonly s: bigint) {
|
|
||||||
this.assertValidity();
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromHex(hex: Hex) {
|
|
||||||
const bytes = ensureBytes(hex, 2 * fieldLen);
|
|
||||||
const r = Point.fromHex(bytes.slice(0, fieldLen), false);
|
|
||||||
const s = bytesToNumberLE(bytes.slice(fieldLen, 2 * fieldLen));
|
|
||||||
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 concatBytes(this.r.toRawBytes(), numberToBytesLE(this.s, fieldLen));
|
|
||||||
}
|
|
||||||
|
|
||||||
toHex() {
|
|
||||||
return bytesToHex(this.toRawBytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Little-endian SHA512 with modulo n
|
|
||||||
function modlLE(hash: Uint8Array): bigint {
|
|
||||||
return mod.mod(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 (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');
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkPrivateKey(key: PrivKey) {
|
|
||||||
// Normalize bigint / number / string to Uint8Array
|
|
||||||
key =
|
|
||||||
typeof key === 'bigint' || typeof key === 'number'
|
|
||||||
? numberToBytesLE(normalizeScalar(key, maxGroupElement), groupLen)
|
|
||||||
: ensureBytes(key);
|
|
||||||
if (key.length !== groupLen) throw new Error(`Expected ${groupLen} bytes, got ${key.length}`);
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Takes 64 bytes
|
|
||||||
function getKeyFromHash(hashed: Uint8Array) {
|
|
||||||
// First 32 bytes of 64b uniformingly random input are taken,
|
|
||||||
// clears 3 bits of it to produce a random field element.
|
|
||||||
const head = adjustScalarBytes(hashed.slice(0, groupLen));
|
|
||||||
// Second 32 bytes is called key prefix (5.1.6)
|
|
||||||
const prefix = hashed.slice(groupLen, 2 * groupLen);
|
|
||||||
// The actual private scalar
|
|
||||||
const scalar = modlLE(head);
|
|
||||||
// Point on Edwards curve aka public key
|
|
||||||
const point = Point.BASE.multiply(scalar);
|
|
||||||
const pointBytes = point.toRawBytes();
|
|
||||||
return { head, prefix, scalar, point, pointBytes };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convenience method that creates public key and other stuff. RFC8032 5.1.5 */
|
|
||||||
function getExtendedPublicKey(key: PrivKey) {
|
|
||||||
return getKeyFromHash(CURVE.hash(checkPrivateKey(key)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates ed25519 public key. RFC8032 5.1.5
|
|
||||||
* 1. private key is hashed with sha512, then first 32 bytes are taken from the hash
|
|
||||||
* 2. 3 least significant bits of the first byte are cleared
|
|
||||||
*/
|
|
||||||
function getPublicKey(privateKey: PrivKey): Uint8Array {
|
|
||||||
return getExtendedPublicKey(privateKey).pointBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EMPTY = new Uint8Array();
|
|
||||||
function hashDomainToScalar(message: Uint8Array, context: Hex = EMPTY) {
|
|
||||||
context = ensureBytes(context);
|
|
||||||
return modlLE(CURVE.hash(domain(message, context, !!CURVE.preHash)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Signs message with privateKey. RFC8032 5.1.6 */
|
|
||||||
function sign(message: Hex, privateKey: Hex, context?: Hex): Uint8Array {
|
|
||||||
message = ensureBytes(message);
|
|
||||||
if (CURVE.preHash) message = CURVE.preHash(message);
|
|
||||||
const { prefix, scalar, pointBytes } = getExtendedPublicKey(privateKey);
|
|
||||||
const r = hashDomainToScalar(concatBytes(prefix, message), context);
|
|
||||||
const R = Point.BASE.multiply(r); // R = rG
|
|
||||||
const k = hashDomainToScalar(concatBytes(R.toRawBytes(), pointBytes, message), context); // k = hash(R+P+msg)
|
|
||||||
const s = mod.mod(r + k * scalar, CURVE_ORDER); // s = r + kp
|
|
||||||
return new Signature(R, s).toRawBytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies EdDSA signature against message and public key.
|
|
||||||
* An extended group equation is checked.
|
|
||||||
* RFC8032 5.1.7
|
|
||||||
* Compliant with ZIP215:
|
|
||||||
* 0 <= sig.R/publicKey < 2**256 (can be >= curve.P)
|
|
||||||
* 0 <= sig.s < l
|
|
||||||
* Not compliant with RFC8032: it's not possible to comply to both ZIP & RFC at the same time.
|
|
||||||
*/
|
|
||||||
function verify(sig: SigType, message: Hex, publicKey: PubKey, context?: Hex): boolean {
|
|
||||||
message = ensureBytes(message);
|
|
||||||
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(
|
|
||||||
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'
|
|
||||||
return RkA.subtract(SB).multiplyUnsafe(CURVE.h).equals(ExtendedPoint.ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable precomputes. Slows down first publicKey computation by 20ms.
|
|
||||||
Point.BASE._setWindowSize(8);
|
|
||||||
|
|
||||||
const utils = {
|
|
||||||
getExtendedPublicKey,
|
|
||||||
mod: modP,
|
|
||||||
invert: Fp.invert,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not needed for ed25519 private keys. Needed if you use scalars directly (rare).
|
|
||||||
*/
|
|
||||||
hashToPrivateScalar: (hash: Hex): bigint => 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(fieldLen),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We're doing scalar multiplication (used in getPublicKey etc) with precomputed BASE_POINT
|
|
||||||
* values. This slows down first getPublicKey() by milliseconds (see Speed section),
|
|
||||||
* but allows to speed-up subsequent getPublicKey() calls up to 20x.
|
|
||||||
* @param windowSize 2, 4, 8, 16
|
|
||||||
*/
|
|
||||||
precompute(windowSize = 8, point = Point.BASE): Point {
|
|
||||||
const cached = point.equals(Point.BASE) ? point : new Point(point.x, point.y);
|
|
||||||
cached._setWindowSize(windowSize);
|
|
||||||
cached.multiply(_2n);
|
|
||||||
return cached;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
CURVE,
|
|
||||||
getPublicKey,
|
|
||||||
sign,
|
|
||||||
verify,
|
|
||||||
ExtendedPoint,
|
|
||||||
Point,
|
|
||||||
Signature,
|
|
||||||
utils,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import { CHash, concatBytes } from './utils.js';
|
|
||||||
import * as mod from './modular.js';
|
|
||||||
|
|
||||||
export type htfOpts = {
|
|
||||||
// DST: a domain separation tag
|
|
||||||
// defined in section 2.2.5
|
|
||||||
DST: string;
|
|
||||||
// p: the characteristic of F
|
|
||||||
// where F is a finite field of characteristic p and order q = p^m
|
|
||||||
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;
|
|
||||||
// k: the target security level for the suite in bits
|
|
||||||
// defined in section 5.1
|
|
||||||
k: number;
|
|
||||||
// option to use a message that has already been processed by
|
|
||||||
// expand_message_xmd
|
|
||||||
expand: boolean;
|
|
||||||
// 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
|
|
||||||
hash: CHash;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function validateHTFOpts(opts: htfOpts) {
|
|
||||||
if (typeof opts.DST !== 'string') throw new Error('Invalid htf/DST');
|
|
||||||
if (typeof opts.p !== 'bigint') throw new Error('Invalid htf/p');
|
|
||||||
if (typeof opts.m !== 'number') throw new Error('Invalid htf/m');
|
|
||||||
if (typeof opts.k !== 'number') throw new Error('Invalid htf/k');
|
|
||||||
if (typeof opts.expand !== 'boolean') 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
|
|
||||||
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)
|
|
||||||
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 {
|
|
||||||
if (value < 0 || value >= 1 << (8 * length)) {
|
|
||||||
throw new Error(`bad I2OSP call: value=${value} length=${length}`);
|
|
||||||
}
|
|
||||||
const res = Array.from({ length }).fill(0) as number[];
|
|
||||||
for (let i = length - 1; i >= 0; i--) {
|
|
||||||
res[i] = value & 0xff;
|
|
||||||
value >>>= 8;
|
|
||||||
}
|
|
||||||
return new Uint8Array(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
function strxor(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
||||||
const arr = new Uint8Array(a.length);
|
|
||||||
for (let i = 0; i < a.length; i++) {
|
|
||||||
arr[i] = a[i] ^ b[i];
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
export function expand_message_xmd(
|
|
||||||
msg: Uint8Array,
|
|
||||||
DST: Uint8Array,
|
|
||||||
lenInBytes: number,
|
|
||||||
H: CHash
|
|
||||||
): Uint8Array {
|
|
||||||
// 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));
|
|
||||||
const b_in_bytes = H.outputLen;
|
|
||||||
const r_in_bytes = b_in_bytes * 2;
|
|
||||||
const ell = Math.ceil(lenInBytes / b_in_bytes);
|
|
||||||
if (ell > 255) throw new Error('Invalid xmd length');
|
|
||||||
const DST_prime = concatBytes(DST, i2osp(DST.length, 1));
|
|
||||||
const Z_pad = i2osp(0, r_in_bytes);
|
|
||||||
const l_i_b_str = i2osp(lenInBytes, 2);
|
|
||||||
const b = new Array<Uint8Array>(ell);
|
|
||||||
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));
|
|
||||||
for (let i = 1; i <= ell; i++) {
|
|
||||||
const args = [strxor(b_0, b[i - 1]), i2osp(i + 1, 1), DST_prime];
|
|
||||||
b[i] = H(concatBytes(...args));
|
|
||||||
}
|
|
||||||
const pseudo_random_bytes = concatBytes(...b);
|
|
||||||
return pseudo_random_bytes.slice(0, lenInBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// hashes arbitrary-length byte strings to a list of one or more elements of a finite field F
|
|
||||||
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3
|
|
||||||
// Inputs:
|
|
||||||
// msg - a byte string containing the message to hash.
|
|
||||||
// count - the number of elements of F to output.
|
|
||||||
// Outputs:
|
|
||||||
// [u_0, ..., u_(count - 1)], a list of field elements.
|
|
||||||
export function hash_to_field(msg: Uint8Array, count: number, options: htfOpts): bigint[][] {
|
|
||||||
// if options is provided but incomplete, fill any missing fields with the
|
|
||||||
// value in hftDefaults (ie hash to G2).
|
|
||||||
const log2p = options.p.toString(2).length;
|
|
||||||
const L = Math.ceil((log2p + options.k) / 8); // section 5.1 of ietf draft link above
|
|
||||||
const len_in_bytes = count * options.m * L;
|
|
||||||
const DST = stringToBytes(options.DST);
|
|
||||||
let pseudo_random_bytes = msg;
|
|
||||||
if (options.expand) {
|
|
||||||
pseudo_random_bytes = expand_message_xmd(msg, DST, len_in_bytes, options.hash);
|
|
||||||
}
|
|
||||||
const u = new Array(count);
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const e = new Array(options.m);
|
|
||||||
for (let j = 0; j < options.m; j++) {
|
|
||||||
const elm_offset = L * (j + i * options.m);
|
|
||||||
const tv = pseudo_random_bytes.subarray(elm_offset, elm_offset + L);
|
|
||||||
e[j] = mod.mod(os2ip(tv), options.p);
|
|
||||||
}
|
|
||||||
u[i] = e;
|
|
||||||
}
|
|
||||||
return u;
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
/*! @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 { sha512 } from '@noble/hashes/sha512';
|
||||||
import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
|
import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
|
||||||
import { twistedEdwards } from '@noble/curves/edwards';
|
import { twistedEdwards } from './abstract/edwards.js';
|
||||||
import { blake2s } from '@noble/hashes/blake2s';
|
import { blake2s } from '@noble/hashes/blake2s';
|
||||||
import { Fp } from '@noble/curves/modular';
|
import { Field } from './abstract/modular.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* jubjub Twisted Edwards curve.
|
* jubjub Twisted Edwards curve.
|
||||||
* https://neuromancer.sk/std/other/JubJub
|
* https://neuromancer.sk/std/other/JubJub
|
||||||
|
* jubjub does not use EdDSA, so `hash`/sha512 params are passed because interface expects them.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const jubjub = twistedEdwards({
|
export const jubjub = twistedEdwards({
|
||||||
@@ -15,16 +16,16 @@ export const jubjub = twistedEdwards({
|
|||||||
a: BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000'),
|
a: BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000'),
|
||||||
d: BigInt('0x2a9318e74bfa2b48f5fd9207e6bd7fd4292d7f6d37579d2601065fd6d6343eb1'),
|
d: BigInt('0x2a9318e74bfa2b48f5fd9207e6bd7fd4292d7f6d37579d2601065fd6d6343eb1'),
|
||||||
// Finite field 𝔽p over which we'll do calculations
|
// Finite field 𝔽p over which we'll do calculations
|
||||||
Fp: Fp(BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001')),
|
// Same value as bls12-381 Fr (not Fp)
|
||||||
// Subgroup order: how many points ed25519 has
|
Fp: Field(BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001')),
|
||||||
// 2n ** 252n + 27742317777372353535851937790883648493n;
|
// Subgroup order: how many points curve has
|
||||||
n: BigInt('0xe7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7'),
|
n: BigInt('0xe7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7'),
|
||||||
// Cofactor
|
// Cofactor
|
||||||
h: BigInt(8),
|
h: BigInt(8),
|
||||||
// Base point (x, y) aka generator point
|
// Base point (x, y) aka generator point
|
||||||
Gx: BigInt('0x11dafe5d23e1218086a365b99fbf3d3be72f6afd7d1f72623e6b071492d1122b'),
|
Gx: BigInt('0x11dafe5d23e1218086a365b99fbf3d3be72f6afd7d1f72623e6b071492d1122b'),
|
||||||
Gy: BigInt('0x1d523cf1ddab1a1793132e78c866c0c33e26ba5cc220fed7cc3f870e59d292aa'),
|
Gy: BigInt('0x1d523cf1ddab1a1793132e78c866c0c33e26ba5cc220fed7cc3f870e59d292aa'),
|
||||||
hash: sha256,
|
hash: sha512,
|
||||||
randomBytes,
|
randomBytes,
|
||||||
} as const);
|
} as const);
|
||||||
|
|
||||||
@@ -38,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');
|
||||||
301
src/modular.ts
301
src/modular.ts
@@ -1,301 +0,0 @@
|
|||||||
/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
||||||
import * as utils from './utils.js';
|
|
||||||
// Utilities for modular arithmetics
|
|
||||||
const _0n = BigInt(0);
|
|
||||||
const _1n = BigInt(1);
|
|
||||||
const _2n = BigInt(2);
|
|
||||||
|
|
||||||
// Calculates a modulo b
|
|
||||||
export function mod(a: bigint, b: bigint): bigint {
|
|
||||||
const result = a % b;
|
|
||||||
return result >= _0n ? result : b + result;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Efficiently exponentiate num to power and do modular division.
|
|
||||||
* Unsafe in some contexts: uses ladder, so can expose bigint bits.
|
|
||||||
* @example
|
|
||||||
* powMod(2n, 6n, 11n) // 64n % 11n == 9n
|
|
||||||
*/
|
|
||||||
// TODO: use field version && remove
|
|
||||||
export function pow(num: bigint, power: bigint, modulo: bigint): bigint {
|
|
||||||
if (modulo <= _0n || power < _0n) throw new Error('Expected power/modulo > 0');
|
|
||||||
if (modulo === _1n) return _0n;
|
|
||||||
let res = _1n;
|
|
||||||
while (power > _0n) {
|
|
||||||
if (power & _1n) res = (res * num) % modulo;
|
|
||||||
num = (num * num) % modulo;
|
|
||||||
power >>= _1n;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
let res = x;
|
|
||||||
while (power-- > _0n) {
|
|
||||||
res *= res;
|
|
||||||
res %= modulo;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inverses number over modulo
|
|
||||||
export function invert(number: bigint, modulo: bigint): bigint {
|
|
||||||
if (number === _0n || modulo <= _0n) {
|
|
||||||
throw new Error(`invert: expected positive integers, got n=${number} mod=${modulo}`);
|
|
||||||
}
|
|
||||||
// Eucledian GCD https://brilliant.org/wiki/extended-euclidean-algorithm/
|
|
||||||
let a = mod(number, modulo);
|
|
||||||
let b = modulo;
|
|
||||||
// prettier-ignore
|
|
||||||
let x = _0n, y = _1n, u = _1n, v = _0n;
|
|
||||||
while (a !== _0n) {
|
|
||||||
const q = b / a;
|
|
||||||
const r = b % a;
|
|
||||||
const m = x - u * q;
|
|
||||||
const n = y - v * q;
|
|
||||||
// prettier-ignore
|
|
||||||
b = a, a = r, x = u, y = v, u = m, v = n;
|
|
||||||
}
|
|
||||||
const gcd = b;
|
|
||||||
if (gcd !== _1n) throw new Error('invert: does not exist');
|
|
||||||
return mod(x, modulo);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates Legendre symbol (a | p), which denotes the value of a^((p-1)/2) (mod p).
|
|
||||||
* * (a | p) ≡ 1 if a is a square (mod p)
|
|
||||||
* * (a | p) ≡ -1 if a is not a square (mod p)
|
|
||||||
* * (a | p) ≡ 0 if a ≡ 0 (mod p)
|
|
||||||
*/
|
|
||||||
export function legendre(num: bigint, fieldPrime: bigint): bigint {
|
|
||||||
return pow(num, (fieldPrime - _1n) / _2n, fieldPrime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates square root of a number in a finite field.
|
|
||||||
*/
|
|
||||||
// TODO: rewrite as generic Fp function && remove bls versions
|
|
||||||
export function sqrt(number: bigint, modulo: bigint): bigint {
|
|
||||||
// prettier-ignore
|
|
||||||
const _3n = BigInt(3), _4n = BigInt(4), _5n = BigInt(5), _8n = BigInt(8);
|
|
||||||
const n = number;
|
|
||||||
const P = modulo;
|
|
||||||
const p1div4 = (P + _1n) / _4n;
|
|
||||||
|
|
||||||
// P ≡ 3 (mod 4)
|
|
||||||
// sqrt n = n^((P+1)/4)
|
|
||||||
if (P % _4n === _3n) {
|
|
||||||
// Not all roots possible!
|
|
||||||
// const ORDER =
|
|
||||||
// 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaabn;
|
|
||||||
// const NUM = 72057594037927816n;
|
|
||||||
// TODO: fix sqrtMod in secp256k1
|
|
||||||
const root = pow(n, p1div4, P);
|
|
||||||
if (mod(root * root, modulo) !== number) throw new Error('Cannot find square root');
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
// P ≡ 5 (mod 8)
|
|
||||||
if (P % _8n === _5n) {
|
|
||||||
const n2 = mod(n * _2n, P);
|
|
||||||
const v = pow(n2, (P - _5n) / _8n, P);
|
|
||||||
const nv = mod(n * v, P);
|
|
||||||
const i = mod(_2n * nv * v, P);
|
|
||||||
const r = mod(nv * (i - _1n), P);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other cases: Tonelli-Shanks algorithm
|
|
||||||
if (legendre(n, P) !== _1n) throw new Error('Cannot find square root');
|
|
||||||
let q: bigint, s: number, z: bigint;
|
|
||||||
for (q = P - _1n, s = 0; q % _2n === _0n; q /= _2n, s++);
|
|
||||||
if (s === 1) return pow(n, p1div4, P);
|
|
||||||
for (z = _2n; z < P && legendre(z, P) !== P - _1n; z++);
|
|
||||||
|
|
||||||
let c = pow(z, q, P);
|
|
||||||
let r = pow(n, (q + _1n) / _2n, P);
|
|
||||||
let t = pow(n, q, P);
|
|
||||||
|
|
||||||
let t2 = _0n;
|
|
||||||
while (mod(t - _1n, P) !== _0n) {
|
|
||||||
t2 = mod(t * t, P);
|
|
||||||
let i;
|
|
||||||
for (i = 1; i < s; i++) {
|
|
||||||
if (mod(t2 - _1n, P) === _0n) break;
|
|
||||||
t2 = mod(t2 * t2, P);
|
|
||||||
}
|
|
||||||
let b = pow(c, BigInt(1 << (s - i - 1)), P);
|
|
||||||
r = mod(r * b, P);
|
|
||||||
c = mod(b * b, P);
|
|
||||||
t = mod(t * c, P);
|
|
||||||
s = i;
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Little-endian check for first LE bit (last BE bit);
|
|
||||||
export const isNegativeLE = (num: bigint, modulo: bigint) => (mod(num, modulo) & _1n) === _1n;
|
|
||||||
|
|
||||||
// Currently completly inconsistent naming:
|
|
||||||
// - readable: add, mul, sqr, sqrt, inv, div, pow, eq, sub
|
|
||||||
// - unreadable mess: addition, multiply, square, squareRoot, inversion, divide, power, equals, subtract
|
|
||||||
|
|
||||||
export interface Field<T> {
|
|
||||||
ORDER: bigint;
|
|
||||||
BYTES: number;
|
|
||||||
BITS: number;
|
|
||||||
MASK: bigint;
|
|
||||||
ZERO: T;
|
|
||||||
ONE: T;
|
|
||||||
// 1-arg
|
|
||||||
create: (num: T) => T;
|
|
||||||
isValid: (num: T) => boolean;
|
|
||||||
isZero: (num: T) => boolean;
|
|
||||||
negate(num: T): T;
|
|
||||||
invert(num: T): T;
|
|
||||||
sqrt(num: T): T;
|
|
||||||
square(num: T): T;
|
|
||||||
// 2-args
|
|
||||||
equals(lhs: T, rhs: T): boolean;
|
|
||||||
add(lhs: T, rhs: T): T;
|
|
||||||
subtract(lhs: T, rhs: T): T;
|
|
||||||
multiply(lhs: T, rhs: T | bigint): T;
|
|
||||||
pow(lhs: T, power: bigint): T;
|
|
||||||
div(lhs: T, rhs: T | bigint): T;
|
|
||||||
// N for NonNormalized (for now)
|
|
||||||
addN(lhs: T, rhs: T): T;
|
|
||||||
subtractN(lhs: T, rhs: T): T;
|
|
||||||
multiplyN(lhs: T, rhs: T | bigint): T;
|
|
||||||
squareN(num: T): T;
|
|
||||||
|
|
||||||
// Optional
|
|
||||||
isOdd?(num: T): boolean; // Odd instead of even since we have it for Fp2
|
|
||||||
legendre?(num: T): T;
|
|
||||||
pow(lhs: T, power: bigint): T;
|
|
||||||
invertBatch: (lst: T[]) => T[];
|
|
||||||
toBytes(num: T): Uint8Array;
|
|
||||||
fromBytes(bytes: Uint8Array): T;
|
|
||||||
}
|
|
||||||
// prettier-ignore
|
|
||||||
const FIELD_FIELDS = [
|
|
||||||
'create', 'isValid', 'isZero', 'negate', 'invert', 'sqrt', 'square',
|
|
||||||
'equals', 'add', 'subtract', 'multiply', 'pow', 'div',
|
|
||||||
'addN', 'subtractN', 'multiplyN', 'squareN'
|
|
||||||
] as const;
|
|
||||||
export function validateField<T>(field: Field<T>) {
|
|
||||||
for (const i of ['ORDER', 'MASK'] as const) {
|
|
||||||
if (typeof field[i] !== 'bigint')
|
|
||||||
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`);
|
|
||||||
}
|
|
||||||
for (const i of ['BYTES', 'BITS'] as const) {
|
|
||||||
if (typeof field[i] !== 'number')
|
|
||||||
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`);
|
|
||||||
}
|
|
||||||
for (const i of FIELD_FIELDS) {
|
|
||||||
if (typeof field[i] !== 'function')
|
|
||||||
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic field functions
|
|
||||||
export function FpPow<T>(f: Field<T>, num: T, power: bigint): T {
|
|
||||||
// Should have same speed as pow for bigints
|
|
||||||
// TODO: benchmark!
|
|
||||||
if (power < _0n) throw new Error('Expected power > 0');
|
|
||||||
if (power === _0n) return f.ONE;
|
|
||||||
if (power === _1n) return num;
|
|
||||||
let p = f.ONE;
|
|
||||||
let d = num;
|
|
||||||
while (power > _0n) {
|
|
||||||
if (power & _1n) p = f.multiply(p, d);
|
|
||||||
d = f.square(d);
|
|
||||||
power >>= 1n;
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FpInvertBatch<T>(f: Field<T>, nums: T[]): T[] {
|
|
||||||
const tmp = new Array(nums.length);
|
|
||||||
// Walk from first to last, multiply them by each other MOD p
|
|
||||||
const lastMultiplied = nums.reduce((acc, num, i) => {
|
|
||||||
if (f.isZero(num)) return acc;
|
|
||||||
tmp[i] = acc;
|
|
||||||
return f.multiply(acc, num);
|
|
||||||
}, f.ONE);
|
|
||||||
// Invert last element
|
|
||||||
const inverted = f.invert(lastMultiplied);
|
|
||||||
// Walk from last to first, multiply them by inverted each other MOD p
|
|
||||||
nums.reduceRight((acc, num, i) => {
|
|
||||||
if (f.isZero(num)) return acc;
|
|
||||||
tmp[i] = f.multiply(acc, tmp[i]);
|
|
||||||
return f.multiply(acc, num);
|
|
||||||
}, inverted);
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FpDiv<T>(f: Field<T>, lhs: T, rhs: T | bigint): T {
|
|
||||||
return f.multiply(lhs, typeof rhs === 'bigint' ? invert(rhs, f.ORDER) : f.invert(rhs));
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: very fragile, always bench. Major performance points:
|
|
||||||
// - NonNormalized ops
|
|
||||||
// - Object.freeze
|
|
||||||
// - same shape of object (don't add/remove keys)
|
|
||||||
export function Fp(
|
|
||||||
ORDER: bigint,
|
|
||||||
bitLen?: number,
|
|
||||||
isLE = false,
|
|
||||||
redef: Partial<Field<bigint>> = {}
|
|
||||||
): Readonly<Field<bigint>> {
|
|
||||||
if (ORDER <= _0n) throw new Error(`Expected Fp ORDER > 0, got ${ORDER}`);
|
|
||||||
const { nBitLength: BITS, nByteLength: BYTES } = utils.nLength(ORDER, bitLen);
|
|
||||||
if (BYTES > 2048) throw new Error('Field lengths over 2048 bytes are not supported');
|
|
||||||
const sqrtP = (num: bigint) => sqrt(num, ORDER);
|
|
||||||
const f: Field<bigint> = Object.freeze({
|
|
||||||
ORDER,
|
|
||||||
BITS,
|
|
||||||
BYTES,
|
|
||||||
MASK: utils.bitMask(BITS),
|
|
||||||
ZERO: _0n,
|
|
||||||
ONE: _1n,
|
|
||||||
create: (num) => mod(num, ORDER),
|
|
||||||
isValid: (num) => {
|
|
||||||
if (typeof num !== 'bigint')
|
|
||||||
throw new Error(`Invalid field element: expected bigint, got ${typeof num}`);
|
|
||||||
return _0n <= num && num < ORDER;
|
|
||||||
},
|
|
||||||
isZero: (num) => num === _0n,
|
|
||||||
isOdd: (num) => (num & _1n) === _1n,
|
|
||||||
negate: (num) => mod(-num, ORDER),
|
|
||||||
equals: (lhs, rhs) => lhs === rhs,
|
|
||||||
|
|
||||||
square: (num) => mod(num * num, ORDER),
|
|
||||||
add: (lhs, rhs) => mod(lhs + rhs, ORDER),
|
|
||||||
subtract: (lhs, rhs) => mod(lhs - rhs, ORDER),
|
|
||||||
multiply: (lhs, rhs) => mod(lhs * rhs, ORDER),
|
|
||||||
pow: (num, power) => FpPow(f, num, power),
|
|
||||||
div: (lhs, rhs) => mod(lhs * invert(rhs, ORDER), ORDER),
|
|
||||||
|
|
||||||
// Same as above, but doesn't normalize
|
|
||||||
squareN: (num) => num * num,
|
|
||||||
addN: (lhs, rhs) => lhs + rhs,
|
|
||||||
subtractN: (lhs, rhs) => lhs - rhs,
|
|
||||||
multiplyN: (lhs, rhs) => lhs * rhs,
|
|
||||||
|
|
||||||
invert: (num) => invert(num, ORDER),
|
|
||||||
sqrt: redef.sqrt || sqrtP,
|
|
||||||
invertBatch: (lst) => FpInvertBatch(f, lst),
|
|
||||||
|
|
||||||
toBytes: (num) =>
|
|
||||||
isLE ? utils.numberToBytesLE(num, BYTES) : utils.numberToBytesBE(num, BYTES),
|
|
||||||
|
|
||||||
fromBytes: (bytes) => {
|
|
||||||
if (bytes.length !== BYTES)
|
|
||||||
throw new Error(`Fp.fromBytes: expected ${BYTES}, got ${bytes.length}`);
|
|
||||||
return isLE ? utils.bytesToNumberLE(bytes) : utils.bytesToNumberBE(bytes);
|
|
||||||
},
|
|
||||||
} as Field<bigint>);
|
|
||||||
return Object.freeze(f);
|
|
||||||
}
|
|
||||||
53
src/p256.ts
Normal file
53
src/p256.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
|
import { createCurve } from './_shortw_utils.js';
|
||||||
|
import { sha256 } from '@noble/hashes/sha256';
|
||||||
|
import { Field } from './abstract/modular.js';
|
||||||
|
import { mapToCurveSimpleSWU } from './abstract/weierstrass.js';
|
||||||
|
import * as htf from './abstract/hash-to-curve.js';
|
||||||
|
|
||||||
|
// NIST secp256r1 aka P256
|
||||||
|
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-256
|
||||||
|
|
||||||
|
// Field over which we'll do calculations; 2n**224n * (2n**32n-1n) + 2n**192n + 2n**96n-1n
|
||||||
|
const Fp = Field(BigInt('0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff'));
|
||||||
|
const CURVE_A = Fp.create(BigInt('-3'));
|
||||||
|
const CURVE_B = BigInt('0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b');
|
||||||
|
|
||||||
|
const mapSWU = mapToCurveSimpleSWU(Fp, {
|
||||||
|
A: CURVE_A,
|
||||||
|
B: CURVE_B,
|
||||||
|
Z: Fp.create(BigInt('-10')),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const P256 = createCurve(
|
||||||
|
{
|
||||||
|
// Params: a, b
|
||||||
|
a: CURVE_A,
|
||||||
|
b: CURVE_B,
|
||||||
|
Fp,
|
||||||
|
// Curve order, total count of valid points in the field
|
||||||
|
n: BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551'),
|
||||||
|
// Base point (x, y) aka generator point
|
||||||
|
Gx: BigInt('0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296'),
|
||||||
|
Gy: BigInt('0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5'),
|
||||||
|
h: BigInt(1),
|
||||||
|
lowS: false,
|
||||||
|
} as const,
|
||||||
|
sha256
|
||||||
|
);
|
||||||
|
export const secp256r1 = P256;
|
||||||
|
|
||||||
|
const { hashToCurve, encodeToCurve } = htf.createHasher(
|
||||||
|
secp256r1.ProjectivePoint,
|
||||||
|
(scalars: bigint[]) => mapSWU(scalars[0]),
|
||||||
|
{
|
||||||
|
DST: 'P256_XMD:SHA-256_SSWU_RO_',
|
||||||
|
encodeDST: 'P256_XMD:SHA-256_SSWU_NU_',
|
||||||
|
p: Fp.ORDER,
|
||||||
|
m: 1,
|
||||||
|
k: 128,
|
||||||
|
expand: 'xmd',
|
||||||
|
hash: sha256,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export { hashToCurve, encodeToCurve };
|
||||||
57
src/p384.ts
Normal file
57
src/p384.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
|
import { createCurve } from './_shortw_utils.js';
|
||||||
|
import { sha384 } from '@noble/hashes/sha512';
|
||||||
|
import { Field } from './abstract/modular.js';
|
||||||
|
import { mapToCurveSimpleSWU } from './abstract/weierstrass.js';
|
||||||
|
import * as htf from './abstract/hash-to-curve.js';
|
||||||
|
|
||||||
|
// NIST secp384r1 aka P384
|
||||||
|
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-384
|
||||||
|
|
||||||
|
// Field over which we'll do calculations. 2n**384n - 2n**128n - 2n**96n + 2n**32n - 1n
|
||||||
|
// prettier-ignore
|
||||||
|
const P = BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff');
|
||||||
|
const Fp = Field(P);
|
||||||
|
const CURVE_A = Fp.create(BigInt('-3'));
|
||||||
|
// prettier-ignore
|
||||||
|
const CURVE_B = BigInt('0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef');
|
||||||
|
|
||||||
|
const mapSWU = mapToCurveSimpleSWU(Fp, {
|
||||||
|
A: CURVE_A,
|
||||||
|
B: CURVE_B,
|
||||||
|
Z: Fp.create(BigInt('-12')),
|
||||||
|
});
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
export const P384 = createCurve({
|
||||||
|
// Params: a, b
|
||||||
|
a: CURVE_A,
|
||||||
|
b: CURVE_B,
|
||||||
|
// Field over which we'll do calculations. 2n**384n - 2n**128n - 2n**96n + 2n**32n - 1n
|
||||||
|
Fp,
|
||||||
|
// Curve order, total count of valid points in the field.
|
||||||
|
n: BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973'),
|
||||||
|
// Base point (x, y) aka generator point
|
||||||
|
Gx: BigInt('0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7'),
|
||||||
|
Gy: BigInt('0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f'),
|
||||||
|
h: BigInt(1),
|
||||||
|
lowS: false,
|
||||||
|
} as const,
|
||||||
|
sha384
|
||||||
|
);
|
||||||
|
export const secp384r1 = P384;
|
||||||
|
|
||||||
|
const { hashToCurve, encodeToCurve } = htf.createHasher(
|
||||||
|
secp384r1.ProjectivePoint,
|
||||||
|
(scalars: bigint[]) => mapSWU(scalars[0]),
|
||||||
|
{
|
||||||
|
DST: 'P384_XMD:SHA-384_SSWU_RO_',
|
||||||
|
encodeDST: 'P384_XMD:SHA-384_SSWU_NU_',
|
||||||
|
p: Fp.ORDER,
|
||||||
|
m: 1,
|
||||||
|
k: 192,
|
||||||
|
expand: 'xmd',
|
||||||
|
hash: sha384,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export { hashToCurve, encodeToCurve };
|
||||||
57
src/p521.ts
Normal file
57
src/p521.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
|
import { createCurve } from './_shortw_utils.js';
|
||||||
|
import { sha512 } from '@noble/hashes/sha512';
|
||||||
|
import { Field } from './abstract/modular.js';
|
||||||
|
import { mapToCurveSimpleSWU } from './abstract/weierstrass.js';
|
||||||
|
import * as htf from './abstract/hash-to-curve.js';
|
||||||
|
|
||||||
|
// NIST secp521r1 aka P521
|
||||||
|
// Note that it's 521, which differs from 512 of its hash function.
|
||||||
|
// https://www.secg.org/sec2-v2.pdf, https://neuromancer.sk/std/nist/P-521
|
||||||
|
|
||||||
|
// Field over which we'll do calculations; 2n**521n - 1n
|
||||||
|
// prettier-ignore
|
||||||
|
const P = BigInt('0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
|
||||||
|
const Fp = Field(P);
|
||||||
|
|
||||||
|
const CURVE_A = Fp.create(BigInt('-3'));
|
||||||
|
// prettier-ignore
|
||||||
|
const CURVE_B = BigInt('0x0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00');
|
||||||
|
|
||||||
|
const mapSWU = mapToCurveSimpleSWU(Fp, {
|
||||||
|
A: CURVE_A,
|
||||||
|
B: CURVE_B,
|
||||||
|
Z: Fp.create(BigInt('-4')),
|
||||||
|
});
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
export const P521 = createCurve({
|
||||||
|
// Params: a, b
|
||||||
|
a: CURVE_A,
|
||||||
|
b: CURVE_B,
|
||||||
|
Fp,
|
||||||
|
// Curve order, total count of valid points in the field
|
||||||
|
n: BigInt('0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409'),
|
||||||
|
// Base point (x, y) aka generator point
|
||||||
|
Gx: BigInt('0x00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66'),
|
||||||
|
Gy: BigInt('0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650'),
|
||||||
|
h: BigInt(1),
|
||||||
|
lowS: false,
|
||||||
|
allowedPrivateKeyLengths: [130, 131, 132] // P521 keys are variable-length. Normalize to 132b
|
||||||
|
} as const, sha512);
|
||||||
|
export const secp521r1 = P521;
|
||||||
|
|
||||||
|
const { hashToCurve, encodeToCurve } = htf.createHasher(
|
||||||
|
secp521r1.ProjectivePoint,
|
||||||
|
(scalars: bigint[]) => mapSWU(scalars[0]),
|
||||||
|
{
|
||||||
|
DST: 'P521_XMD:SHA-512_SSWU_RO_',
|
||||||
|
encodeDST: 'P521_XMD:SHA-512_SSWU_NU_',
|
||||||
|
p: Fp.ORDER,
|
||||||
|
m: 1,
|
||||||
|
k: 256,
|
||||||
|
expand: 'xmd',
|
||||||
|
hash: sha512,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export { hashToCurve, encodeToCurve };
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
/*! @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 { weierstrass } from '@noble/curves/weierstrass';
|
import { weierstrass } from './abstract/weierstrass.js';
|
||||||
import { getHash } from './_shortw_utils.js';
|
import { getHash } from './_shortw_utils.js';
|
||||||
import * as mod from '@noble/curves/modular';
|
import * as mod from './abstract/modular.js';
|
||||||
|
|
||||||
const p = BigInt('0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001');
|
export const p = BigInt('0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001');
|
||||||
const q = BigInt('0x40000000000000000000000000000000224698fc0994a8dd8c46eb2100000001');
|
export const q = BigInt('0x40000000000000000000000000000000224698fc0994a8dd8c46eb2100000001');
|
||||||
|
|
||||||
// https://neuromancer.sk/std/other/Pallas
|
// https://neuromancer.sk/std/other/Pallas
|
||||||
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),
|
||||||
269
src/secp256k1.ts
Normal file
269
src/secp256k1.ts
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
|
import { sha256 } from '@noble/hashes/sha256';
|
||||||
|
import { randomBytes } from '@noble/hashes/utils';
|
||||||
|
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';
|
||||||
|
import { bytesToNumberBE, concatBytes, ensureBytes, numberToBytesBE } from './abstract/utils.js';
|
||||||
|
import * as htf from './abstract/hash-to-curve.js';
|
||||||
|
import { createCurve } from './_shortw_utils.js';
|
||||||
|
|
||||||
|
const secp256k1P = BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f');
|
||||||
|
const secp256k1N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141');
|
||||||
|
const _1n = BigInt(1);
|
||||||
|
const _2n = BigInt(2);
|
||||||
|
const divNearest = (a: bigint, b: bigint) => (a + b / _2n) / b;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* √n = n^((p+1)/4) for fields p = 3 mod 4. We unwrap the loop and multiply bit-by-bit.
|
||||||
|
* (P+1n/4n).toString(2) would produce bits [223x 1, 0, 22x 1, 4x 0, 11, 00]
|
||||||
|
*/
|
||||||
|
function sqrtMod(y: bigint): bigint {
|
||||||
|
const P = secp256k1P;
|
||||||
|
// prettier-ignore
|
||||||
|
const _3n = BigInt(3), _6n = BigInt(6), _11n = BigInt(11), _22n = BigInt(22);
|
||||||
|
// prettier-ignore
|
||||||
|
const _23n = BigInt(23), _44n = BigInt(44), _88n = BigInt(88);
|
||||||
|
const b2 = (y * y * y) % P; // x^3, 11
|
||||||
|
const b3 = (b2 * b2 * y) % P; // x^7
|
||||||
|
const b6 = (pow2(b3, _3n, P) * b3) % P;
|
||||||
|
const b9 = (pow2(b6, _3n, P) * b3) % P;
|
||||||
|
const b11 = (pow2(b9, _2n, P) * b2) % P;
|
||||||
|
const b22 = (pow2(b11, _11n, P) * b11) % P;
|
||||||
|
const b44 = (pow2(b22, _22n, P) * b22) % P;
|
||||||
|
const b88 = (pow2(b44, _44n, P) * b44) % P;
|
||||||
|
const b176 = (pow2(b88, _88n, P) * b88) % P;
|
||||||
|
const b220 = (pow2(b176, _44n, P) * b44) % P;
|
||||||
|
const b223 = (pow2(b220, _3n, P) * b3) % P;
|
||||||
|
const t1 = (pow2(b223, _23n, P) * b22) % P;
|
||||||
|
const t2 = (pow2(t1, _6n, P) * b2) % P;
|
||||||
|
const root = pow2(t2, _2n, P);
|
||||||
|
if (!Fp.eql(Fp.sqr(root), y)) throw new Error('Cannot find square root');
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Fp = Field(secp256k1P, undefined, undefined, { sqrt: sqrtMod });
|
||||||
|
|
||||||
|
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,
|
||||||
|
[
|
||||||
|
// xNum
|
||||||
|
[
|
||||||
|
'0x8e38e38e38e38e38e38e38e38e38e38e38e38e38e38e38e38e38e38daaaaa8c7',
|
||||||
|
'0x7d3d4c80bc321d5b9f315cea7fd44c5d595d2fc0bf63b92dfff1044f17c6581',
|
||||||
|
'0x534c328d23f234e6e2a413deca25caece4506144037c40314ecbd0b53d9dd262',
|
||||||
|
'0x8e38e38e38e38e38e38e38e38e38e38e38e38e38e38e38e38e38e38daaaaa88c',
|
||||||
|
],
|
||||||
|
// xDen
|
||||||
|
[
|
||||||
|
'0xd35771193d94918a9ca34ccbb7b640dd86cd409542f8487d9fe6b745781eb49b',
|
||||||
|
'0xedadc6f64383dc1df7c4b2d51b54225406d36b641f5e41bbc52a56612a8c6d14',
|
||||||
|
'0x0000000000000000000000000000000000000000000000000000000000000001', // LAST 1
|
||||||
|
],
|
||||||
|
// yNum
|
||||||
|
[
|
||||||
|
'0x4bda12f684bda12f684bda12f684bda12f684bda12f684bda12f684b8e38e23c',
|
||||||
|
'0xc75e0c32d5cb7c0fa9d0a54b12a0a6d5647ab046d686da6fdffc90fc201d71a3',
|
||||||
|
'0x29a6194691f91a73715209ef6512e576722830a201be2018a765e85a9ecee931',
|
||||||
|
'0x2f684bda12f684bda12f684bda12f684bda12f684bda12f684bda12f38e38d84',
|
||||||
|
],
|
||||||
|
// yDen
|
||||||
|
[
|
||||||
|
'0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffff93b',
|
||||||
|
'0x7a06534bb8bdb49fd5e9e6632722c2989467c1bfc8e8d978dfb425d2685c2573',
|
||||||
|
'0x6484aa716545ca2cf3a70c3fa8fe337e0a3d21162f0d6299a7bf8192bfd2a76f',
|
||||||
|
'0x0000000000000000000000000000000000000000000000000000000000000001', // LAST 1
|
||||||
|
],
|
||||||
|
].map((i) => i.map((j) => BigInt(j))) as [bigint[], bigint[], bigint[], bigint[]]
|
||||||
|
);
|
||||||
|
const mapSWU = mapToCurveSimpleSWU(Fp, {
|
||||||
|
A: BigInt('0x3f8731abdd661adca08a5558f0f5d272e953d363cb6f0e5d405447c01a444533'),
|
||||||
|
B: BigInt('1771'),
|
||||||
|
Z: Fp.create(BigInt('-11')),
|
||||||
|
});
|
||||||
|
export const { hashToCurve, encodeToCurve } = htf.createHasher(
|
||||||
|
secp256k1.ProjectivePoint,
|
||||||
|
(scalars: bigint[]) => {
|
||||||
|
const { x, y } = mapSWU(Fp.create(scalars[0]));
|
||||||
|
return isoMap(x, y);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DST: 'secp256k1_XMD:SHA-256_SSWU_RO_',
|
||||||
|
encodeDST: 'secp256k1_XMD:SHA-256_SSWU_NU_',
|
||||||
|
p: Fp.ORDER,
|
||||||
|
m: 1,
|
||||||
|
k: 128,
|
||||||
|
expand: 'xmd',
|
||||||
|
hash: sha256,
|
||||||
|
}
|
||||||
|
);
|
||||||
187
src/utils.ts
187
src/utils.ts
@@ -1,187 +0,0 @@
|
|||||||
/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
||||||
import * as mod from './modular.js';
|
|
||||||
const _0n = BigInt(0);
|
|
||||||
const _1n = BigInt(1);
|
|
||||||
const _2n = BigInt(2);
|
|
||||||
|
|
||||||
// We accept hex strings besides Uint8Array for simplicity
|
|
||||||
export type Hex = Uint8Array | string;
|
|
||||||
// Very few implementations accept numbers, we do it to ease learning curve
|
|
||||||
export type PrivKey = Hex | bigint | number;
|
|
||||||
export type CHash = {
|
|
||||||
(message: Uint8Array | string): Uint8Array;
|
|
||||||
blockLen: number;
|
|
||||||
outputLen: number;
|
|
||||||
create(): any;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function validateOpts<FP, T>(curve: BasicCurve<FP> & T) {
|
|
||||||
mod.validateField(curve.Fp);
|
|
||||||
for (const i of ['n', 'h'] as const) {
|
|
||||||
if (typeof curve[i] !== 'bigint')
|
|
||||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
if (curve[i] === undefined) continue; // Optional
|
|
||||||
if (!Number.isSafeInteger(curve[i]))
|
|
||||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
|
|
||||||
}
|
|
||||||
// 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'));
|
|
||||||
export function bytesToHex(uint8a: Uint8Array): string {
|
|
||||||
if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
|
||||||
// pre-caching improves the speed 6x
|
|
||||||
let hex = '';
|
|
||||||
for (let i = 0; i < uint8a.length; i++) {
|
|
||||||
hex += hexes[uint8a[i]];
|
|
||||||
}
|
|
||||||
return hex;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function numberToHexUnpadded(num: number | bigint): string {
|
|
||||||
const hex = num.toString(16);
|
|
||||||
return hex.length & 1 ? `0${hex}` : hex;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hexToNumber(hex: string): bigint {
|
|
||||||
if (typeof hex !== 'string') {
|
|
||||||
throw new TypeError('hexToNumber: expected string, got ' + typeof hex);
|
|
||||||
}
|
|
||||||
// Big Endian
|
|
||||||
return BigInt(`0x${hex}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caching slows it down 2-3x
|
|
||||||
export function hexToBytes(hex: string): Uint8Array {
|
|
||||||
if (typeof hex !== 'string') {
|
|
||||||
throw new TypeError('hexToBytes: expected string, got ' + typeof hex);
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Big Endian
|
|
||||||
export function bytesToNumberBE(bytes: Uint8Array): bigint {
|
|
||||||
return hexToNumber(bytesToHex(bytes));
|
|
||||||
}
|
|
||||||
export function bytesToNumberLE(uint8a: Uint8Array): bigint {
|
|
||||||
if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
|
||||||
return BigInt('0x' + bytesToHex(Uint8Array.from(uint8a).reverse()));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const numberToBytesBE = (n: bigint, len: number) =>
|
|
||||||
hexToBytes(n.toString(16).padStart(len * 2, '0'));
|
|
||||||
export const numberToBytesLE = (n: bigint, len: number) => numberToBytesBE(n, len).reverse();
|
|
||||||
|
|
||||||
export function ensureBytes(hex: Hex, expectedLength?: number): Uint8Array {
|
|
||||||
// Uint8Array.from() instead of hash.slice() because node.js Buffer
|
|
||||||
// is instance of Uint8Array, and its slice() creates **mutable** copy
|
|
||||||
const bytes = hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex);
|
|
||||||
if (typeof expectedLength === 'number' && bytes.length !== expectedLength)
|
|
||||||
throw new Error(`Expected ${expectedLength} bytes`);
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copies several Uint8Arrays into one.
|
|
||||||
export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
|
|
||||||
if (!arrays.every((b) => b instanceof Uint8Array)) throw new Error('Uint8Array list expected');
|
|
||||||
if (arrays.length === 1) return arrays[0];
|
|
||||||
const length = arrays.reduce((a, arr) => a + arr.length, 0);
|
|
||||||
const result = new Uint8Array(length);
|
|
||||||
for (let i = 0, pad = 0; i < arrays.length; i++) {
|
|
||||||
const arr = arrays[i];
|
|
||||||
result.set(arr, pad);
|
|
||||||
pad += arr.length;
|
|
||||||
}
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
* As per FIPS 186 B.4.1.
|
|
||||||
* https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
|
|
||||||
* @param hash hash output from sha512, or a similar function
|
|
||||||
* @returns valid private scalar
|
|
||||||
*/
|
|
||||||
export function hashToPrivateScalar(hash: Hex, CURVE_ORDER: bigint, isLE = false): bigint {
|
|
||||||
hash = ensureBytes(hash);
|
|
||||||
const orderLen = nLength(CURVE_ORDER).nByteLength;
|
|
||||||
const minLen = orderLen + 8;
|
|
||||||
if (orderLen < 16 || hash.length < minLen || hash.length > 1024)
|
|
||||||
throw new Error('Expected valid bytes of private key as per FIPS 186');
|
|
||||||
const num = isLE ? bytesToNumberLE(hash) : bytesToNumberBE(hash);
|
|
||||||
return mod.mod(num, CURVE_ORDER - _1n) + _1n;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function equalBytes(b1: Uint8Array, b2: Uint8Array) {
|
|
||||||
// We don't care about timing attacks here
|
|
||||||
if (b1.length !== b2.length) return false;
|
|
||||||
for (let i = 0; i < b1.length; i++) if (b1[i] !== b2[i]) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bit operations
|
|
||||||
|
|
||||||
// Amount of bits inside bigint (Same as n.toString(2).length)
|
|
||||||
export function bitLen(n: bigint) {
|
|
||||||
let len;
|
|
||||||
for (len = 0; n > 0n; n >>= _1n, len += 1);
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
// Gets single bit at position. NOTE: first bit position is 0 (same as arrays)
|
|
||||||
// Same as !!+Array.from(n.toString(2)).reverse()[pos]
|
|
||||||
export const bitGet = (n: bigint, pos: number) => (n >> BigInt(pos)) & 1n;
|
|
||||||
// Sets single bit at position
|
|
||||||
export const bitSet = (n: bigint, pos: number, value: boolean) =>
|
|
||||||
n | ((value ? _1n : _0n) << BigInt(pos));
|
|
||||||
// Return mask for N bits (Same as BigInt(`0b${Array(i).fill('1').join('')}`))
|
|
||||||
// Not using ** operator with bigints for old engines.
|
|
||||||
export const bitMask = (n: number) => (_2n << BigInt(n - 1)) - _1n;
|
|
||||||
1242
src/weierstrass.ts
1242
src/weierstrass.ts
File diff suppressed because it is too large
Load Diff
44
test/_more-curves.helpers.js
Normal file
44
test/_more-curves.helpers.js
Normal 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
103
test/_poseidon.helpers.js
Normal 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];
|
||||||
|
}
|
||||||
749
test/basic.test.js
Normal file
749
test/basic.test.js
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
import { deepStrictEqual, throws } from 'assert';
|
||||||
|
import { should, describe } from 'micro-should';
|
||||||
|
import * as fc from 'fast-check';
|
||||||
|
import * as mod from '../esm/abstract/modular.js';
|
||||||
|
import { bytesToHex as toHex } from '../esm/abstract/utils.js';
|
||||||
|
// Generic tests for all curves in package
|
||||||
|
import { secp192r1, secp224r1 } from './_more-curves.helpers.js';
|
||||||
|
import { secp256r1 } from '../esm/p256.js';
|
||||||
|
import { secp384r1 } from '../esm/p384.js';
|
||||||
|
import { secp521r1 } from '../esm/p521.js';
|
||||||
|
import { secp256k1 } from '../esm/secp256k1.js';
|
||||||
|
import { ed25519, ed25519ctx, ed25519ph, x25519 } from '../esm/ed25519.js';
|
||||||
|
import { ed448, ed448ph } from '../esm/ed448.js';
|
||||||
|
import { pallas, vesta } from '../esm/pasta.js';
|
||||||
|
import { bn254 } from '../esm/bn.js';
|
||||||
|
import { jubjub } from '../esm/jubjub.js';
|
||||||
|
import { bls12_381 } from '../esm/bls12-381.js';
|
||||||
|
|
||||||
|
// Fields tests
|
||||||
|
const FIELDS = {
|
||||||
|
secp192r1: { Fp: [secp192r1.CURVE.Fp] },
|
||||||
|
secp224r1: { Fp: [secp224r1.CURVE.Fp] },
|
||||||
|
secp256r1: { Fp: [secp256r1.CURVE.Fp] },
|
||||||
|
secp521r1: { Fp: [secp521r1.CURVE.Fp] },
|
||||||
|
secp256k1: { Fp: [secp256k1.CURVE.Fp] },
|
||||||
|
jubjub: { Fp: [jubjub.CURVE.Fp] },
|
||||||
|
ed25519: { Fp: [ed25519.CURVE.Fp] },
|
||||||
|
ed448: { Fp: [ed448.CURVE.Fp] },
|
||||||
|
bn254: { Fp: [bn254.CURVE.Fp] },
|
||||||
|
pallas: { Fp: [pallas.CURVE.Fp] },
|
||||||
|
vesta: { Fp: [vesta.CURVE.Fp] },
|
||||||
|
bls12: {
|
||||||
|
Fp: [bls12_381.CURVE.Fp],
|
||||||
|
Fp2: [
|
||||||
|
bls12_381.CURVE.Fp2,
|
||||||
|
fc.array(fc.bigInt(1n, bls12_381.CURVE.Fp.ORDER - 1n), {
|
||||||
|
minLength: 2,
|
||||||
|
maxLength: 2,
|
||||||
|
}),
|
||||||
|
(Fp2, num) => Fp2.fromBigTuple([num[0], num[1]]),
|
||||||
|
],
|
||||||
|
// Fp6: [bls12_381.CURVE.Fp6],
|
||||||
|
Fp12: [
|
||||||
|
bls12_381.CURVE.Fp12,
|
||||||
|
fc.array(fc.bigInt(1n, bls12_381.CURVE.Fp.ORDER - 1n), {
|
||||||
|
minLength: 12,
|
||||||
|
maxLength: 12,
|
||||||
|
}),
|
||||||
|
(Fp12, num) => Fp12.fromBigTwelve(num),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const c in FIELDS) {
|
||||||
|
const curve = FIELDS[c];
|
||||||
|
for (const f in curve) {
|
||||||
|
const Fp = curve[f][0];
|
||||||
|
const name = `${c}/${f}:`;
|
||||||
|
const FC_BIGINT = curve[f][1] ? curve[f][1] : fc.bigInt(1n, Fp.ORDER - 1n);
|
||||||
|
|
||||||
|
const create = curve[f][2] ? curve[f][2].bind(null, Fp) : (num) => Fp.create(num);
|
||||||
|
describe(name, () => {
|
||||||
|
should('equality', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (num) => {
|
||||||
|
const a = create(num);
|
||||||
|
const b = create(num);
|
||||||
|
deepStrictEqual(Fp.eql(a, b), true);
|
||||||
|
deepStrictEqual(Fp.eql(b, a), true);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('non-equality', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => {
|
||||||
|
const a = create(num1);
|
||||||
|
const b = create(num2);
|
||||||
|
deepStrictEqual(Fp.eql(a, b), num1 === num2);
|
||||||
|
deepStrictEqual(Fp.eql(b, a), num1 === num2);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('add/subtract/commutativity', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => {
|
||||||
|
const a = create(num1);
|
||||||
|
const b = create(num2);
|
||||||
|
deepStrictEqual(Fp.add(a, b), Fp.add(b, a));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('add/subtract/associativity', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => {
|
||||||
|
const a = create(num1);
|
||||||
|
const b = create(num2);
|
||||||
|
const c = create(num3);
|
||||||
|
deepStrictEqual(Fp.add(a, Fp.add(b, c)), Fp.add(Fp.add(a, b), c));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('add/subtract/x+0=x', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (num) => {
|
||||||
|
const a = create(num);
|
||||||
|
deepStrictEqual(Fp.add(a, Fp.ZERO), a);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('add/subtract/x-0=x', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (num) => {
|
||||||
|
const a = create(num);
|
||||||
|
deepStrictEqual(Fp.sub(a, Fp.ZERO), a);
|
||||||
|
deepStrictEqual(Fp.sub(a, a), Fp.ZERO);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('add/subtract/negate equality', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (num1) => {
|
||||||
|
const a = create(num1);
|
||||||
|
const b = create(num1);
|
||||||
|
deepStrictEqual(Fp.sub(Fp.ZERO, a), Fp.neg(a));
|
||||||
|
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))));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('add/subtract/negate', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (num) => {
|
||||||
|
const a = create(num);
|
||||||
|
deepStrictEqual(Fp.neg(a), Fp.sub(Fp.ZERO, a));
|
||||||
|
deepStrictEqual(Fp.neg(a), Fp.mul(a, Fp.create(-1n)));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('negate(0)', () => {
|
||||||
|
deepStrictEqual(Fp.neg(Fp.ZERO), Fp.ZERO);
|
||||||
|
});
|
||||||
|
|
||||||
|
should('multiply/commutativity', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => {
|
||||||
|
const a = create(num1);
|
||||||
|
const b = create(num2);
|
||||||
|
deepStrictEqual(Fp.mul(a, b), Fp.mul(b, a));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('multiply/associativity', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => {
|
||||||
|
const a = create(num1);
|
||||||
|
const b = create(num2);
|
||||||
|
const c = create(num3);
|
||||||
|
deepStrictEqual(Fp.mul(a, Fp.mul(b, c)), Fp.mul(Fp.mul(a, b), c));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('multiply/distributivity', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => {
|
||||||
|
const a = create(num1);
|
||||||
|
const b = create(num2);
|
||||||
|
const c = create(num3);
|
||||||
|
deepStrictEqual(Fp.mul(a, Fp.add(b, c)), Fp.add(Fp.mul(b, a), Fp.mul(c, a)));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('multiply/add equality', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (num) => {
|
||||||
|
const a = create(num);
|
||||||
|
deepStrictEqual(Fp.mul(a, 0n), Fp.ZERO);
|
||||||
|
deepStrictEqual(Fp.mul(a, Fp.ZERO), Fp.ZERO);
|
||||||
|
deepStrictEqual(Fp.mul(a, 1n), a);
|
||||||
|
deepStrictEqual(Fp.mul(a, Fp.ONE), a);
|
||||||
|
deepStrictEqual(Fp.mul(a, 2n), Fp.add(a, a));
|
||||||
|
deepStrictEqual(Fp.mul(a, 3n), Fp.add(Fp.add(a, a), a));
|
||||||
|
deepStrictEqual(Fp.mul(a, 4n), Fp.add(Fp.add(Fp.add(a, a), a), a));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('multiply/square equality', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (num) => {
|
||||||
|
const a = create(num);
|
||||||
|
deepStrictEqual(Fp.sqr(a), Fp.mul(a, a));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('multiply/pow equality', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (num) => {
|
||||||
|
const a = create(num);
|
||||||
|
deepStrictEqual(Fp.pow(a, 0n), Fp.ONE);
|
||||||
|
deepStrictEqual(Fp.pow(a, 1n), a);
|
||||||
|
deepStrictEqual(Fp.pow(a, 2n), Fp.mul(a, a));
|
||||||
|
deepStrictEqual(Fp.pow(a, 3n), Fp.mul(Fp.mul(a, a), a));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
should('square(0)', () => {
|
||||||
|
deepStrictEqual(Fp.sqr(Fp.ZERO), Fp.ZERO);
|
||||||
|
deepStrictEqual(Fp.mul(Fp.ZERO, Fp.ZERO), Fp.ZERO);
|
||||||
|
});
|
||||||
|
|
||||||
|
should('square(1)', () => {
|
||||||
|
deepStrictEqual(Fp.sqr(Fp.ONE), Fp.ONE);
|
||||||
|
deepStrictEqual(Fp.mul(Fp.ONE, Fp.ONE), Fp.ONE);
|
||||||
|
});
|
||||||
|
|
||||||
|
should('square(-1)', () => {
|
||||||
|
const minus1 = Fp.neg(Fp.ONE);
|
||||||
|
deepStrictEqual(Fp.sqr(minus1), Fp.ONE);
|
||||||
|
deepStrictEqual(Fp.mul(minus1, minus1), Fp.ONE);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSquare = mod.FpIsSquare(Fp);
|
||||||
|
// Not implemented
|
||||||
|
if (Fp !== bls12_381.CURVE.Fp12) {
|
||||||
|
should('multiply/sqrt', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (num) => {
|
||||||
|
const a = create(num);
|
||||||
|
let root;
|
||||||
|
try {
|
||||||
|
root = Fp.sqrt(a);
|
||||||
|
} catch (e) {
|
||||||
|
deepStrictEqual(isSquare(a), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deepStrictEqual(isSquare(a), true);
|
||||||
|
deepStrictEqual(Fp.eql(Fp.sqr(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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
should('sqrt(0)', () => {
|
||||||
|
deepStrictEqual(Fp.sqrt(Fp.ZERO), Fp.ZERO);
|
||||||
|
const sqrt1 = Fp.sqrt(Fp.ONE);
|
||||||
|
deepStrictEqual(
|
||||||
|
Fp.eql(sqrt1, Fp.ONE) || Fp.eql(sqrt1, Fp.neg(Fp.ONE)),
|
||||||
|
true,
|
||||||
|
'sqrt(1) = 1 or -1'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
should('div/division by one equality', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (num) => {
|
||||||
|
const a = create(num);
|
||||||
|
if (Fp.eql(a, Fp.ZERO)) return; // No division by zero
|
||||||
|
deepStrictEqual(Fp.div(a, Fp.ONE), a);
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('zero division equality', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (num) => {
|
||||||
|
const a = create(num);
|
||||||
|
deepStrictEqual(Fp.div(Fp.ZERO, a), Fp.ZERO);
|
||||||
|
deepStrictEqual(mod.FpDiv(Fp, Fp.ZERO, a), Fp.ZERO);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('div/division distributivity', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, FC_BIGINT, FC_BIGINT, (num1, num2, num3) => {
|
||||||
|
const a = create(num1);
|
||||||
|
const b = create(num2);
|
||||||
|
const c = create(num3);
|
||||||
|
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))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('div/division and multiplication equality', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, FC_BIGINT, (num1, num2) => {
|
||||||
|
const a = create(num1);
|
||||||
|
const b = create(num2);
|
||||||
|
deepStrictEqual(Fp.div(a, b), Fp.mul(a, Fp.inv(b)));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group tests
|
||||||
|
// prettier-ignore
|
||||||
|
const CURVES = {
|
||||||
|
secp192r1, secp224r1, secp256r1, secp384r1, secp521r1,
|
||||||
|
secp256k1,
|
||||||
|
ed25519, ed25519ctx, ed25519ph,
|
||||||
|
ed448, ed448ph,
|
||||||
|
pallas, vesta,
|
||||||
|
bn254,
|
||||||
|
jubjub,
|
||||||
|
};
|
||||||
|
|
||||||
|
const NUM_RUNS = 5;
|
||||||
|
|
||||||
|
const getXY = (p) => ({ x: p.x, y: p.y });
|
||||||
|
|
||||||
|
function equal(a, b, comment) {
|
||||||
|
deepStrictEqual(a.equals(b), true, `eq(${comment})`);
|
||||||
|
if (a.toAffine && b.toAffine) {
|
||||||
|
deepStrictEqual(getXY(a.toAffine()), getXY(b.toAffine()), `eqToAffine(${comment})`);
|
||||||
|
} else if (!a.toAffine && !b.toAffine) {
|
||||||
|
// Already affine
|
||||||
|
deepStrictEqual(getXY(a), getXY(b), `eqAffine(${comment})`);
|
||||||
|
} else throw new Error('Different point types');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name in CURVES) {
|
||||||
|
const C = CURVES[name];
|
||||||
|
const CURVE_ORDER = C.CURVE.n;
|
||||||
|
const FC_BIGINT = fc.bigInt(1n + 1n, CURVE_ORDER - 1n);
|
||||||
|
|
||||||
|
// Check that curve doesn't accept points from other curves
|
||||||
|
const O = name === 'secp256k1' ? secp256r1 : secp256k1;
|
||||||
|
const POINTS = {};
|
||||||
|
const OTHER_POINTS = {};
|
||||||
|
for (const name of ['Point', 'ProjectivePoint', 'ExtendedPoint', 'ProjectivePoint']) {
|
||||||
|
POINTS[name] = C[name];
|
||||||
|
OTHER_POINTS[name] = O[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pointName in POINTS) {
|
||||||
|
const p = POINTS[pointName];
|
||||||
|
const o = OTHER_POINTS[pointName];
|
||||||
|
if (!p) continue;
|
||||||
|
|
||||||
|
const G = [p.ZERO, p.BASE];
|
||||||
|
for (let i = 2n; i < 10n; i++) G.push(G[1].multiply(i));
|
||||||
|
const title = `${name}/${pointName}`;
|
||||||
|
describe(title, () => {
|
||||||
|
describe('basic group laws', () => {
|
||||||
|
// Here we check basic group laws, to verify that points works as group
|
||||||
|
should('zero', () => {
|
||||||
|
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].subtract(G[0]), G[0], '0*G - 0*G = 0');
|
||||||
|
equal(G[0].negate(), G[0], '-0 = 0');
|
||||||
|
for (let i = 0; i < G.length; i++) {
|
||||||
|
const p = G[i];
|
||||||
|
equal(p, p.add(G[0]), `${i}*G + 0 = ${i}*G`);
|
||||||
|
equal(G[0].multiply(BigInt(i + 1)), G[0], `${i + 1}*0 = 0`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should('one', () => {
|
||||||
|
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].add(G[1]), G[2], '1*G + 1*G = 2*G');
|
||||||
|
});
|
||||||
|
should('sanity tests', () => {
|
||||||
|
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[7].add(G[3].negate()), G[4], '7*G - 3*G = 4*G');
|
||||||
|
});
|
||||||
|
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[2]).add(G[2]), '4*G + 3*G = 3*G + 2*G + 2*G');
|
||||||
|
});
|
||||||
|
should('double', () => {
|
||||||
|
equal(G[3].double(), G[6], '(3*G).double() = 6*G');
|
||||||
|
});
|
||||||
|
should('multiply', () => {
|
||||||
|
equal(G[2].multiply(3n), G[6], '(2*G).multiply(3) = 6*G');
|
||||||
|
});
|
||||||
|
should('add same-point', () => {
|
||||||
|
equal(G[3].add(G[3]), G[6], '3*G + 3*G = 6*G');
|
||||||
|
});
|
||||||
|
should('add same-point negative', () => {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
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[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');
|
||||||
|
const half = CURVE_ORDER / 2n;
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
should('inversion', () => {
|
||||||
|
const a = 1234n;
|
||||||
|
const b = 5678n;
|
||||||
|
const c = a * b;
|
||||||
|
equal(G[1].multiply(a).multiply(b), G[1].multiply(c), 'a*b*G = c*G');
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
should('multiply, rand', () =>
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, FC_BIGINT, (a, b) => {
|
||||||
|
const c = mod.mod(a + b, CURVE_ORDER);
|
||||||
|
if (c === CURVE_ORDER || c < 1n) return;
|
||||||
|
const pA = G[1].multiply(a);
|
||||||
|
const pB = G[1].multiply(b);
|
||||||
|
const pC = G[1].multiply(c);
|
||||||
|
equal(pA.add(pB), pB.add(pA), 'pA + pB = pB + pA');
|
||||||
|
equal(pA.add(pB), pC, 'pA + pB = pC');
|
||||||
|
}),
|
||||||
|
{ numRuns: NUM_RUNS }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
should('multiply2, rand', () =>
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, FC_BIGINT, (a, b) => {
|
||||||
|
const c = mod.mod(a * b, CURVE_ORDER);
|
||||||
|
const pA = G[1].multiply(a);
|
||||||
|
const pB = G[1].multiply(b);
|
||||||
|
equal(pA.multiply(b), pB.multiply(a), 'b*pA = a*pB');
|
||||||
|
equal(pA.multiply(b), G[1].multiply(c), 'b*pA = c*G');
|
||||||
|
}),
|
||||||
|
{ numRuns: NUM_RUNS }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const op of ['add', 'subtract']) {
|
||||||
|
describe(op, () => {
|
||||||
|
should('type check', () => {
|
||||||
|
throws(() => G[1][op](0), '0');
|
||||||
|
throws(() => G[1][op](0n), '0n');
|
||||||
|
G[1][op](G[2]);
|
||||||
|
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](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]({ 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(
|
||||||
|
() => G[1][op]({ x: 1n, y: 1n, z: 1n, t: 1n }),
|
||||||
|
'{ x: 1n, y: 1n, z: 1n, t: 1n }'
|
||||||
|
);
|
||||||
|
throws(() => G[1][op](new Uint8Array([])), 'ui8a([])');
|
||||||
|
throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])');
|
||||||
|
throws(() => G[1][op](new Uint8Array([1])), 'ui8a([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}`);
|
||||||
|
throws(() => G[1][op](o.BASE), `${op}/other curve point`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
should('equals type check', () => {
|
||||||
|
throws(() => G[1].equals(0), '0');
|
||||||
|
throws(() => G[1].equals(0n), '0n');
|
||||||
|
deepStrictEqual(G[1].equals(G[2]), false, '1*G != 2*G');
|
||||||
|
deepStrictEqual(G[1].equals(G[1]), true, '1*G == 1*G');
|
||||||
|
deepStrictEqual(G[2].equals(G[2]), true, '2*G == 2*G');
|
||||||
|
throws(() => G[1].equals(CURVE_ORDER), 'CURVE_ORDER');
|
||||||
|
throws(() => G[1].equals(123.456), '123.456');
|
||||||
|
throws(() => G[1].equals(true), 'true');
|
||||||
|
throws(() => G[1].equals('1'), "'1'");
|
||||||
|
throws(() => G[1].equals({ x: 1n, y: 1n, z: 1n, t: 1n }), '{ x: 1n, y: 1n, z: 1n, t: 1n }');
|
||||||
|
throws(() => G[1].equals(new Uint8Array([])), 'ui8a([])');
|
||||||
|
throws(() => G[1].equals(new Uint8Array([0])), 'ui8a([0])');
|
||||||
|
throws(() => G[1].equals(new Uint8Array([1])), 'ui8a([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})');
|
||||||
|
throws(() => G[1].equals(o.BASE), 'other curve point');
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const op of ['multiply', 'multiplyUnsafe']) {
|
||||||
|
if (!p.BASE[op]) continue;
|
||||||
|
describe(op, () => {
|
||||||
|
should('type check', () => {
|
||||||
|
if (op !== 'multiplyUnsafe') {
|
||||||
|
throws(() => G[1][op](0), '0');
|
||||||
|
throws(() => G[1][op](0n), '0n');
|
||||||
|
}
|
||||||
|
G[1][op](1n);
|
||||||
|
G[1][op](CURVE_ORDER - 1n);
|
||||||
|
throws(() => G[1][op](G[2]), 'G[2]');
|
||||||
|
throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER');
|
||||||
|
throws(() => G[1][op](CURVE_ORDER + 1n), 'CURVE_ORDER+1');
|
||||||
|
throws(() => G[1][op](123.456), '123.456');
|
||||||
|
throws(() => G[1][op](true), 'true');
|
||||||
|
throws(() => G[1][op]('1'), '1');
|
||||||
|
throws(() => G[1][op](new Uint8Array([])), 'ui8a([])');
|
||||||
|
throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])');
|
||||||
|
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](o.BASE), 'other curve point');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Complex point (Extended/Jacobian/Projective?)
|
||||||
|
// if (p.BASE.toAffine && C.Point) {
|
||||||
|
// should('toAffine()', () => {
|
||||||
|
// equal(p.ZERO.toAffine(), C.Point.ZERO, '0 = 0');
|
||||||
|
// equal(p.BASE.toAffine(), C.Point.BASE, '1 = 1');
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// if (p.fromAffine && C.Point) {
|
||||||
|
// should('fromAffine()', () => {
|
||||||
|
// equal(p.ZERO, p.fromAffine(C.Point.ZERO), '0 = 0');
|
||||||
|
// equal(p.BASE, p.fromAffine(C.Point.BASE), '1 = 1');
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// toHex/fromHex (if available)
|
||||||
|
if (p.fromHex && p.BASE.toHex) {
|
||||||
|
should('fromHex(toHex()) roundtrip', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (x) => {
|
||||||
|
const point = p.BASE.multiply(x);
|
||||||
|
const hex = point.toHex();
|
||||||
|
const bytes = point.toRawBytes();
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
describe(name, () => {
|
||||||
|
if (['bn254', 'pallas', 'vesta'].includes(name)) return;
|
||||||
|
// Generic complex things (getPublicKey/sign/verify/getSharedSecret)
|
||||||
|
should('.getPublicKey() type check', () => {
|
||||||
|
throws(() => C.getPublicKey(0), '0');
|
||||||
|
throws(() => C.getPublicKey(0n), '0n');
|
||||||
|
throws(() => C.getPublicKey(-123n), '-123n');
|
||||||
|
throws(() => C.getPublicKey(123), '123');
|
||||||
|
throws(() => C.getPublicKey(123.456), '123.456');
|
||||||
|
throws(() => C.getPublicKey(true), 'true');
|
||||||
|
throws(() => C.getPublicKey(false), 'false');
|
||||||
|
throws(() => C.getPublicKey(null), 'null');
|
||||||
|
throws(() => C.getPublicKey(undefined), 'undefined');
|
||||||
|
throws(() => C.getPublicKey(''), "''");
|
||||||
|
// NOTE: passes because of disabled hex padding checks for starknet, maybe enable?
|
||||||
|
// throws(() => C.getPublicKey('1'), "'1'");
|
||||||
|
throws(() => C.getPublicKey('key'), "'key'");
|
||||||
|
throws(() => C.getPublicKey({}));
|
||||||
|
throws(() => C.getPublicKey(new Uint8Array([])));
|
||||||
|
throws(() => C.getPublicKey(new Uint8Array([0])));
|
||||||
|
throws(() => C.getPublicKey(new Uint8Array([1])));
|
||||||
|
throws(() => C.getPublicKey(new Uint8Array(4096).fill(1)));
|
||||||
|
throws(() => C.getPublicKey(Array(32).fill(1)));
|
||||||
|
});
|
||||||
|
should('.verify() should verify random signatures', () =>
|
||||||
|
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(
|
||||||
|
C.verify(sig, msg, pub),
|
||||||
|
true,
|
||||||
|
`priv=${toHex(priv)},pub=${toHex(pub)},msg=${msg}`
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
{ 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', () => {
|
||||||
|
throws(() => C.sign());
|
||||||
|
throws(() => C.sign(''));
|
||||||
|
throws(() => C.sign('', ''));
|
||||||
|
throws(() => C.sign(new Uint8Array(), new Uint8Array()));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('verify()', () => {
|
||||||
|
const msg = '01'.repeat(32);
|
||||||
|
should('true for proper signatures', () => {
|
||||||
|
const priv = C.utils.randomPrivateKey();
|
||||||
|
const sig = C.sign(msg, priv);
|
||||||
|
const pub = C.getPublicKey(priv);
|
||||||
|
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,
|
||||||
|
// 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', () => {
|
||||||
|
// fc.assert(
|
||||||
|
// fc.property(
|
||||||
|
// fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
||||||
|
// fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
||||||
|
// (bytes, wrongBytes) => {
|
||||||
|
// const privKey = C.utils.randomPrivateKey();
|
||||||
|
// const message = new Uint8Array(bytes);
|
||||||
|
// const wrongMessage = new Uint8Array(wrongBytes);
|
||||||
|
// const publicKey = C.getPublicKey(privKey);
|
||||||
|
// const signature = C.sign(message, privKey);
|
||||||
|
// deepStrictEqual(
|
||||||
|
// C.verify(signature, wrongMessage, publicKey),
|
||||||
|
// bytes.toString() === wrongBytes.toString()
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// ),
|
||||||
|
// { numRuns: NUM_RUNS }
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
|
||||||
|
if (C.getSharedSecret) {
|
||||||
|
should('getSharedSecret() should be commutative', () => {
|
||||||
|
for (let i = 0; i < NUM_RUNS; i++) {
|
||||||
|
const asec = C.utils.randomPrivateKey();
|
||||||
|
const apub = C.getPublicKey(asec);
|
||||||
|
const bsec = C.utils.randomPrivateKey();
|
||||||
|
const bpub = C.getPublicKey(bsec);
|
||||||
|
try {
|
||||||
|
deepStrictEqual(C.getSharedSecret(asec, bpub), C.getSharedSecret(bsec, apub));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('not commutative', { asec, apub, bsec, bpub });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
should('secp224k1 sqrt bug', () => {
|
||||||
|
const { Fp } = secp224r1.CURVE;
|
||||||
|
const sqrtMinus1 = Fp.sqrt(-1n);
|
||||||
|
// Verified against sage
|
||||||
|
deepStrictEqual(
|
||||||
|
sqrtMinus1,
|
||||||
|
23621584063597419797792593680131996961517196803742576047493035507225n
|
||||||
|
);
|
||||||
|
deepStrictEqual(
|
||||||
|
Fp.neg(sqrtMinus1),
|
||||||
|
3338362603553219996874421406887633712040719456283732096017030791656n
|
||||||
|
);
|
||||||
|
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.
|
||||||
|
import url from 'url';
|
||||||
|
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
||||||
|
should.run();
|
||||||
|
}
|
||||||
1371
test/bls12-381.test.js
Normal file
1371
test/bls12-381.test.js
Normal file
File diff suppressed because it is too large
Load Diff
303
test/ed25519-addons.test.js
Normal file
303
test/ed25519-addons.test.js
Normal 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
1
test/ed25519.helpers.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ed25519, ED25519_TORSION_SUBGROUP } from '../esm/ed25519.js';
|
||||||
405
test/ed25519.test.js
Normal file
405
test/ed25519.test.js
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
import { deepStrictEqual, strictEqual, throws } from 'assert';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
||||||
|
import * as fc from 'fast-check';
|
||||||
|
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 zip215 } from './ed25519/zip215.json' assert { type: 'json' };
|
||||||
|
|
||||||
|
describe('ed25519', () => {
|
||||||
|
const ed = ed25519;
|
||||||
|
const hex = bytesToHex;
|
||||||
|
const Point = ed.ExtendedPoint;
|
||||||
|
|
||||||
|
function to32Bytes(numOrStr) {
|
||||||
|
let hex = typeof numOrStr === 'string' ? numOrStr : numOrStr.toString(16);
|
||||||
|
return hexToBytes(hex.padStart(64, '0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function utf8ToBytes(str) {
|
||||||
|
if (typeof str !== 'string') {
|
||||||
|
throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
|
||||||
|
}
|
||||||
|
return new TextEncoder().encode(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
ed.utils.precompute(8);
|
||||||
|
|
||||||
|
should('not accept >32byte private keys', () => {
|
||||||
|
const invalidPriv =
|
||||||
|
100000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800073278156000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000n;
|
||||||
|
throws(() => ed.getPublicKey(invalidPriv));
|
||||||
|
});
|
||||||
|
should('verify recent signature', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(
|
||||||
|
fc.hexaString({ minLength: 2, maxLength: 32 }),
|
||||||
|
fc.bigInt(2n, ed.CURVE.n),
|
||||||
|
(message, privateKey) => {
|
||||||
|
const publicKey = ed.getPublicKey(to32Bytes(privateKey));
|
||||||
|
const signature = ed.sign(to32Bytes(message), to32Bytes(privateKey));
|
||||||
|
deepStrictEqual(publicKey.length, 32);
|
||||||
|
deepStrictEqual(signature.length, 64);
|
||||||
|
deepStrictEqual(ed.verify(signature, to32Bytes(message), publicKey), true);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{ numRuns: 5 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('not verify signature with wrong message', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(
|
||||||
|
fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
||||||
|
fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
||||||
|
fc.bigInt(1n, ed.CURVE.n),
|
||||||
|
(bytes, wrongBytes, privateKey) => {
|
||||||
|
const privKey = to32Bytes(privateKey);
|
||||||
|
const message = new Uint8Array(bytes);
|
||||||
|
const wrongMessage = new Uint8Array(wrongBytes);
|
||||||
|
const publicKey = ed.getPublicKey(privKey);
|
||||||
|
const signature = ed.sign(message, privKey);
|
||||||
|
deepStrictEqual(
|
||||||
|
ed.verify(signature, wrongMessage, publicKey),
|
||||||
|
bytes.toString() === wrongBytes.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{ numRuns: 5 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const privKey = to32Bytes('a665a45920422f9d417e4867ef');
|
||||||
|
const wrongPriv = to32Bytes('a675a45920422f9d417e4867ef');
|
||||||
|
const msg = hexToBytes('874f9960c5d2b7a9b5fad383e1ba44719ebb743a');
|
||||||
|
const wrongMsg = hexToBytes('589d8c7f1da0a24bc07b7381ad48b1cfc211af1c');
|
||||||
|
describe('basic methods', () => {
|
||||||
|
should('sign and verify', () => {
|
||||||
|
const publicKey = ed.getPublicKey(privKey);
|
||||||
|
const signature = ed.sign(msg, privKey);
|
||||||
|
deepStrictEqual(ed.verify(signature, msg, publicKey), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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(wrongPriv);
|
||||||
|
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('BASE_POINT.multiply()', () => {
|
||||||
|
// https://xmr.llcoins.net/addresstests.html
|
||||||
|
should('create right publicKey without SHA-512 hashing TEST 1', () => {
|
||||||
|
const publicKey =
|
||||||
|
Point.BASE.multiply(0x90af56259a4b6bfbc4337980d5d75fbe3c074630368ff3804d33028e5dbfa77n);
|
||||||
|
deepStrictEqual(
|
||||||
|
publicKey.toHex(),
|
||||||
|
'0f3b913371411b27e646b537e888f685bf929ea7aab93c950ed84433f064480d'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('create right publicKey without SHA-512 hashing TEST 2', () => {
|
||||||
|
const publicKey =
|
||||||
|
Point.BASE.multiply(0x364e8711a60780382a5d57b061c126f039940f28a9e91fe039d4d3094d8b88n);
|
||||||
|
deepStrictEqual(
|
||||||
|
publicKey.toHex(),
|
||||||
|
'ad545340b58610f0cd62f17d55af1ab11ecde9c084d5476865ddb4dbda015349'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('create right publicKey without SHA-512 hashing TEST 3', () => {
|
||||||
|
const publicKey =
|
||||||
|
Point.BASE.multiply(0xb9bf90ff3abec042752cac3a07a62f0c16cfb9d32a3fc2305d676ec2d86e941n);
|
||||||
|
deepStrictEqual(
|
||||||
|
publicKey.toHex(),
|
||||||
|
'e097c4415fe85724d522b2e449e8fd78dd40d20097bdc9ae36fe8ec6fe12cb8c'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('create right publicKey without SHA-512 hashing TEST 4', () => {
|
||||||
|
const publicKey =
|
||||||
|
Point.BASE.multiply(0x69d896f02d79524c9878e080308180e2859d07f9f54454e0800e8db0847a46en);
|
||||||
|
deepStrictEqual(
|
||||||
|
publicKey.toHex(),
|
||||||
|
'f12cb7c43b59971395926f278ce7c2eaded9444fbce62ca717564cb508a0db1d'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('throw Point#multiply on TEST 5', () => {
|
||||||
|
for (const num of [0n, 0, -1n, -1, 1.1]) {
|
||||||
|
throws(() => Point.BASE.multiply(num));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://ed25519.cr.yp.to/python/sign.py
|
||||||
|
// https://ed25519.cr.yp.to/python/sign.input
|
||||||
|
const data = readFileSync('./test/ed25519/vectors.txt', 'utf-8');
|
||||||
|
const vectors = data
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.split(':'));
|
||||||
|
should('ed25519 official vectors/should match 1024 official vectors', () => {
|
||||||
|
for (let i = 0; i < vectors.length; i++) {
|
||||||
|
const vector = vectors[i];
|
||||||
|
// Extract.
|
||||||
|
const priv = vector[0].slice(0, 64);
|
||||||
|
const expectedPub = vector[1];
|
||||||
|
const msg = vector[2];
|
||||||
|
const expectedSignature = vector[3].slice(0, 128);
|
||||||
|
|
||||||
|
// Calculate
|
||||||
|
const pub = ed.getPublicKey(to32Bytes(priv));
|
||||||
|
deepStrictEqual(hex(pub), expectedPub);
|
||||||
|
deepStrictEqual(pub, Point.fromHex(pub).toRawBytes());
|
||||||
|
|
||||||
|
const signature = hex(ed.sign(msg, priv));
|
||||||
|
// console.log('vector', i);
|
||||||
|
// expect(pub).toBe(expectedPub);
|
||||||
|
deepStrictEqual(signature, expectedSignature);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc8032#section-7
|
||||||
|
should('rfc8032 vectors/should create right signature for 0x9d and empty string', () => {
|
||||||
|
const privateKey = '9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60';
|
||||||
|
const publicKey = ed.getPublicKey(privateKey);
|
||||||
|
const message = '';
|
||||||
|
const signature = ed.sign(message, privateKey);
|
||||||
|
deepStrictEqual(
|
||||||
|
hex(publicKey),
|
||||||
|
'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'
|
||||||
|
);
|
||||||
|
deepStrictEqual(
|
||||||
|
hex(signature),
|
||||||
|
'e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('rfc8032 vectors/should create right signature for 0x4c and 72', () => {
|
||||||
|
const privateKey = '4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb';
|
||||||
|
const publicKey = ed.getPublicKey(privateKey);
|
||||||
|
const message = '72';
|
||||||
|
const signature = ed.sign(message, privateKey);
|
||||||
|
deepStrictEqual(
|
||||||
|
hex(publicKey),
|
||||||
|
'3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c'
|
||||||
|
);
|
||||||
|
deepStrictEqual(
|
||||||
|
hex(signature),
|
||||||
|
'92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('rfc8032 vectors/should create right signature for 0x00 and 5a', () => {
|
||||||
|
const privateKey = '002fdd1f7641793ab064bb7aa848f762e7ec6e332ffc26eeacda141ae33b1783';
|
||||||
|
const publicKey = ed.getPublicKey(privateKey);
|
||||||
|
const message =
|
||||||
|
'5ac1dfc324f43e6cb79a87ab0470fa857b51fb944982e19074ca44b1e40082c1d07b92efa7ea55ad42b7c027e0b9e33756d95a2c1796a7c2066811dc41858377d4b835c1688d638884cd2ad8970b74c1a54aadd27064163928a77988b24403aa85af82ceab6b728e554761af7175aeb99215b7421e4474c04d213e01ff03e3529b11077cdf28964b8c49c5649e3a46fa0a09dcd59dcad58b9b922a83210acd5e65065531400234f5e40cddcf9804968e3e9ac6f5c44af65001e158067fc3a660502d13fa8874fa93332138d9606bc41b4cee7edc39d753dae12a873941bb357f7e92a4498847d6605456cb8c0b425a47d7d3ca37e54e903a41e6450a35ebe5237c6f0c1bbbc1fd71fb7cd893d189850295c199b7d88af26bc8548975fda1099ffefee42a52f3428ddff35e0173d3339562507ac5d2c45bbd2c19cfe89b';
|
||||||
|
const signature = ed.sign(message, privateKey);
|
||||||
|
deepStrictEqual(
|
||||||
|
hex(publicKey),
|
||||||
|
'77d1d8ebacd13f4e2f8a40e28c4a63bc9ce3bfb69716334bcb28a33eb134086c'
|
||||||
|
);
|
||||||
|
deepStrictEqual(
|
||||||
|
hex(signature),
|
||||||
|
'0df3aa0d0999ad3dc580378f52d152700d5b3b057f56a66f92112e441e1cb9123c66f18712c87efe22d2573777296241216904d7cdd7d5ea433928bd2872fa0c'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('rfc8032 vectors/should create right signature for 0xf5 and long msg', () => {
|
||||||
|
const privateKey = 'f5e5767cf153319517630f226876b86c8160cc583bc013744c6bf255f5cc0ee5';
|
||||||
|
const publicKey = ed.getPublicKey(privateKey);
|
||||||
|
const message =
|
||||||
|
'08b8b2b733424243760fe426a4b54908632110a66c2f6591eabd3345e3e4eb98fa6e264bf09efe12ee50f8f54e9f77b1e355f6c50544e23fb1433ddf73be84d879de7c0046dc4996d9e773f4bc9efe5738829adb26c81b37c93a1b270b20329d658675fc6ea534e0810a4432826bf58c941efb65d57a338bbd2e26640f89ffbc1a858efcb8550ee3a5e1998bd177e93a7363c344fe6b199ee5d02e82d522c4feba15452f80288a821a579116ec6dad2b3b310da903401aa62100ab5d1a36553e06203b33890cc9b832f79ef80560ccb9a39ce767967ed628c6ad573cb116dbefefd75499da96bd68a8a97b928a8bbc103b6621fcde2beca1231d206be6cd9ec7aff6f6c94fcd7204ed3455c68c83f4a41da4af2b74ef5c53f1d8ac70bdcb7ed185ce81bd84359d44254d95629e9855a94a7c1958d1f8ada5d0532ed8a5aa3fb2d17ba70eb6248e594e1a2297acbbb39d502f1a8c6eb6f1ce22b3de1a1f40cc24554119a831a9aad6079cad88425de6bde1a9187ebb6092cf67bf2b13fd65f27088d78b7e883c8759d2c4f5c65adb7553878ad575f9fad878e80a0c9ba63bcbcc2732e69485bbc9c90bfbd62481d9089beccf80cfe2df16a2cf65bd92dd597b0707e0917af48bbb75fed413d238f5555a7a569d80c3414a8d0859dc65a46128bab27af87a71314f318c782b23ebfe808b82b0ce26401d2e22f04d83d1255dc51addd3b75a2b1ae0784504df543af8969be3ea7082ff7fc9888c144da2af58429ec96031dbcad3dad9af0dcbaaaf268cb8fcffead94f3c7ca495e056a9b47acdb751fb73e666c6c655ade8297297d07ad1ba5e43f1bca32301651339e22904cc8c42f58c30c04aafdb038dda0847dd988dcda6f3bfd15c4b4c4525004aa06eeff8ca61783aacec57fb3d1f92b0fe2fd1a85f6724517b65e614ad6808d6f6ee34dff7310fdc82aebfd904b01e1dc54b2927094b2db68d6f903b68401adebf5a7e08d78ff4ef5d63653a65040cf9bfd4aca7984a74d37145986780fc0b16ac451649de6188a7dbdf191f64b5fc5e2ab47b57f7f7276cd419c17a3ca8e1b939ae49e488acba6b965610b5480109c8b17b80e1b7b750dfc7598d5d5011fd2dcc5600a32ef5b52a1ecc820e308aa342721aac0943bf6686b64b2579376504ccc493d97e6aed3fb0f9cd71a43dd497f01f17c0e2cb3797aa2a2f256656168e6c496afc5fb93246f6b1116398a346f1a641f3b041e989f7914f90cc2c7fff357876e506b50d334ba77c225bc307ba537152f3f1610e4eafe595f6d9d90d11faa933a15ef1369546868a7f3a45a96768d40fd9d03412c091c6315cf4fde7cb68606937380db2eaaa707b4c4185c32eddcdd306705e4dc1ffc872eeee475a64dfac86aba41c0618983f8741c5ef68d3a101e8a3b8cac60c905c15fc910840b94c00a0b9d0';
|
||||||
|
const signature = ed.sign(message, privateKey);
|
||||||
|
deepStrictEqual(
|
||||||
|
hex(publicKey),
|
||||||
|
'278117fc144c72340f67d0f2316e8386ceffbf2b2428c9c51fef7c597f1d426e'
|
||||||
|
);
|
||||||
|
deepStrictEqual(
|
||||||
|
hex(signature),
|
||||||
|
'0aab4c900501b3e24d7cdf4663326a3a87df5e4843b2cbdb67cbf6e460fec350aa5371b1508f9f4528ecea23c436d94b5e8fcd4f681e30a6ac00a9704a188a03'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// const PRIVATE_KEY = 0xa665a45920422f9d417e4867efn;
|
||||||
|
// const MESSAGE = ripemd160(new Uint8Array([97, 98, 99, 100, 101, 102, 103]));
|
||||||
|
// prettier-ignore
|
||||||
|
// const MESSAGE = new Uint8Array([
|
||||||
|
// 135, 79, 153, 96, 197, 210, 183, 169, 181, 250, 211, 131, 225, 186, 68, 113, 158, 187, 116, 58,
|
||||||
|
// ]);
|
||||||
|
// const WRONG_MESSAGE = ripemd160(new Uint8Array([98, 99, 100, 101, 102, 103]));
|
||||||
|
// prettier-ignore
|
||||||
|
// const WRONG_MESSAGE = new Uint8Array([
|
||||||
|
// 88, 157, 140, 127, 29, 160, 162, 75, 192, 123, 115, 129, 173, 72, 177, 207, 194, 17, 175, 28,
|
||||||
|
// ]);
|
||||||
|
// // it("should verify just signed message", async () => {
|
||||||
|
// // await fc.assert(fc.asyncProperty(
|
||||||
|
// // fc.hexa(),
|
||||||
|
// // fc.bigInt(2n, ristretto25519.PRIME_ORDER),
|
||||||
|
// // async (message, privateKey) => {
|
||||||
|
// // const publicKey = await ristretto25519.getPublicKey(privateKey);
|
||||||
|
// // const signature = await ristretto25519.sign(message, privateKey);
|
||||||
|
// // expect(publicKey.length).toBe(32);
|
||||||
|
// // expect(signature.length).toBe(64);
|
||||||
|
// // expect(await ristretto25519.verify(signature, message, publicKey)).toBe(true);
|
||||||
|
// // }),
|
||||||
|
// // { numRuns: 1 }
|
||||||
|
// // );
|
||||||
|
// // });
|
||||||
|
// // it("should not verify sign with wrong message", async () => {
|
||||||
|
// // await fc.assert(fc.asyncProperty(
|
||||||
|
// // fc.array(fc.integer(0x00, 0xff)),
|
||||||
|
// // fc.array(fc.integer(0x00, 0xff)),
|
||||||
|
// // fc.bigInt(2n, ristretto25519.PRIME_ORDER),
|
||||||
|
// // async (bytes, wrongBytes, privateKey) => {
|
||||||
|
// // const message = new Uint8Array(bytes);
|
||||||
|
// // const wrongMessage = new Uint8Array(wrongBytes);
|
||||||
|
// // const publicKey = await ristretto25519.getPublicKey(privateKey);
|
||||||
|
// // const signature = await ristretto25519.sign(message, privateKey);
|
||||||
|
// // expect(await ristretto25519.verify(signature, wrongMessage, publicKey)).toBe(
|
||||||
|
// // bytes.toString() === wrongBytes.toString()
|
||||||
|
// // );
|
||||||
|
// // }),
|
||||||
|
// // { numRuns: 1 }
|
||||||
|
// // );
|
||||||
|
// // });
|
||||||
|
// // it("should sign and verify", async () => {
|
||||||
|
// // const publicKey = await ristretto25519.getPublicKey(PRIVATE_KEY);
|
||||||
|
// // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY);
|
||||||
|
// // expect(await ristretto25519.verify(signature, MESSAGE, publicKey)).toBe(true);
|
||||||
|
// // });
|
||||||
|
// // it("should not verify signature with wrong public key", async () => {
|
||||||
|
// // const publicKey = await ristretto25519.getPublicKey(12);
|
||||||
|
// // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY);
|
||||||
|
// // expect(await ristretto25519.verify(signature, MESSAGE, publicKey)).toBe(false);
|
||||||
|
// // });
|
||||||
|
// // it("should not verify signature with wrong hash", async () => {
|
||||||
|
// // const publicKey = await ristretto25519.getPublicKey(PRIVATE_KEY);
|
||||||
|
// // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY);
|
||||||
|
// // expect(await ristretto25519.verify(signature, WRONG_MESSAGE, publicKey)).toBe(false);
|
||||||
|
// // });
|
||||||
|
|
||||||
|
should('input immutability: sign/verify are immutable', () => {
|
||||||
|
const privateKey = ed.utils.randomPrivateKey();
|
||||||
|
const publicKey = ed.getPublicKey(privateKey);
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
let payload = randomBytes(100);
|
||||||
|
let signature = ed.sign(payload, privateKey);
|
||||||
|
if (!ed.verify(signature, payload, publicKey)) {
|
||||||
|
throw new Error('Signature verification failed');
|
||||||
|
}
|
||||||
|
const signatureCopy = Buffer.alloc(signature.byteLength);
|
||||||
|
signatureCopy.set(signature, 0); // <-- breaks
|
||||||
|
payload = payload.slice();
|
||||||
|
signature = signature.slice();
|
||||||
|
|
||||||
|
if (!ed.verify(signatureCopy, payload, publicKey))
|
||||||
|
throw new Error('Copied signature verification failed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://zips.z.cash/zip-0215
|
||||||
|
// Vectors from https://gist.github.com/hdevalence/93ed42d17ecab8e42138b213812c8cc7
|
||||||
|
should('ZIP-215 compliance tests/should pass all of them', () => {
|
||||||
|
const str = utf8ToBytes('Zcash');
|
||||||
|
for (let v of zip215) {
|
||||||
|
let noble = false;
|
||||||
|
try {
|
||||||
|
noble = ed.verify(v.sig_bytes, str, v.vk_bytes);
|
||||||
|
} catch (e) {
|
||||||
|
noble = false;
|
||||||
|
}
|
||||||
|
deepStrictEqual(noble, v.valid_zip215, JSON.stringify(v));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should('ZIP-215 compliance tests/disallows sig.s >= CURVE.n', () => {
|
||||||
|
// sig.R = BASE, sig.s = N+1
|
||||||
|
const sig =
|
||||||
|
'5866666666666666666666666666666666666666666666666666666666666666eed3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010';
|
||||||
|
throws(() => ed.verify(sig, 'deadbeef', Point.BASE));
|
||||||
|
});
|
||||||
|
|
||||||
|
// should('X25519/getSharedSecret() should be commutative', () => {
|
||||||
|
// for (let i = 0; i < 512; i++) {
|
||||||
|
// const asec = ed.utils.randomPrivateKey();
|
||||||
|
// const apub = ed.getPublicKey(asec);
|
||||||
|
// const bsec = ed.utils.randomPrivateKey();
|
||||||
|
// const bpub = ed.getPublicKey(bsec);
|
||||||
|
// try {
|
||||||
|
// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('not commutative', { asec, apub, bsec, bpub });
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// should('X25519: should convert base point to montgomery using fromPoint', () => {
|
||||||
|
// deepStrictEqual(
|
||||||
|
// hex(ed.montgomeryCurve.UfromPoint(Point.BASE)),
|
||||||
|
// ed.montgomeryCurve.BASE_POINT_U
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
|
||||||
|
should(`Wycheproof/ED25519`, () => {
|
||||||
|
for (let g = 0; g < ed25519vectors.testGroups.length; g++) {
|
||||||
|
const group = ed25519vectors.testGroups[g];
|
||||||
|
const key = group.key;
|
||||||
|
deepStrictEqual(hex(ed.getPublicKey(key.sk)), key.pk, `(${g}, public)`);
|
||||||
|
for (let i = 0; i < group.tests.length; i++) {
|
||||||
|
const v = group.tests[i];
|
||||||
|
const comment = `(${g}/${i}, ${v.result}): ${v.comment}`;
|
||||||
|
if (v.result === 'valid' || v.result === 'acceptable') {
|
||||||
|
deepStrictEqual(hex(ed.sign(v.msg, key.sk)), v.sig, comment);
|
||||||
|
deepStrictEqual(ed.verify(v.sig, v.msg, key.pk), true, comment);
|
||||||
|
} else if (v.result === 'invalid') {
|
||||||
|
let failed = false;
|
||||||
|
try {
|
||||||
|
failed = !ed.verify(v.sig, v.msg, key.pk);
|
||||||
|
} catch (error) {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
deepStrictEqual(failed, true, comment);
|
||||||
|
} else throw new Error('unknown test result');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
should('Property test issue #1', () => {
|
||||||
|
const message = new Uint8Array([12, 12, 12]);
|
||||||
|
const signature = ed.sign(message, to32Bytes(1n));
|
||||||
|
const publicKey = ed.getPublicKey(to32Bytes(1n)); // <- was 1n
|
||||||
|
deepStrictEqual(ed.verify(signature, message, publicKey), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
should('isTorsionFree()', () => {
|
||||||
|
const orig = ed.utils.getExtendedPublicKey(ed.utils.randomPrivateKey()).point;
|
||||||
|
for (const hex of ED25519_TORSION_SUBGROUP.slice(1)) {
|
||||||
|
const dirty = orig.add(Point.fromHex(hex));
|
||||||
|
const cleared = dirty.clearCofactor();
|
||||||
|
strictEqual(orig.isTorsionFree(), true, `orig must be torsionFree: ${hex}`);
|
||||||
|
strictEqual(dirty.isTorsionFree(), false, `dirty must not be torsionFree: ${hex}`);
|
||||||
|
strictEqual(cleared.isTorsionFree(), true, `cleared must be torsionFree: ${hex}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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.
|
||||||
|
import url from 'url';
|
||||||
|
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
||||||
|
should.run();
|
||||||
|
}
|
||||||
675
test/ed448.test.js
Normal file
675
test/ed448.test.js
Normal file
@@ -0,0 +1,675 @@
|
|||||||
|
import { deepStrictEqual, throws } from 'assert';
|
||||||
|
import { describe, should } from 'micro-should';
|
||||||
|
import * as fc from 'fast-check';
|
||||||
|
import { ed448, ed448ph, x448 } from '../esm/ed448.js';
|
||||||
|
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
||||||
|
import { numberToBytesLE } from '../esm/abstract/utils.js';
|
||||||
|
import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' };
|
||||||
|
import { default as x448vectors } from './wycheproof/x448_test.json' assert { type: 'json' };
|
||||||
|
|
||||||
|
describe('ed448', () => {
|
||||||
|
const ed = ed448;
|
||||||
|
const hex = bytesToHex;
|
||||||
|
ed.utils.precompute(4);
|
||||||
|
const Point = ed.ExtendedPoint;
|
||||||
|
|
||||||
|
should(`Basic`, () => {
|
||||||
|
const G1 = Point.BASE.toAffine();
|
||||||
|
deepStrictEqual(
|
||||||
|
G1.x,
|
||||||
|
224580040295924300187604334099896036246789641632564134246125461686950415467406032909029192869357953282578032075146446173674602635247710n
|
||||||
|
);
|
||||||
|
deepStrictEqual(
|
||||||
|
G1.y,
|
||||||
|
298819210078481492676017930443930673437544040154080242095928241372331506189835876003536878655418784733982303233503462500531545062832660n
|
||||||
|
);
|
||||||
|
const G2 = Point.BASE.multiply(2n).toAffine();
|
||||||
|
deepStrictEqual(
|
||||||
|
G2.x,
|
||||||
|
484559149530404593699549205258669689569094240458212040187660132787056912146709081364401144455726350866276831544947397859048262938744149n
|
||||||
|
);
|
||||||
|
deepStrictEqual(
|
||||||
|
G2.y,
|
||||||
|
494088759867433727674302672526735089350544552303727723746126484473087719117037293890093462157703888342865036477787453078312060500281069n
|
||||||
|
);
|
||||||
|
const G3 = Point.BASE.multiply(3n).toAffine();
|
||||||
|
deepStrictEqual(
|
||||||
|
G3.x,
|
||||||
|
23839778817283171003887799738662344287085130522697782688245073320169861206004018274567429238677677920280078599146891901463786155880335n
|
||||||
|
);
|
||||||
|
deepStrictEqual(
|
||||||
|
G3.y,
|
||||||
|
636046652612779686502873775776967954190574036985351036782021535703553242737829645273154208057988851307101009474686328623630835377952508n
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
should('Basic/decompress', () => {
|
||||||
|
const G1 = Point.BASE;
|
||||||
|
const G2 = Point.BASE.multiply(2n);
|
||||||
|
const G3 = Point.BASE.multiply(3n);
|
||||||
|
const points = [G1, G2, G3];
|
||||||
|
const getXY = (p) => p.toAffine();
|
||||||
|
for (const p of points) deepStrictEqual(getXY(Point.fromHex(p.toHex())), getXY(p));
|
||||||
|
});
|
||||||
|
|
||||||
|
const VECTORS_RFC8032 = [
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'6c82a562cb808d10d632be89c8513ebf' +
|
||||||
|
'6c929f34ddfa8c9f63c9960ef6e348a3' +
|
||||||
|
'528c8a3fcc2f044e39a3fc5b94492f8f' +
|
||||||
|
'032e7549a20098f95b',
|
||||||
|
publicKey:
|
||||||
|
'5fd7449b59b461fd2ce787ec616ad46a' +
|
||||||
|
'1da1342485a70e1f8a0ea75d80e96778' +
|
||||||
|
'edf124769b46c7061bd6783df1e50f6c' +
|
||||||
|
'd1fa1abeafe8256180',
|
||||||
|
message: '',
|
||||||
|
signature:
|
||||||
|
'533a37f6bbe457251f023c0d88f976ae' +
|
||||||
|
'2dfb504a843e34d2074fd823d41a591f' +
|
||||||
|
'2b233f034f628281f2fd7a22ddd47d78' +
|
||||||
|
'28c59bd0a21bfd3980ff0d2028d4b18a' +
|
||||||
|
'9df63e006c5d1c2d345b925d8dc00b41' +
|
||||||
|
'04852db99ac5c7cdda8530a113a0f4db' +
|
||||||
|
'b61149f05a7363268c71d95808ff2e65' +
|
||||||
|
'2600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'c4eab05d357007c632f3dbb48489924d' +
|
||||||
|
'552b08fe0c353a0d4a1f00acda2c463a' +
|
||||||
|
'fbea67c5e8d2877c5e3bc397a659949e' +
|
||||||
|
'f8021e954e0a12274e',
|
||||||
|
publicKey:
|
||||||
|
'43ba28f430cdff456ae531545f7ecd0a' +
|
||||||
|
'c834a55d9358c0372bfa0c6c6798c086' +
|
||||||
|
'6aea01eb00742802b8438ea4cb82169c' +
|
||||||
|
'235160627b4c3a9480',
|
||||||
|
|
||||||
|
message: '03',
|
||||||
|
signature:
|
||||||
|
'26b8f91727bd62897af15e41eb43c377' +
|
||||||
|
'efb9c610d48f2335cb0bd0087810f435' +
|
||||||
|
'2541b143c4b981b7e18f62de8ccdf633' +
|
||||||
|
'fc1bf037ab7cd779805e0dbcc0aae1cb' +
|
||||||
|
'cee1afb2e027df36bc04dcecbf154336' +
|
||||||
|
'c19f0af7e0a6472905e799f1953d2a0f' +
|
||||||
|
'f3348ab21aa4adafd1d234441cf807c0' +
|
||||||
|
'3a00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'cd23d24f714274e744343237b93290f5' +
|
||||||
|
'11f6425f98e64459ff203e8985083ffd' +
|
||||||
|
'f60500553abc0e05cd02184bdb89c4cc' +
|
||||||
|
'd67e187951267eb328',
|
||||||
|
publicKey:
|
||||||
|
'dcea9e78f35a1bf3499a831b10b86c90' +
|
||||||
|
'aac01cd84b67a0109b55a36e9328b1e3' +
|
||||||
|
'65fce161d71ce7131a543ea4cb5f7e9f' +
|
||||||
|
'1d8b00696447001400',
|
||||||
|
message: '0c3e544074ec63b0265e0c',
|
||||||
|
signature:
|
||||||
|
'1f0a8888ce25e8d458a21130879b840a' +
|
||||||
|
'9089d999aaba039eaf3e3afa090a09d3' +
|
||||||
|
'89dba82c4ff2ae8ac5cdfb7c55e94d5d' +
|
||||||
|
'961a29fe0109941e00b8dbdeea6d3b05' +
|
||||||
|
'1068df7254c0cdc129cbe62db2dc957d' +
|
||||||
|
'bb47b51fd3f213fb8698f064774250a5' +
|
||||||
|
'028961c9bf8ffd973fe5d5c206492b14' +
|
||||||
|
'0e00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'258cdd4ada32ed9c9ff54e63756ae582' +
|
||||||
|
'fb8fab2ac721f2c8e676a72768513d93' +
|
||||||
|
'9f63dddb55609133f29adf86ec9929dc' +
|
||||||
|
'cb52c1c5fd2ff7e21b',
|
||||||
|
publicKey:
|
||||||
|
'3ba16da0c6f2cc1f30187740756f5e79' +
|
||||||
|
'8d6bc5fc015d7c63cc9510ee3fd44adc' +
|
||||||
|
'24d8e968b6e46e6f94d19b945361726b' +
|
||||||
|
'd75e149ef09817f580',
|
||||||
|
message: '64a65f3cdedcdd66811e2915',
|
||||||
|
signature:
|
||||||
|
'7eeeab7c4e50fb799b418ee5e3197ff6' +
|
||||||
|
'bf15d43a14c34389b59dd1a7b1b85b4a' +
|
||||||
|
'e90438aca634bea45e3a2695f1270f07' +
|
||||||
|
'fdcdf7c62b8efeaf00b45c2c96ba457e' +
|
||||||
|
'b1a8bf075a3db28e5c24f6b923ed4ad7' +
|
||||||
|
'47c3c9e03c7079efb87cb110d3a99861' +
|
||||||
|
'e72003cbae6d6b8b827e4e6c143064ff' +
|
||||||
|
'3c00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'7ef4e84544236752fbb56b8f31a23a10' +
|
||||||
|
'e42814f5f55ca037cdcc11c64c9a3b29' +
|
||||||
|
'49c1bb60700314611732a6c2fea98eeb' +
|
||||||
|
'c0266a11a93970100e',
|
||||||
|
publicKey:
|
||||||
|
'b3da079b0aa493a5772029f0467baebe' +
|
||||||
|
'e5a8112d9d3a22532361da294f7bb381' +
|
||||||
|
'5c5dc59e176b4d9f381ca0938e13c6c0' +
|
||||||
|
'7b174be65dfa578e80',
|
||||||
|
message: '64a65f3cdedcdd66811e2915e7',
|
||||||
|
signature:
|
||||||
|
'6a12066f55331b6c22acd5d5bfc5d712' +
|
||||||
|
'28fbda80ae8dec26bdd306743c5027cb' +
|
||||||
|
'4890810c162c027468675ecf645a8317' +
|
||||||
|
'6c0d7323a2ccde2d80efe5a1268e8aca' +
|
||||||
|
'1d6fbc194d3f77c44986eb4ab4177919' +
|
||||||
|
'ad8bec33eb47bbb5fc6e28196fd1caf5' +
|
||||||
|
'6b4e7e0ba5519234d047155ac727a105' +
|
||||||
|
'3100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'd65df341ad13e008567688baedda8e9d' +
|
||||||
|
'cdc17dc024974ea5b4227b6530e339bf' +
|
||||||
|
'f21f99e68ca6968f3cca6dfe0fb9f4fa' +
|
||||||
|
'b4fa135d5542ea3f01',
|
||||||
|
publicKey:
|
||||||
|
'df9705f58edbab802c7f8363cfe5560a' +
|
||||||
|
'b1c6132c20a9f1dd163483a26f8ac53a' +
|
||||||
|
'39d6808bf4a1dfbd261b099bb03b3fb5' +
|
||||||
|
'0906cb28bd8a081f00',
|
||||||
|
message:
|
||||||
|
'bd0f6a3747cd561bdddf4640a332461a' +
|
||||||
|
'4a30a12a434cd0bf40d766d9c6d458e5' +
|
||||||
|
'512204a30c17d1f50b5079631f64eb31' +
|
||||||
|
'12182da3005835461113718d1a5ef944',
|
||||||
|
signature:
|
||||||
|
'554bc2480860b49eab8532d2a533b7d5' +
|
||||||
|
'78ef473eeb58c98bb2d0e1ce488a98b1' +
|
||||||
|
'8dfde9b9b90775e67f47d4a1c3482058' +
|
||||||
|
'efc9f40d2ca033a0801b63d45b3b722e' +
|
||||||
|
'f552bad3b4ccb667da350192b61c508c' +
|
||||||
|
'f7b6b5adadc2c8d9a446ef003fb05cba' +
|
||||||
|
'5f30e88e36ec2703b349ca229c267083' +
|
||||||
|
'3900',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'2ec5fe3c17045abdb136a5e6a913e32a' +
|
||||||
|
'b75ae68b53d2fc149b77e504132d3756' +
|
||||||
|
'9b7e766ba74a19bd6162343a21c8590a' +
|
||||||
|
'a9cebca9014c636df5',
|
||||||
|
publicKey:
|
||||||
|
'79756f014dcfe2079f5dd9e718be4171' +
|
||||||
|
'e2ef2486a08f25186f6bff43a9936b9b' +
|
||||||
|
'fe12402b08ae65798a3d81e22e9ec80e' +
|
||||||
|
'7690862ef3d4ed3a00',
|
||||||
|
message:
|
||||||
|
'15777532b0bdd0d1389f636c5f6b9ba7' +
|
||||||
|
'34c90af572877e2d272dd078aa1e567c' +
|
||||||
|
'fa80e12928bb542330e8409f31745041' +
|
||||||
|
'07ecd5efac61ae7504dabe2a602ede89' +
|
||||||
|
'e5cca6257a7c77e27a702b3ae39fc769' +
|
||||||
|
'fc54f2395ae6a1178cab4738e543072f' +
|
||||||
|
'c1c177fe71e92e25bf03e4ecb72f47b6' +
|
||||||
|
'4d0465aaea4c7fad372536c8ba516a60' +
|
||||||
|
'39c3c2a39f0e4d832be432dfa9a706a6' +
|
||||||
|
'e5c7e19f397964ca4258002f7c0541b5' +
|
||||||
|
'90316dbc5622b6b2a6fe7a4abffd9610' +
|
||||||
|
'5eca76ea7b98816af0748c10df048ce0' +
|
||||||
|
'12d901015a51f189f3888145c03650aa' +
|
||||||
|
'23ce894c3bd889e030d565071c59f409' +
|
||||||
|
'a9981b51878fd6fc110624dcbcde0bf7' +
|
||||||
|
'a69ccce38fabdf86f3bef6044819de11',
|
||||||
|
signature:
|
||||||
|
'c650ddbb0601c19ca11439e1640dd931' +
|
||||||
|
'f43c518ea5bea70d3dcde5f4191fe53f' +
|
||||||
|
'00cf966546b72bcc7d58be2b9badef28' +
|
||||||
|
'743954e3a44a23f880e8d4f1cfce2d7a' +
|
||||||
|
'61452d26da05896f0a50da66a239a8a1' +
|
||||||
|
'88b6d825b3305ad77b73fbac0836ecc6' +
|
||||||
|
'0987fd08527c1a8e80d5823e65cafe2a' +
|
||||||
|
'3d00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'872d093780f5d3730df7c212664b37b8' +
|
||||||
|
'a0f24f56810daa8382cd4fa3f77634ec' +
|
||||||
|
'44dc54f1c2ed9bea86fafb7632d8be19' +
|
||||||
|
'9ea165f5ad55dd9ce8',
|
||||||
|
publicKey:
|
||||||
|
'a81b2e8a70a5ac94ffdbcc9badfc3feb' +
|
||||||
|
'0801f258578bb114ad44ece1ec0e799d' +
|
||||||
|
'a08effb81c5d685c0c56f64eecaef8cd' +
|
||||||
|
'f11cc38737838cf400',
|
||||||
|
message:
|
||||||
|
'6ddf802e1aae4986935f7f981ba3f035' +
|
||||||
|
'1d6273c0a0c22c9c0e8339168e675412' +
|
||||||
|
'a3debfaf435ed651558007db4384b650' +
|
||||||
|
'fcc07e3b586a27a4f7a00ac8a6fec2cd' +
|
||||||
|
'86ae4bf1570c41e6a40c931db27b2faa' +
|
||||||
|
'15a8cedd52cff7362c4e6e23daec0fbc' +
|
||||||
|
'3a79b6806e316efcc7b68119bf46bc76' +
|
||||||
|
'a26067a53f296dafdbdc11c77f7777e9' +
|
||||||
|
'72660cf4b6a9b369a6665f02e0cc9b6e' +
|
||||||
|
'dfad136b4fabe723d2813db3136cfde9' +
|
||||||
|
'b6d044322fee2947952e031b73ab5c60' +
|
||||||
|
'3349b307bdc27bc6cb8b8bbd7bd32321' +
|
||||||
|
'9b8033a581b59eadebb09b3c4f3d2277' +
|
||||||
|
'd4f0343624acc817804728b25ab79717' +
|
||||||
|
'2b4c5c21a22f9c7839d64300232eb66e' +
|
||||||
|
'53f31c723fa37fe387c7d3e50bdf9813' +
|
||||||
|
'a30e5bb12cf4cd930c40cfb4e1fc6225' +
|
||||||
|
'92a49588794494d56d24ea4b40c89fc0' +
|
||||||
|
'596cc9ebb961c8cb10adde976a5d602b' +
|
||||||
|
'1c3f85b9b9a001ed3c6a4d3b1437f520' +
|
||||||
|
'96cd1956d042a597d561a596ecd3d173' +
|
||||||
|
'5a8d570ea0ec27225a2c4aaff26306d1' +
|
||||||
|
'526c1af3ca6d9cf5a2c98f47e1c46db9' +
|
||||||
|
'a33234cfd4d81f2c98538a09ebe76998' +
|
||||||
|
'd0d8fd25997c7d255c6d66ece6fa56f1' +
|
||||||
|
'1144950f027795e653008f4bd7ca2dee' +
|
||||||
|
'85d8e90f3dc315130ce2a00375a318c7' +
|
||||||
|
'c3d97be2c8ce5b6db41a6254ff264fa6' +
|
||||||
|
'155baee3b0773c0f497c573f19bb4f42' +
|
||||||
|
'40281f0b1f4f7be857a4e59d416c06b4' +
|
||||||
|
'c50fa09e1810ddc6b1467baeac5a3668' +
|
||||||
|
'd11b6ecaa901440016f389f80acc4db9' +
|
||||||
|
'77025e7f5924388c7e340a732e554440' +
|
||||||
|
'e76570f8dd71b7d640b3450d1fd5f041' +
|
||||||
|
'0a18f9a3494f707c717b79b4bf75c984' +
|
||||||
|
'00b096b21653b5d217cf3565c9597456' +
|
||||||
|
'f70703497a078763829bc01bb1cbc8fa' +
|
||||||
|
'04eadc9a6e3f6699587a9e75c94e5bab' +
|
||||||
|
'0036e0b2e711392cff0047d0d6b05bd2' +
|
||||||
|
'a588bc109718954259f1d86678a579a3' +
|
||||||
|
'120f19cfb2963f177aeb70f2d4844826' +
|
||||||
|
'262e51b80271272068ef5b3856fa8535' +
|
||||||
|
'aa2a88b2d41f2a0e2fda7624c2850272' +
|
||||||
|
'ac4a2f561f8f2f7a318bfd5caf969614' +
|
||||||
|
'9e4ac824ad3460538fdc25421beec2cc' +
|
||||||
|
'6818162d06bbed0c40a387192349db67' +
|
||||||
|
'a118bada6cd5ab0140ee273204f628aa' +
|
||||||
|
'd1c135f770279a651e24d8c14d75a605' +
|
||||||
|
'9d76b96a6fd857def5e0b354b27ab937' +
|
||||||
|
'a5815d16b5fae407ff18222c6d1ed263' +
|
||||||
|
'be68c95f32d908bd895cd76207ae7264' +
|
||||||
|
'87567f9a67dad79abec316f683b17f2d' +
|
||||||
|
'02bf07e0ac8b5bc6162cf94697b3c27c' +
|
||||||
|
'd1fea49b27f23ba2901871962506520c' +
|
||||||
|
'392da8b6ad0d99f7013fbc06c2c17a56' +
|
||||||
|
'9500c8a7696481c1cd33e9b14e40b82e' +
|
||||||
|
'79a5f5db82571ba97bae3ad3e0479515' +
|
||||||
|
'bb0e2b0f3bfcd1fd33034efc6245eddd' +
|
||||||
|
'7ee2086ddae2600d8ca73e214e8c2b0b' +
|
||||||
|
'db2b047c6a464a562ed77b73d2d841c4' +
|
||||||
|
'b34973551257713b753632efba348169' +
|
||||||
|
'abc90a68f42611a40126d7cb21b58695' +
|
||||||
|
'568186f7e569d2ff0f9e745d0487dd2e' +
|
||||||
|
'b997cafc5abf9dd102e62ff66cba87',
|
||||||
|
signature:
|
||||||
|
'e301345a41a39a4d72fff8df69c98075' +
|
||||||
|
'a0cc082b802fc9b2b6bc503f926b65bd' +
|
||||||
|
'df7f4c8f1cb49f6396afc8a70abe6d8a' +
|
||||||
|
'ef0db478d4c6b2970076c6a0484fe76d' +
|
||||||
|
'76b3a97625d79f1ce240e7c576750d29' +
|
||||||
|
'5528286f719b413de9ada3e8eb78ed57' +
|
||||||
|
'3603ce30d8bb761785dc30dbc320869e' +
|
||||||
|
'1a00',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('RFC8032', () => {
|
||||||
|
for (let i = 0; i < VECTORS_RFC8032.length; i++) {
|
||||||
|
const v = VECTORS_RFC8032[i];
|
||||||
|
should(`${i}`, () => {
|
||||||
|
deepStrictEqual(hex(ed.getPublicKey(v.secretKey)), v.publicKey);
|
||||||
|
deepStrictEqual(hex(ed.sign(v.message, v.secretKey)), v.signature);
|
||||||
|
deepStrictEqual(ed.verify(v.signature, v.message, v.publicKey), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
should('not accept >57byte private keys', () => {
|
||||||
|
const invalidPriv =
|
||||||
|
100000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800073278156000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000n;
|
||||||
|
throws(() => ed.getPublicKey(invalidPriv));
|
||||||
|
});
|
||||||
|
|
||||||
|
function to57Bytes(numOrStr) {
|
||||||
|
let hex = typeof numOrStr === 'string' ? numOrStr : numOrStr.toString(16);
|
||||||
|
return hexToBytes(hex.padStart(114, '0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
should('verify recent signature', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(
|
||||||
|
fc.hexaString({ minLength: 2, maxLength: 57 }),
|
||||||
|
fc.bigInt(2n, ed.CURVE.n),
|
||||||
|
(message, privateKey) => {
|
||||||
|
const publicKey = ed.getPublicKey(to57Bytes(privateKey));
|
||||||
|
const signature = ed.sign(to57Bytes(message), to57Bytes(privateKey));
|
||||||
|
deepStrictEqual(publicKey.length, 57);
|
||||||
|
deepStrictEqual(signature.length, 114);
|
||||||
|
deepStrictEqual(ed.verify(signature, to57Bytes(message), publicKey), true);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{ numRuns: 5 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('not verify signature with wrong message', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(
|
||||||
|
fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
||||||
|
fc.array(fc.integer({ min: 0x00, max: 0xff })),
|
||||||
|
fc.bigInt(1n, ed.CURVE.n),
|
||||||
|
(bytes, wrongBytes, privateKey) => {
|
||||||
|
const message = new Uint8Array(bytes);
|
||||||
|
const wrongMessage = new Uint8Array(wrongBytes);
|
||||||
|
const priv = to57Bytes(privateKey);
|
||||||
|
const publicKey = ed.getPublicKey(priv);
|
||||||
|
const signature = ed.sign(message, priv);
|
||||||
|
deepStrictEqual(
|
||||||
|
ed.verify(signature, wrongMessage, publicKey),
|
||||||
|
bytes.toString() === wrongBytes.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{ numRuns: 5 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const privKey = to57Bytes('a665a45920422f9d417e4867ef');
|
||||||
|
const msg = hexToBytes('874f9960c5d2b7a9b5fad383e1ba44719ebb743a');
|
||||||
|
const wrongMsg = hexToBytes('589d8c7f1da0a24bc07b7381ad48b1cfc211af1c');
|
||||||
|
describe('basic 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);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
should('BASE_POINT.multiply() throws in Point#multiply on TEST 5', () => {
|
||||||
|
for (const num of [0n, 0, -1n, -1, 1.1]) {
|
||||||
|
throws(() => ed.ExtendedPoint.BASE.multiply(num));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
should('input immutability: sign/verify are immutable', () => {
|
||||||
|
const privateKey = ed.utils.randomPrivateKey();
|
||||||
|
const publicKey = ed.getPublicKey(privateKey);
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
let payload = randomBytes(100);
|
||||||
|
let signature = ed.sign(payload, privateKey);
|
||||||
|
if (!ed.verify(signature, payload, publicKey)) {
|
||||||
|
throw new Error('Signature verification failed');
|
||||||
|
}
|
||||||
|
const signatureCopy = Buffer.alloc(signature.byteLength);
|
||||||
|
signatureCopy.set(signature, 0); // <-- breaks
|
||||||
|
payload = payload.slice();
|
||||||
|
signature = signature.slice();
|
||||||
|
|
||||||
|
if (!ed.verify(signatureCopy, payload, publicKey))
|
||||||
|
throw new Error('Copied signature verification failed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('wycheproof', () => {
|
||||||
|
for (let g = 0; g < ed448vectors.testGroups.length; g++) {
|
||||||
|
const group = ed448vectors.testGroups[g];
|
||||||
|
const key = group.key;
|
||||||
|
should(`ED448(${g}, public)`, () => {
|
||||||
|
deepStrictEqual(hex(ed.getPublicKey(key.sk)), key.pk);
|
||||||
|
});
|
||||||
|
should(`ED448`, () => {
|
||||||
|
for (let i = 0; i < group.tests.length; i++) {
|
||||||
|
const v = group.tests[i];
|
||||||
|
const index = `${g}/${i} ${v.comment}`;
|
||||||
|
if (v.result === 'valid' || v.result === 'acceptable') {
|
||||||
|
deepStrictEqual(hex(ed.sign(v.msg, key.sk)), v.sig, index);
|
||||||
|
deepStrictEqual(ed.verify(v.sig, v.msg, key.pk), true, index);
|
||||||
|
} else if (v.result === 'invalid') {
|
||||||
|
let failed = false;
|
||||||
|
try {
|
||||||
|
failed = !ed.verify(v.sig, v.msg, key.pk);
|
||||||
|
} catch (error) {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
deepStrictEqual(failed, true, index);
|
||||||
|
} else throw new Error('unknown test result');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ECDH
|
||||||
|
const rfc7748Mul = [
|
||||||
|
{
|
||||||
|
scalar:
|
||||||
|
'3d262fddf9ec8e88495266fea19a34d28882acef045104d0d1aae121700a779c984c24f8cdd78fbff44943eba368f54b29259a4f1c600ad3',
|
||||||
|
u: '06fce640fa3487bfda5f6cf2d5263f8aad88334cbd07437f020f08f9814dc031ddbdc38c19c6da2583fa5429db94ada18aa7a7fb4ef8a086',
|
||||||
|
outputU:
|
||||||
|
'ce3e4ff95a60dc6697da1db1d85e6afbdf79b50a2412d7546d5f239fe14fbaadeb445fc66a01b0779d98223961111e21766282f73dd96b6f',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scalar:
|
||||||
|
'203d494428b8399352665ddca42f9de8fef600908e0d461cb021f8c538345dd77c3e4806e25f46d3315c44e0a5b4371282dd2c8d5be3095f',
|
||||||
|
u: '0fbcc2f993cd56d3305b0b7d9e55d4c1a8fb5dbb52f8e9a1e9b6201b165d015894e56c4d3570bee52fe205e28a78b91cdfbde71ce8d157db',
|
||||||
|
outputU:
|
||||||
|
'884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
describe('RFC7748', () => {
|
||||||
|
for (let i = 0; i < rfc7748Mul.length; i++) {
|
||||||
|
const v = rfc7748Mul[i];
|
||||||
|
should(`scalarMult (${i})`, () => {
|
||||||
|
deepStrictEqual(hex(x448.scalarMult(v.scalar, v.u)), v.outputU);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const rfc7748Iter = [
|
||||||
|
{
|
||||||
|
scalar:
|
||||||
|
'3f482c8a9f19b01e6c46ee9711d9dc14fd4bf67af30765c2ae2b846a4d23a8cd0db897086239492caf350b51f833868b9bc2b3bca9cf4113',
|
||||||
|
iters: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scalar:
|
||||||
|
'aa3b4749d55b9daf1e5b00288826c467274ce3ebbdd5c17b975e09d4af6c67cf10d087202db88286e2b79fceea3ec353ef54faa26e219f38',
|
||||||
|
iters: 1000,
|
||||||
|
},
|
||||||
|
// { scalar: '077f453681caca3693198420bbe515cae0002472519b3e67661a7e89cab94695c8f4bcd66e61b9b9c946da8d524de3d69bd9d9d66b997e37', iters: 1000000 },
|
||||||
|
];
|
||||||
|
for (let i = 0; i < rfc7748Iter.length; i++) {
|
||||||
|
const { scalar, iters } = rfc7748Iter[i];
|
||||||
|
should(`RFC7748: scalarMult iteration (${i})`, () => {
|
||||||
|
let k = x448.GuBytes;
|
||||||
|
for (let i = 0, u = k; i < iters; i++) [k, u] = [x448.scalarMult(k, u), k];
|
||||||
|
deepStrictEqual(hex(k), scalar);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
should('RFC7748 getSharedKey', () => {
|
||||||
|
const alicePrivate =
|
||||||
|
'9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b';
|
||||||
|
const alicePublic =
|
||||||
|
'9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0';
|
||||||
|
const bobPrivate =
|
||||||
|
'1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d';
|
||||||
|
const bobPublic =
|
||||||
|
'3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609';
|
||||||
|
const shared =
|
||||||
|
'07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d';
|
||||||
|
deepStrictEqual(alicePublic, hex(x448.getPublicKey(alicePrivate)));
|
||||||
|
deepStrictEqual(bobPublic, hex(x448.getPublicKey(bobPrivate)));
|
||||||
|
deepStrictEqual(hex(x448.scalarMult(alicePrivate, bobPublic)), shared);
|
||||||
|
deepStrictEqual(hex(x448.scalarMult(bobPrivate, alicePublic)), shared);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('wycheproof', () => {
|
||||||
|
const group = x448vectors.testGroups[0];
|
||||||
|
should(`X448`, () => {
|
||||||
|
for (let i = 0; i < group.tests.length; i++) {
|
||||||
|
const v = group.tests[i];
|
||||||
|
const index = `(${i}, ${v.result}) ${v.comment}`;
|
||||||
|
if (v.result === 'valid' || v.result === 'acceptable') {
|
||||||
|
try {
|
||||||
|
const shared = hex(x448.scalarMult(v.private, v.public));
|
||||||
|
deepStrictEqual(shared, v.shared, index);
|
||||||
|
} 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;
|
||||||
|
if (e.message.includes('Expected 56 bytes')) return;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} else if (v.result === 'invalid') {
|
||||||
|
let failed = false;
|
||||||
|
try {
|
||||||
|
x448.scalarMult(v.private, v.public);
|
||||||
|
} catch (error) {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
deepStrictEqual(failed, true, index);
|
||||||
|
} else throw new Error('unknown test result');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// should('X448: should convert base point to montgomery using fromPoint', () => {
|
||||||
|
// deepStrictEqual(
|
||||||
|
// hex(ed.montgomeryCurve.UfromPoint(Point.BASE)),
|
||||||
|
// ed.montgomeryCurve.BASE_POINT_U
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
|
||||||
|
// should('X448/getSharedSecret() should be commutative', async () => {
|
||||||
|
// for (let i = 0; i < 512; i++) {
|
||||||
|
// const asec = ed.utils.randomPrivateKey();
|
||||||
|
// const apub = ed.getPublicKey(asec);
|
||||||
|
// const bsec = ed.utils.randomPrivateKey();
|
||||||
|
// const bpub = ed.getPublicKey(bsec);
|
||||||
|
// try {
|
||||||
|
// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('not commutative', { asec, apub, bsec, bpub });
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
const VECTORS_RFC8032_CTX = [
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'c4eab05d357007c632f3dbb48489924d552b08fe0c353a0d4a1f00acda2c463afbea67c5e8d2877c5e3bc397a659949ef8021e954e0a12274e',
|
||||||
|
publicKey:
|
||||||
|
'43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a9480',
|
||||||
|
message: '03',
|
||||||
|
context: '666f6f',
|
||||||
|
signature:
|
||||||
|
'd4f8f6131770dd46f40867d6fd5d5055' +
|
||||||
|
'de43541f8c5e35abbcd001b32a89f7d2' +
|
||||||
|
'151f7647f11d8ca2ae279fb842d60721' +
|
||||||
|
'7fce6e042f6815ea000c85741de5c8da' +
|
||||||
|
'1144a6a1aba7f96de42505d7a7298524' +
|
||||||
|
'fda538fccbbb754f578c1cad10d54d0d' +
|
||||||
|
'5428407e85dcbc98a49155c13764e66c' +
|
||||||
|
'3c00',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < VECTORS_RFC8032_CTX.length; i++) {
|
||||||
|
const v = VECTORS_RFC8032_CTX[i];
|
||||||
|
should(`RFC8032ctx/${i}`, () => {
|
||||||
|
deepStrictEqual(hex(ed.getPublicKey(v.secretKey)), v.publicKey);
|
||||||
|
deepStrictEqual(hex(ed.sign(v.message, v.secretKey, v.context)), v.signature);
|
||||||
|
deepStrictEqual(ed.verify(v.signature, v.message, v.publicKey, v.context), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const VECTORS_RFC8032_PH = [
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42ef7822e0d5104127dc05d6dbefde69e3ab2cec7c867c6e2c49',
|
||||||
|
publicKey:
|
||||||
|
'259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743c09428a131d6b1b57303d90d8132c276d5ed3d5d01c0f53880',
|
||||||
|
message: '616263',
|
||||||
|
signature:
|
||||||
|
'822f6901f7480f3d5f562c592994d969' +
|
||||||
|
'3602875614483256505600bbc281ae38' +
|
||||||
|
'1f54d6bce2ea911574932f52a4e6cadd' +
|
||||||
|
'78769375ec3ffd1b801a0d9b3f4030cd' +
|
||||||
|
'433964b6457ea39476511214f97469b5' +
|
||||||
|
'7dd32dbc560a9a94d00bff07620464a3' +
|
||||||
|
'ad203df7dc7ce360c3cd3696d9d9fab9' +
|
||||||
|
'0f00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42ef7822e0d5104127dc05d6dbefde69e3ab2cec7c867c6e2c49',
|
||||||
|
publicKey:
|
||||||
|
'259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743c09428a131d6b1b57303d90d8132c276d5ed3d5d01c0f53880',
|
||||||
|
message: '616263',
|
||||||
|
context: '666f6f',
|
||||||
|
signature:
|
||||||
|
'c32299d46ec8ff02b54540982814dce9' +
|
||||||
|
'a05812f81962b649d528095916a2aa48' +
|
||||||
|
'1065b1580423ef927ecf0af5888f90da' +
|
||||||
|
'0f6a9a85ad5dc3f280d91224ba9911a3' +
|
||||||
|
'653d00e484e2ce232521481c8658df30' +
|
||||||
|
'4bb7745a73514cdb9bf3e15784ab7128' +
|
||||||
|
'4f8d0704a608c54a6b62d97beb511d13' +
|
||||||
|
'2100',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < VECTORS_RFC8032_PH.length; i++) {
|
||||||
|
const v = VECTORS_RFC8032_PH[i];
|
||||||
|
should(`RFC8032ph/${i}`, () => {
|
||||||
|
deepStrictEqual(hex(ed448ph.getPublicKey(v.secretKey)), v.publicKey);
|
||||||
|
deepStrictEqual(hex(ed448ph.sign(v.message, v.secretKey, v.context)), v.signature);
|
||||||
|
deepStrictEqual(ed448ph.verify(v.signature, v.message, v.publicKey, v.context), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
should('X448 base point', () => {
|
||||||
|
const { x, y } = Point.BASE;
|
||||||
|
const { Fp } = ed448.CURVE;
|
||||||
|
// const invX = Fp.invert(x * x); // x²
|
||||||
|
const u = Fp.div(Fp.create(y * y), Fp.create(x * x)); // (y²/x²)
|
||||||
|
// const u = Fp.create(y * y * invX);
|
||||||
|
deepStrictEqual(numberToBytesLE(u, 56), x448.GuBytes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ESM is broken.
|
||||||
|
import url from 'url';
|
||||||
|
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
||||||
|
should.run();
|
||||||
|
}
|
||||||
@@ -28,16 +28,16 @@
|
|||||||
"Uy": "EEAB6F3DEBE455E3DBF85416F7030CBD94F34F2D6F232C69F3C1385A",
|
"Uy": "EEAB6F3DEBE455E3DBF85416F7030CBD94F34F2D6F232C69F3C1385A",
|
||||||
"cases": [
|
"cases": [
|
||||||
{
|
{
|
||||||
"k": "AD3029E0278F80643DE33917CE6908C70A8FF50A411F06E41DEDFCDC",
|
"k": "C1D1F2F10881088301880506805FEB4825FE09ACB6816C36991AA06D",
|
||||||
"message": "sample",
|
"message": "sample",
|
||||||
"r": "61AA3DA010E8E8406C656BC477A7A7189895E7E840CDFE8FF42307BA",
|
"r": "1CDFE6662DDE1E4A1EC4CDEDF6A1F5A2FB7FBD9145C12113E6ABFD3E",
|
||||||
"s": "BC814050DAB5D23770879494F9E0A680DC1AF7161991BDE692B10101"
|
"s": "A6694FD7718A21053F225D3F46197CA699D45006C06F871808F43EBC"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"k": "FF86F57924DA248D6E44E8154EB69F0AE2AEBAEE9931D0B5A969F904",
|
"k": "DF8B38D40DCA3E077D0AC520BF56B6D565134D9B5F2EAE0D34900524",
|
||||||
"message": "test",
|
"message": "test",
|
||||||
"r": "AD04DDE87B84747A243A631EA47A1BA6D1FAA059149AD2440DE6FBA6",
|
"r": "C441CE8E261DED634E4CF84910E4C5D1D22C5CF3B732BB204DBEF019",
|
||||||
"s": "178D49B1AE90E3D8B629BE3DB5683915F4E8C99FDF6E666CF37ADCFD"
|
"s": "902F42847A63BDC5F6046ADA114953120F99442D76510150F372A3F4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user