Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
210
README.md
210
README.md
@@ -7,12 +7,11 @@ Minimal, auditable JS implementation of elliptic curve cryptography.
|
|||||||
- [hash to curve](https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/)
|
- [hash to curve](https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/)
|
||||||
for encoding or hashing an arbitrary string to a point on an elliptic curve
|
for encoding or hashing an arbitrary string to a point on an elliptic curve
|
||||||
- [Poseidon](https://www.poseidon-hash.info) ZK-friendly hash
|
- [Poseidon](https://www.poseidon-hash.info) ZK-friendly hash
|
||||||
- Auditable
|
|
||||||
- 🏎 [Ultra-fast](#speed), hand-optimized for caveats of JS engines
|
- 🏎 [Ultra-fast](#speed), hand-optimized for caveats of JS engines
|
||||||
- 🔍 Unique tests ensure correctness. Wycheproof vectors included
|
- 🔍 Unique tests ensure correctness. Wycheproof vectors included
|
||||||
- 🔻 Tree-shaking-friendly: there is no entry point, which ensures small size of your app
|
- 🔻 Tree-shaking-friendly: there is no entry point, which ensures small size of your app
|
||||||
|
|
||||||
There are two parts of the package:
|
Package consists of two parts:
|
||||||
|
|
||||||
1. `abstract/` directory specifies zero-dependency EC algorithms
|
1. `abstract/` directory specifies zero-dependency EC algorithms
|
||||||
2. root directory utilizes one dependency `@noble/hashes` and provides ready-to-use:
|
2. root directory utilizes one dependency `@noble/hashes` and provides ready-to-use:
|
||||||
@@ -23,22 +22,22 @@ There are two parts of the package:
|
|||||||
|
|
||||||
Curves incorporate work from previous noble packages
|
Curves incorporate work from previous noble packages
|
||||||
([secp256k1](https://github.com/paulmillr/noble-secp256k1),
|
([secp256k1](https://github.com/paulmillr/noble-secp256k1),
|
||||||
[ed25519](https://github.com/paulmillr/noble-ed25519),
|
[ed25519](https://github.com/paulmillr/noble-ed25519)),
|
||||||
[bls12-381](https://github.com/paulmillr/noble-bls12-381)),
|
|
||||||
which had security audits and were developed from 2019 to 2022.
|
which had security audits and were developed from 2019 to 2022.
|
||||||
|
Check out [Upgrading](#upgrading) section if you've used them before.
|
||||||
|
|
||||||
### This library belongs to _noble_ crypto
|
### This library belongs to _noble_ crypto
|
||||||
|
|
||||||
> **noble-crypto** — high-security, easily auditable set of contained cryptographic libraries and tools.
|
> **noble-crypto** — high-security, easily auditable set of contained cryptographic libraries and tools.
|
||||||
|
|
||||||
- Minimal dependencies, small files
|
- Protection against supply chain attacks
|
||||||
- Easily auditable TypeScript/JS code
|
- Easily auditable TypeScript/JS code
|
||||||
- Supported in all major browsers and stable node.js versions
|
- Supported in all major browsers and stable node.js versions
|
||||||
- All releases are signed with PGP keys
|
- All releases are signed with PGP keys
|
||||||
- Check out [homepage](https://paulmillr.com/noble/) & all libraries:
|
- Check out [homepage](https://paulmillr.com/noble/) & all libraries:
|
||||||
[curves](https://github.com/paulmillr/noble-curves) ([secp256k1](https://github.com/paulmillr/noble-secp256k1),
|
[curves](https://github.com/paulmillr/noble-curves)
|
||||||
[ed25519](https://github.com/paulmillr/noble-ed25519),
|
([secp256k1](https://github.com/paulmillr/noble-secp256k1),
|
||||||
[bls12-381](https://github.com/paulmillr/noble-bls12-381)),
|
[ed25519](https://github.com/paulmillr/noble-ed25519)),
|
||||||
[hashes](https://github.com/paulmillr/noble-hashes)
|
[hashes](https://github.com/paulmillr/noble-hashes)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -48,23 +47,7 @@ Use NPM in node.js / browser, or include single file from
|
|||||||
|
|
||||||
> npm install @noble/curves
|
> npm install @noble/curves
|
||||||
|
|
||||||
The library does not have an entry point. It allows you to select specific primitives and drop everything else. If you only want to use secp256k1, just use the library with rollup or other bundlers. This is done to make your bundles tiny.
|
The library does not have an entry point. It allows you to select specific primitives and drop everything else. If you only want to use secp256k1, just use the library with rollup or other bundlers. This is done to make your bundles tiny. All curves:
|
||||||
|
|
||||||
```ts
|
|
||||||
// Common.js and ECMAScript Modules (ESM)
|
|
||||||
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
||||||
|
|
||||||
const key = secp256k1.utils.randomPrivateKey();
|
|
||||||
const pub = secp256k1.getPublicKey(key);
|
|
||||||
const msg = new Uint8Array(32).fill(1);
|
|
||||||
const sig = secp256k1.sign(msg, key);
|
|
||||||
secp256k1.verify(sig, msg, pub) === true;
|
|
||||||
sig.recoverPublicKey(msg) === pub;
|
|
||||||
const someonesPub = secp256k1.getPublicKey(secp256k1.utils.randomPrivateKey());
|
|
||||||
const shared = secp256k1.getSharedSecret(key, someonesPub);
|
|
||||||
```
|
|
||||||
|
|
||||||
All curves:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { secp256k1 } from '@noble/curves/secp256k1';
|
import { secp256k1 } from '@noble/curves/secp256k1';
|
||||||
@@ -80,7 +63,25 @@ import { bn254 } from '@noble/curves/bn';
|
|||||||
import { jubjub } from '@noble/curves/jubjub';
|
import { jubjub } from '@noble/curves/jubjub';
|
||||||
```
|
```
|
||||||
|
|
||||||
To define a custom curve, check out API below.
|
Every curve can be used in the following way:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { secp256k1 } from '@noble/curves/secp256k1'; // Common.js and ECMAScript Modules (ESM)
|
||||||
|
|
||||||
|
const key = secp256k1.utils.randomPrivateKey();
|
||||||
|
const pub = secp256k1.getPublicKey(key);
|
||||||
|
const msg = new Uint8Array(32).fill(1);
|
||||||
|
const sig = secp256k1.sign(msg, key);
|
||||||
|
// weierstrass curves should use extraEntropy: https://moderncrypto.org/mail-archive/curves/2017/000925.html
|
||||||
|
const sigImprovedSecurity = secp256k1.sign(msg, key, { extraEntropy: true });
|
||||||
|
secp256k1.verify(sig, msg, pub) === true;
|
||||||
|
// secp, p*, pasta curves allow pub recovery
|
||||||
|
sig.recoverPublicKey(msg) === pub;
|
||||||
|
const someonesPub = secp256k1.getPublicKey(secp256k1.utils.randomPrivateKey());
|
||||||
|
const shared = secp256k1.getSharedSecret(key, someonesPub);
|
||||||
|
```
|
||||||
|
|
||||||
|
To define a custom curve, check out docs below.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -109,17 +110,20 @@ import * as utils from '@noble/curves/abstract/utils';
|
|||||||
They allow to define a new curve in a few lines of code:
|
They allow to define a new curve in a few lines of code:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { Fp } from '@noble/curves/abstract/modular';
|
import { Field } from '@noble/curves/abstract/modular';
|
||||||
import { weierstrass } from '@noble/curves/abstract/weierstrass';
|
import { weierstrass } from '@noble/curves/abstract/weierstrass';
|
||||||
import { hmac } from '@noble/hashes/hmac';
|
import { hmac } from '@noble/hashes/hmac';
|
||||||
import { sha256 } from '@noble/hashes/sha256';
|
import { sha256 } from '@noble/hashes/sha256';
|
||||||
import { concatBytes, randomBytes } from '@noble/hashes/utils';
|
import { concatBytes, randomBytes } from '@noble/hashes/utils';
|
||||||
|
|
||||||
const secp256k1 = weierstrass({
|
// secq (NOT secp) 256k1: cycle of secp256k1 with Fp/N flipped.
|
||||||
|
// https://zcash.github.io/halo2/background/curves.html#cycles-of-curves
|
||||||
|
// https://personaelabs.org/posts/spartan-ecdsa
|
||||||
|
const secq256k1 = weierstrass({
|
||||||
a: 0n,
|
a: 0n,
|
||||||
b: 7n,
|
b: 7n,
|
||||||
Fp: Fp(2n ** 256n - 2n ** 32n - 2n ** 9n - 2n ** 8n - 2n ** 7n - 2n ** 6n - 2n ** 4n - 1n),
|
Fp: Field(2n ** 256n - 432420386565659656852420866394968145599n),
|
||||||
n: 2n ** 256n - 432420386565659656852420866394968145599n,
|
n: 2n ** 256n - 2n ** 32n - 2n ** 9n - 2n ** 8n - 2n ** 7n - 2n ** 6n - 2n ** 4n - 1n,
|
||||||
Gx: 55066263022277343669578718895168534326250603453777594175500187360389116729240n,
|
Gx: 55066263022277343669578718895168534326250603453777594175500187360389116729240n,
|
||||||
Gy: 32670510020758816978083085130507043184471273380659243275938904335757337482424n,
|
Gy: 32670510020758816978083085130507043184471273380659243275938904335757337482424n,
|
||||||
hash: sha256,
|
hash: sha256,
|
||||||
@@ -331,19 +335,26 @@ The module allows to hash arbitrary strings to elliptic curve points.
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
function expand_message_xmd(
|
function expand_message_xmd(
|
||||||
msg: Uint8Array, DST: Uint8Array, lenInBytes: number, H: CHash
|
msg: Uint8Array,
|
||||||
|
DST: Uint8Array,
|
||||||
|
lenInBytes: number,
|
||||||
|
H: CHash
|
||||||
): Uint8Array;
|
): Uint8Array;
|
||||||
function expand_message_xof(
|
function expand_message_xof(
|
||||||
msg: Uint8Array, DST: Uint8Array, lenInBytes: number, k: number, H: CHash
|
msg: Uint8Array,
|
||||||
|
DST: Uint8Array,
|
||||||
|
lenInBytes: number,
|
||||||
|
k: number,
|
||||||
|
H: CHash
|
||||||
): Uint8Array;
|
): Uint8Array;
|
||||||
```
|
```
|
||||||
|
|
||||||
- `hash_to_field(msg, count, options)` [(spec)](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3)
|
- `hash_to_field(msg, count, options)` [(spec)](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3)
|
||||||
hashes arbitrary-length byte strings to a list of one or more elements of a finite field F.
|
hashes arbitrary-length byte strings to a list of one or more elements of a finite field F.
|
||||||
* `msg` a byte string containing the message to hash
|
_ `msg` a byte string containing the message to hash
|
||||||
* `count` the number of elements of F to output
|
_ `count` the number of elements of F to output
|
||||||
* `options` `{DST: string, p: bigint, m: number, k: number, expand: 'xmd' | 'xof', hash: H}`
|
_ `options` `{DST: string, p: bigint, m: number, k: number, expand: 'xmd' | 'xof', hash: H}`
|
||||||
* Returns `[u_0, ..., u_(count - 1)]`, a list of field elements.
|
_ Returns `[u_0, ..., u_(count - 1)]`, a list of field elements.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
function hash_to_field(msg: Uint8Array, count: number, options: htfOpts): bigint[][];
|
function hash_to_field(msg: Uint8Array, count: number, options: htfOpts): bigint[][];
|
||||||
@@ -445,63 +456,94 @@ We consider infrastructure attacks like rogue NPM modules very important; that's
|
|||||||
Benchmark results on Apple M2 with node v18.10:
|
Benchmark results on Apple M2 with node v18.10:
|
||||||
|
|
||||||
```
|
```
|
||||||
getPublicKey
|
secp256k1
|
||||||
secp256k1 x 5,241 ops/sec @ 190μs/op
|
init x 57 ops/sec @ 17ms/op
|
||||||
P256 x 7,993 ops/sec @ 125μs/op
|
getPublicKey x 4,946 ops/sec @ 202μs/op
|
||||||
P384 x 3,819 ops/sec @ 261μs/op
|
sign x 3,914 ops/sec @ 255μs/op
|
||||||
P521 x 2,074 ops/sec @ 481μs/op
|
verify x 682 ops/sec @ 1ms/op
|
||||||
ed25519 x 8,390 ops/sec @ 119μs/op
|
getSharedSecret x 427 ops/sec @ 2ms/op
|
||||||
ed448 x 3,224 ops/sec @ 310μs/op
|
recoverPublicKey x 683 ops/sec @ 1ms/op
|
||||||
sign
|
schnorr.sign x 539 ops/sec @ 1ms/op
|
||||||
secp256k1 x 3,934 ops/sec @ 254μs/op
|
schnorr.verify x 716 ops/sec @ 1ms/op
|
||||||
P256 x 5,327 ops/sec @ 187μs/op
|
|
||||||
P384 x 2,728 ops/sec @ 366μs/op
|
P256
|
||||||
P521 x 1,594 ops/sec @ 626μs/op
|
init x 30 ops/sec @ 32ms/op
|
||||||
ed25519 x 4,233 ops/sec @ 236μs/op
|
getPublicKey x 5,008 ops/sec @ 199μs/op
|
||||||
ed448 x 1,561 ops/sec @ 640μs/op
|
sign x 3,970 ops/sec @ 251μs/op
|
||||||
verify
|
verify x 515 ops/sec @ 1ms/op
|
||||||
secp256k1 x 731 ops/sec @ 1ms/op
|
|
||||||
P256 x 806 ops/sec @ 1ms/op
|
P384
|
||||||
P384 x 353 ops/sec @ 2ms/op
|
init x 14 ops/sec @ 66ms/op
|
||||||
P521 x 171 ops/sec @ 5ms/op
|
getPublicKey x 2,434 ops/sec @ 410μs/op
|
||||||
ed25519 x 860 ops/sec @ 1ms/op
|
sign x 1,942 ops/sec @ 514μs/op
|
||||||
ed448 x 313 ops/sec @ 3ms/op
|
verify x 206 ops/sec @ 4ms/op
|
||||||
getSharedSecret
|
|
||||||
secp256k1 x 445 ops/sec @ 2ms/op
|
P521
|
||||||
recoverPublicKey
|
init x 7 ops/sec @ 126ms/op
|
||||||
secp256k1 x 732 ops/sec @ 1ms/op
|
getPublicKey x 1,282 ops/sec @ 779μs/op
|
||||||
==== bls12-381 ====
|
sign x 1,077 ops/sec @ 928μs/op
|
||||||
getPublicKey x 817 ops/sec @ 1ms/op
|
verify x 110 ops/sec @ 9ms/op
|
||||||
sign x 50 ops/sec @ 19ms/op
|
|
||||||
verify x 34 ops/sec @ 28ms/op
|
ed25519
|
||||||
pairing x 89 ops/sec @ 11ms/op
|
init x 37 ops/sec @ 26ms/op
|
||||||
==== stark ====
|
getPublicKey x 8,147 ops/sec @ 122μs/op
|
||||||
|
sign x 3,979 ops/sec @ 251μs/op
|
||||||
|
verify x 848 ops/sec @ 1ms/op
|
||||||
|
|
||||||
|
ed448
|
||||||
|
init x 17 ops/sec @ 58ms/op
|
||||||
|
getPublicKey x 3,083 ops/sec @ 324μs/op
|
||||||
|
sign x 1,473 ops/sec @ 678μs/op
|
||||||
|
verify x 323 ops/sec @ 3ms/op
|
||||||
|
|
||||||
|
bls12-381
|
||||||
|
init x 30 ops/sec @ 33ms/op
|
||||||
|
getPublicKey x 788 ops/sec @ 1ms/op
|
||||||
|
sign x 45 ops/sec @ 21ms/op
|
||||||
|
verify x 32 ops/sec @ 30ms/op
|
||||||
|
pairing x 88 ops/sec @ 11ms/op
|
||||||
|
|
||||||
|
stark
|
||||||
|
init x 31 ops/sec @ 31ms/op
|
||||||
pedersen
|
pedersen
|
||||||
old x 85 ops/sec @ 11ms/op
|
├─old x 84 ops/sec @ 11ms/op
|
||||||
noble x 1,216 ops/sec @ 822μs/op
|
└─noble x 802 ops/sec @ 1ms/op
|
||||||
|
poseidon x 7,466 ops/sec @ 133μs/op
|
||||||
verify
|
verify
|
||||||
old x 302 ops/sec @ 3ms/op
|
├─old x 300 ops/sec @ 3ms/op
|
||||||
noble x 698 ops/sec @ 1ms/op
|
└─noble x 474 ops/sec @ 2ms/op
|
||||||
```
|
```
|
||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
Differences from @noble/secp256k1 1.7:
|
If you're coming from single-curve noble packages, the following changes need to be kept in mind:
|
||||||
|
|
||||||
1. Different double() formula (but same addition)
|
- 2d affine (x, y) points have been removed to reduce complexity and improve speed
|
||||||
2. Different sqrt() function
|
- Removed `number` support as a type for private keys. `bigint` is still supported
|
||||||
3. DRBG supports outputLen bigger than outputLen of hmac
|
- `mod`, `invert` are no longer present in `utils`. Use `@noble/curves/abstract/modular.js` now.
|
||||||
4. Support for different hash functions
|
|
||||||
|
|
||||||
Differences from @noble/ed25519 1.7:
|
Upgrading from @noble/secp256k1 1.7:
|
||||||
|
|
||||||
1. Variable field element lengths between EDDSA/ECDH:
|
- Compressed (33-byte) public keys are now returned by default, instead of uncompressed
|
||||||
EDDSA (RFC8032) is 456 bits / 57 bytes, ECDH (RFC7748) is 448 bits / 56 bytes
|
- Methods are now synchronous. Setting `secp.utils.hmacSha256` is no longer required
|
||||||
2. Different addition formula (doubling is same)
|
- `sign()`
|
||||||
3. uvRatio differs between curves (half-expected, not only pow fn changes)
|
- `der`, `recovered` options were removed
|
||||||
4. Point decompression code is different (unexpected), now using generalized formula
|
- `canonical` was renamed to `lowS`
|
||||||
5. Domain function was no-op for ed25519, but adds some data even with empty context for ed448
|
- Return type is now `{ r: bigint, s: bigint, recovery: number }` instance of `Signature`
|
||||||
|
- `verify()`
|
||||||
|
- `strict` was renamed to `lowS`
|
||||||
|
- `recoverPublicKey()`: moved to sig instance `Signature#recoverPublicKey(msgHash)`
|
||||||
|
- `Point` was removed: use `ProjectivePoint` in xyz coordinates
|
||||||
|
- `utils`: Many methods were removed, others were moved to `schnorr` namespace
|
||||||
|
|
||||||
|
Upgrading from @noble/ed25519 1.7:
|
||||||
|
|
||||||
|
- Methods are now synchronous. Setting `secp.utils.hmacSha256` is no longer required
|
||||||
|
- ed25519ph, ed25519ctx
|
||||||
|
- `Point` was removed: use `ExtendedPoint` in xyzt coordinates
|
||||||
|
- `Signature` was removed
|
||||||
|
- `getSharedSecret` was removed: use separate x25519 sub-module
|
||||||
|
- `bigint` is no longer allowed in `getPublicKey`, `sign`, `verify`. Reason: ed25519 is LE, can lead to bugs
|
||||||
|
|
||||||
## Contributing & testing
|
## Contributing & testing
|
||||||
|
|
||||||
|
|||||||
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 '../lib/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 '../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';
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,424 +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]) item.utils.precompute(8);
|
|
||||||
for (let item of [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).toCompactRawBytes(),
|
|
||||||
},
|
|
||||||
verify: {
|
|
||||||
samples: 1000,
|
|
||||||
secp256k1_old: ({ sig, msg, pub }) => {
|
|
||||||
return old_secp.verify(new old_secp.Signature(sig.r, sig.s), msg, pub);
|
|
||||||
},
|
|
||||||
secp256k1: ({ sig, msg, pub }) => secp256k1.verify(sig, msg, pub),
|
|
||||||
},
|
|
||||||
getSharedSecret: {
|
|
||||||
samples: 1000,
|
|
||||||
secp256k1_old: ({ pub, priv }) => old_secp.getSharedSecret(priv, pub),
|
|
||||||
secp256k1: ({ pub, priv }) => secp256k1.getSharedSecret(priv, pub),
|
|
||||||
},
|
|
||||||
recoverPublicKey: {
|
|
||||||
samples: 1000,
|
|
||||||
secp256k1_old: ({ sig, msg }) =>
|
|
||||||
old_secp.recoverPublicKey(msg, new old_secp.Signature(sig.r, sig.s), sig.recovery),
|
|
||||||
secp256k1: ({ sig, msg }) => sig.recoverPublicKey(msg),
|
|
||||||
},
|
|
||||||
// hashToCurve: {
|
|
||||||
// samples: 500,
|
|
||||||
// noble: () => secp256k1.Point.hashToCurve('abcd'),
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
ed25519: {
|
|
||||||
data: () => {
|
|
||||||
function to32Bytes(numOrStr) {
|
|
||||||
const hex = typeof numOrStr === 'string' ? numOrStr : numOrStr.toString(16);
|
|
||||||
return hexToBytes(hex.padStart(64, '0'));
|
|
||||||
}
|
|
||||||
const priv = to32Bytes(0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60n);
|
|
||||||
const pub = noble_ed25519.sync.getPublicKey(priv);
|
|
||||||
const msg = to32Bytes('deadbeefdeadbeefdeadbeefdeadbeefdeadbeef');
|
|
||||||
const sig = noble_ed25519.sync.sign(msg, priv);
|
|
||||||
return { pub, priv, msg, sig };
|
|
||||||
},
|
|
||||||
getPublicKey: {
|
|
||||||
samples: 10000,
|
|
||||||
old: () => noble_ed25519.sync.getPublicKey(noble_ed25519.utils.randomPrivateKey()),
|
|
||||||
noble: () => ed25519.getPublicKey(ed25519.utils.randomPrivateKey()),
|
|
||||||
},
|
|
||||||
sign: {
|
|
||||||
samples: 5000,
|
|
||||||
old: ({ msg, priv }) => noble_ed25519.sync.sign(msg, priv),
|
|
||||||
noble: ({ msg, priv }) => ed25519.sign(msg, priv),
|
|
||||||
},
|
|
||||||
verify: {
|
|
||||||
samples: 1000,
|
|
||||||
old: ({ sig, msg, pub }) => noble_ed25519.sync.verify(sig, msg, pub),
|
|
||||||
noble: ({ sig, msg, pub }) => ed25519.verify(sig, msg, pub),
|
|
||||||
},
|
|
||||||
// hashToCurve: {
|
|
||||||
// samples: 500,
|
|
||||||
// noble: () => ed25519.Point.hashToCurve('abcd'),
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
ed448: {
|
|
||||||
data: () => {
|
|
||||||
const priv = ed448.utils.randomPrivateKey();
|
|
||||||
const pub = ed448.getPublicKey(priv);
|
|
||||||
const msg = ed448.utils.randomPrivateKey();
|
|
||||||
const sig = ed448.sign(msg, priv);
|
|
||||||
return { priv, pub, msg, sig };
|
|
||||||
},
|
|
||||||
getPublicKey: {
|
|
||||||
samples: 5000,
|
|
||||||
noble: () => ed448.getPublicKey(ed448.utils.randomPrivateKey()),
|
|
||||||
},
|
|
||||||
sign: {
|
|
||||||
samples: 2500,
|
|
||||||
noble: ({ msg, priv }) => ed448.sign(msg, priv),
|
|
||||||
},
|
|
||||||
verify: {
|
|
||||||
samples: 500,
|
|
||||||
noble: ({ sig, msg, pub }) => ed448.verify(sig, msg, pub),
|
|
||||||
},
|
|
||||||
// hashToCurve: {
|
|
||||||
// samples: 500,
|
|
||||||
// noble: () => ed448.Point.hashToCurve('abcd'),
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
nist: {
|
|
||||||
data: () => {
|
|
||||||
return { p256: generateData(P256), p384: generateData(P384), p521: generateData(P521) };
|
|
||||||
},
|
|
||||||
getPublicKey: {
|
|
||||||
samples: 2500,
|
|
||||||
P256: () => P256.getPublicKey(P256.utils.randomPrivateKey()),
|
|
||||||
P384: () => P384.getPublicKey(P384.utils.randomPrivateKey()),
|
|
||||||
P521: () => P521.getPublicKey(P521.utils.randomPrivateKey()),
|
|
||||||
},
|
|
||||||
sign: {
|
|
||||||
samples: 1000,
|
|
||||||
P256: ({ p256: { msg, priv } }) => P256.sign(msg, priv),
|
|
||||||
P384: ({ p384: { msg, priv } }) => P384.sign(msg, priv),
|
|
||||||
P521: ({ p521: { msg, priv } }) => P521.sign(msg, priv),
|
|
||||||
},
|
|
||||||
verify: {
|
|
||||||
samples: 250,
|
|
||||||
P256: ({ p256: { sig, msg, pub } }) => P256.verify(sig, msg, pub),
|
|
||||||
P384: ({ p384: { sig, msg, pub } }) => P384.verify(sig, msg, pub),
|
|
||||||
P521: ({ p521: { sig, msg, pub } }) => P521.verify(sig, msg, pub),
|
|
||||||
},
|
|
||||||
// hashToCurve: {
|
|
||||||
// samples: 500,
|
|
||||||
// P256: () => P256.Point.hashToCurve('abcd'),
|
|
||||||
// P384: () => P384.Point.hashToCurve('abcd'),
|
|
||||||
// P521: () => P521.Point.hashToCurve('abcd'),
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
stark: {
|
|
||||||
data: () => {
|
|
||||||
const priv = '2dccce1da22003777062ee0870e9881b460a8b7eca276870f57c601f182136c';
|
|
||||||
const msg = 'c465dd6b1bbffdb05442eb17f5ca38ad1aa78a6f56bf4415bdee219114a47';
|
|
||||||
const pub = stark.getPublicKey(priv);
|
|
||||||
const sig = stark.sign(msg, priv);
|
|
||||||
|
|
||||||
const privateKey = '2dccce1da22003777062ee0870e9881b460a8b7eca276870f57c601f182136c';
|
|
||||||
const msgHash = 'c465dd6b1bbffdb05442eb17f5ca38ad1aa78a6f56bf4415bdee219114a47';
|
|
||||||
const keyPair = starkwareCrypto.default.ec.keyFromPrivate(privateKey, 'hex');
|
|
||||||
const publicKeyStark = starkwareCrypto.default.ec.keyFromPublic(
|
|
||||||
keyPair.getPublic(true, 'hex'),
|
|
||||||
'hex'
|
|
||||||
);
|
|
||||||
|
|
||||||
return { priv, sig, msg, pub, publicKeyStark, msgHash, keyPair };
|
|
||||||
},
|
|
||||||
pedersen: {
|
|
||||||
samples: 500,
|
|
||||||
old: () => {
|
|
||||||
return starkwareCrypto.default.pedersen([
|
|
||||||
'3d937c035c878245caf64531a5756109c53068da139362728feb561405371cb',
|
|
||||||
'208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a',
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
noble: () => {
|
|
||||||
return stark.pedersen(
|
|
||||||
'3d937c035c878245caf64531a5756109c53068da139362728feb561405371cb',
|
|
||||||
'208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
poseidon: {
|
|
||||||
samples: 2000,
|
|
||||||
noble: () => {
|
|
||||||
return stark.poseidonHash(
|
|
||||||
0x3d937c035c878245caf64531a5756109c53068da139362728feb561405371cbn,
|
|
||||||
0x208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31an
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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.ProjectivePoint.BASE.multiply(
|
|
||||||
0x28b90deaf189015d3a325908c5e0e4bf00f84f7e639b056ff82d7e70b6eede4cn
|
|
||||||
);
|
|
||||||
p2 =
|
|
||||||
bls.G2.ProjectivePoint.BASE.multiply(
|
|
||||||
0x28b90deaf189015d3a325908c5e0e4bf00f84f7e639b056ff82d7e70b6eede4dn
|
|
||||||
);
|
|
||||||
bls.pairing(p1, p2);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'getPublicKey (1-bit)': {
|
|
||||||
samples: 1000,
|
|
||||||
old: () => old_bls.getPublicKey('2'.padStart(64, '0')),
|
|
||||||
noble: () => bls.getPublicKey('2'.padStart(64, '0')),
|
|
||||||
},
|
|
||||||
getPublicKey: {
|
|
||||||
samples: 1000,
|
|
||||||
old: ({ priv }) => old_bls.getPublicKey(priv),
|
|
||||||
noble: ({ priv }) => bls.getPublicKey(priv),
|
|
||||||
},
|
|
||||||
sign: {
|
|
||||||
samples: 50,
|
|
||||||
old: ({ priv }) => old_bls.sign('09', priv),
|
|
||||||
noble: ({ priv }) => bls.sign('09', priv),
|
|
||||||
},
|
|
||||||
verify: {
|
|
||||||
samples: 50,
|
|
||||||
old: ({ pub }) =>
|
|
||||||
old_bls.verify(
|
|
||||||
'8647aa9680cd0cdf065b94e818ff2bb948cc97838bcee987b9bc1b76d0a0a6e0d85db4e9d75aaedfc79d4ea2733a21ae0579014de7636dd2943d45b87c82b1c66a289006b0b9767921bb8edd3f6c5c5dec0d54cd65f61513113c50cc977849e5',
|
|
||||||
'09',
|
|
||||||
pub
|
|
||||||
),
|
|
||||||
noble: ({ pub }) =>
|
|
||||||
bls.verify(
|
|
||||||
'8647aa9680cd0cdf065b94e818ff2bb948cc97838bcee987b9bc1b76d0a0a6e0d85db4e9d75aaedfc79d4ea2733a21ae0579014de7636dd2943d45b87c82b1c66a289006b0b9767921bb8edd3f6c5c5dec0d54cd65f61513113c50cc977849e5',
|
|
||||||
'09',
|
|
||||||
pub
|
|
||||||
),
|
|
||||||
},
|
|
||||||
pairing: {
|
|
||||||
samples: 100,
|
|
||||||
old: () => old_bls.pairing(oldp1, oldp2),
|
|
||||||
noble: () => bls.pairing(p1, p2),
|
|
||||||
},
|
|
||||||
// 'hashToCurve/G1': {
|
|
||||||
// samples: 500,
|
|
||||||
// old: () => old_bls.PointG1.hashToCurve('abcd'),
|
|
||||||
// noble: () => bls.hashToCurve.G1.hashToCurve('abcd'),
|
|
||||||
// },
|
|
||||||
// 'hashToCurve/G2': {
|
|
||||||
// samples: 200,
|
|
||||||
// old: () => old_bls.PointG2.hashToCurve('abcd'),
|
|
||||||
// noble: () => bls.hashToCurve.G2.hashToCurve('abcd'),
|
|
||||||
// },
|
|
||||||
// SLOW PART
|
|
||||||
// Requires points which we cannot init before (data fn same for all)
|
|
||||||
// await mark('sign/nc', 30, () => bls.sign(msgp, priv));
|
|
||||||
// await mark('verify/nc', 30, () => bls.verify(sigp, msgp, pubp));
|
|
||||||
'aggregatePublicKeys/8': {
|
|
||||||
samples: 100,
|
|
||||||
old: ({ pubs }) => old_bls.aggregatePublicKeys(pubs.slice(0, 8)),
|
|
||||||
noble: ({ pubs }) => bls.aggregatePublicKeys(pubs.slice(0, 8)),
|
|
||||||
},
|
|
||||||
'aggregatePublicKeys/32': {
|
|
||||||
samples: 50,
|
|
||||||
old: ({ pub32 }) => old_bls.aggregatePublicKeys(pub32.map(old_bls.PointG1.fromHex)),
|
|
||||||
noble: ({ pub32 }) => bls.aggregatePublicKeys(pub32.map(bls.G1.ProjectivePoint.fromHex)),
|
|
||||||
},
|
|
||||||
'aggregatePublicKeys/128': {
|
|
||||||
samples: 20,
|
|
||||||
old: ({ pub128 }) => old_bls.aggregatePublicKeys(pub128.map(old_bls.PointG1.fromHex)),
|
|
||||||
noble: ({ pub128 }) => bls.aggregatePublicKeys(pub128.map(bls.G1.ProjectivePoint.fromHex)),
|
|
||||||
},
|
|
||||||
'aggregatePublicKeys/512': {
|
|
||||||
samples: 10,
|
|
||||||
old: ({ pub512 }) => old_bls.aggregatePublicKeys(pub512.map(old_bls.PointG1.fromHex)),
|
|
||||||
noble: ({ pub512 }) => bls.aggregatePublicKeys(pub512.map(bls.G1.ProjectivePoint.fromHex)),
|
|
||||||
},
|
|
||||||
'aggregatePublicKeys/2048': {
|
|
||||||
samples: 5,
|
|
||||||
old: ({ pub2048 }) => old_bls.aggregatePublicKeys(pub2048.map(old_bls.PointG1.fromHex)),
|
|
||||||
noble: ({ pub2048 }) => bls.aggregatePublicKeys(pub2048.map(bls.G1.ProjectivePoint.fromHex)),
|
|
||||||
},
|
|
||||||
'aggregateSignatures/8': {
|
|
||||||
samples: 50,
|
|
||||||
old: ({ sigs }) => old_bls.aggregateSignatures(sigs.slice(0, 8)),
|
|
||||||
noble: ({ sigs }) => bls.aggregateSignatures(sigs.slice(0, 8)),
|
|
||||||
},
|
|
||||||
'aggregateSignatures/32': {
|
|
||||||
samples: 10,
|
|
||||||
old: ({ sig32 }) => old_bls.aggregateSignatures(sig32.map(old_bls.PointG2.fromSignature)),
|
|
||||||
noble: ({ sig32 }) => bls.aggregateSignatures(sig32.map(bls.Signature.decode)),
|
|
||||||
},
|
|
||||||
'aggregateSignatures/128': {
|
|
||||||
samples: 5,
|
|
||||||
old: ({ sig128 }) => old_bls.aggregateSignatures(sig128.map(old_bls.PointG2.fromSignature)),
|
|
||||||
noble: ({ sig128 }) => bls.aggregateSignatures(sig128.map(bls.Signature.decode)),
|
|
||||||
},
|
|
||||||
'aggregateSignatures/512': {
|
|
||||||
samples: 3,
|
|
||||||
old: ({ sig512 }) => old_bls.aggregateSignatures(sig512.map(old_bls.PointG2.fromSignature)),
|
|
||||||
noble: ({ sig512 }) => bls.aggregateSignatures(sig512.map(bls.Signature.decode)),
|
|
||||||
},
|
|
||||||
'aggregateSignatures/2048': {
|
|
||||||
samples: 2,
|
|
||||||
old: ({ sig2048 }) => old_bls.aggregateSignatures(sig2048.map(old_bls.PointG2.fromSignature)),
|
|
||||||
noble: ({ sig2048 }) => bls.aggregateSignatures(sig2048.map(bls.Signature.decode)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const main = () =>
|
|
||||||
run(async () => {
|
|
||||||
for (const [name, curve] of Object.entries(CURVES)) {
|
|
||||||
console.log(`==== ${name} ====`);
|
|
||||||
const data = await curve.data();
|
|
||||||
for (const [fnName, libs] of Object.entries(curve)) {
|
|
||||||
if (fnName === 'data') continue;
|
|
||||||
const samples = libs.samples;
|
|
||||||
console.log(` - ${fnName} (samples: ${samples})`);
|
|
||||||
for (const [lib, fn] of Object.entries(libs)) {
|
|
||||||
if (lib === 'samples') continue;
|
|
||||||
if (ONLY_NOBLE && lib !== 'noble') continue;
|
|
||||||
await mark(` ${lib}`, samples, () => fn(data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Log current RAM
|
|
||||||
bench.logMem();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ESM is broken.
|
|
||||||
import url from 'url';
|
|
||||||
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
|
||||||
main();
|
|
||||||
}
|
|
||||||
@@ -12,15 +12,11 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"micro-bmark": "0.2.1"
|
"micro-bmark": "0.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/bls12-381": "^1.4.0",
|
|
||||||
"@noble/ed25519": "^1.7.1",
|
|
||||||
"@noble/hashes": "^1.1.5",
|
"@noble/hashes": "^1.1.5",
|
||||||
"@noble/secp256k1": "^1.7.0",
|
|
||||||
"@starkware-industries/starkware-crypto-utils": "^0.0.2",
|
"@starkware-industries/starkware-crypto-utils": "^0.0.2",
|
||||||
"calculate-correlation": "^1.2.3",
|
|
||||||
"elliptic": "^6.5.4"
|
"elliptic": "^6.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
benchmark/secp256k1.js
Normal file
22
benchmark/secp256k1.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { run, mark, utils } from 'micro-bmark';
|
||||||
|
import { secp256k1, schnorr } from '../lib/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();
|
||||||
|
});
|
||||||
56
benchmark/stark.js
Normal file
56
benchmark/stark.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { run, mark, compare, utils } from 'micro-bmark';
|
||||||
|
import * as starkwareCrypto from '@starkware-industries/starkware-crypto-utils';
|
||||||
|
import * as stark from '../lib/stark.js';
|
||||||
|
|
||||||
|
run(async () => {
|
||||||
|
const RAM = false;
|
||||||
|
if (RAM) utils.logMem();
|
||||||
|
console.log(`\x1b[36mstark\x1b[0m`);
|
||||||
|
await mark('init', 1, () => stark.utils.precompute(8));
|
||||||
|
const d = (() => {
|
||||||
|
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 };
|
||||||
|
})();
|
||||||
|
await compare('pedersen', 500, {
|
||||||
|
old: () => {
|
||||||
|
return starkwareCrypto.default.pedersen([
|
||||||
|
'3d937c035c878245caf64531a5756109c53068da139362728feb561405371cb',
|
||||||
|
'208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
noble: () => {
|
||||||
|
return stark.pedersen(
|
||||||
|
'3d937c035c878245caf64531a5756109c53068da139362728feb561405371cb',
|
||||||
|
'208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31a'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await mark('poseidon', 10000, () => stark.poseidonHash(
|
||||||
|
0x3d937c035c878245caf64531a5756109c53068da139362728feb561405371cbn,
|
||||||
|
0x208a0a10250e382e1e4bbe2880906c2791bf6275695e02fbbc6aeff9cd8b31an
|
||||||
|
));
|
||||||
|
await compare('verify', 500, {
|
||||||
|
old: () => {
|
||||||
|
return starkwareCrypto.default.verify(
|
||||||
|
d.publicKeyStark,
|
||||||
|
d.msgHash,
|
||||||
|
starkwareCrypto.default.sign(d.keyPair, d.msgHash)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
noble: () => {
|
||||||
|
return stark.verify(stark.sign(d.msg, d.priv), d.msg, d.pub);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (RAM) utils.logMem();
|
||||||
|
});
|
||||||
179
package-lock.json
generated
Normal file
179
package-lock.json
generated
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
{
|
||||||
|
"name": "@noble/curves",
|
||||||
|
"version": "0.6.2",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@noble/curves",
|
||||||
|
"version": "0.6.2",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@scure/base": "~1.1.1",
|
||||||
|
"@scure/bip32": "~1.1.5",
|
||||||
|
"@scure/bip39": "~1.1.1",
|
||||||
|
"@types/node": "18.11.3",
|
||||||
|
"fast-check": "3.0.0",
|
||||||
|
"micro-bmark": "0.3.0",
|
||||||
|
"micro-should": "0.4.0",
|
||||||
|
"prettier": "2.8.3",
|
||||||
|
"typescript": "4.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@noble/secp256k1": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==",
|
||||||
|
"dev": true,
|
||||||
|
"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.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "~1.2.0",
|
||||||
|
"@noble/secp256k1": "~1.7.0",
|
||||||
|
"@scure/base": "~1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scure/bip39": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "~1.2.0",
|
||||||
|
"@scure/base": "~1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "18.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.3.tgz",
|
||||||
|
"integrity": "sha512-fNjDQzzOsZeKZu5NATgXUPsaFaTxeRgFXoosrHivTl8RGeV733OLawXsGfEk9a8/tySyZUyiZ6E8LcjPFZ2y1A==",
|
||||||
|
"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.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/micro-bmark/-/micro-bmark-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-rYu+AtUq8lC3zPCoxkOOtwhgJoMpCDGe0/BXUCkj6+H9f/U/TunH/n/qkN98yh04dCCtDV8Aj9uYO3+DKxYrcw==",
|
||||||
|
"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.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz",
|
||||||
|
"integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==",
|
||||||
|
"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": "4.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz",
|
||||||
|
"integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
package.json
16
package.json
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@noble/curves",
|
"name": "@noble/curves",
|
||||||
"version": "0.6.0",
|
"version": "0.6.4",
|
||||||
"description": "Minimal, auditable JS implementation of elliptic curve cryptography",
|
"description": "Minimal, auditable JS implementation of elliptic curve cryptography",
|
||||||
"files": [
|
"files": [
|
||||||
"lib"
|
"lib"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bench": "cd benchmark; node index.js",
|
"bench": "cd benchmark; node secp256k1.js; node curves.js; node stark.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}' 'test/*.js'",
|
"lint": "prettier --check 'src/**/*.{js,ts}' 'test/*.js'",
|
||||||
@@ -21,19 +21,17 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "1.1.5"
|
"@noble/hashes": "1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-node-resolve": "13.3.0",
|
|
||||||
"@scure/base": "~1.1.1",
|
"@scure/base": "~1.1.1",
|
||||||
"@scure/bip32": "~1.1.1",
|
"@scure/bip32": "~1.1.5",
|
||||||
"@scure/bip39": "~1.1.0",
|
"@scure/bip39": "~1.1.1",
|
||||||
"@types/node": "18.11.3",
|
"@types/node": "18.11.3",
|
||||||
"fast-check": "3.0.0",
|
"fast-check": "3.0.0",
|
||||||
"micro-bmark": "0.2.0",
|
"micro-bmark": "0.3.0",
|
||||||
"micro-should": "0.3.0",
|
"micro-should": "0.4.0",
|
||||||
"prettier": "2.8.3",
|
"prettier": "2.8.3",
|
||||||
"rollup": "2.75.5",
|
|
||||||
"typescript": "4.7.3"
|
"typescript": "4.7.3"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
// Abelian group utilities
|
// Abelian group utilities
|
||||||
import { Field, validateField, nLength } from './modular.js';
|
import { Field, 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);
|
||||||
|
|
||||||
@@ -24,8 +25,17 @@ export type GroupConstructor<T> = {
|
|||||||
};
|
};
|
||||||
export type Mapper<T> = (i: T[]) => T[];
|
export type Mapper<T> = (i: T[]) => T[];
|
||||||
|
|
||||||
// Elliptic curve multiplication of Point by scalar. Complicated and fragile. Uses wNAF method.
|
// Elliptic curve multiplication of Point by scalar. Fragile.
|
||||||
// Windowed method is 10% faster, but takes 2x longer to generate & consumes 2x memory.
|
// 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();
|
||||||
@@ -53,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);
|
||||||
@@ -75,14 +89,14 @@ export function wNAF<T extends Group<T>>(c: GroupConstructor<T>, bits: number) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements w-ary non-adjacent form for calculating ec multiplication.
|
* Implements ec multiplication using precomputed tables and w-ary non-adjacent form.
|
||||||
* @param W window size
|
* @param W window size
|
||||||
* @param affinePoint optional 2d point to save cached precompute windows on it.
|
* @param precomputes precomputed tables
|
||||||
* @param n bits
|
* @param n scalar (we don't check here, but should be less than curve order)
|
||||||
* @returns real and fake (for const-time) points
|
* @returns real and fake (for const-time) points
|
||||||
*/
|
*/
|
||||||
wNAF(W: number, precomputes: T[], n: bigint): { p: T; f: T } {
|
wNAF(W: number, precomputes: T[], n: bigint): { p: T; f: T } {
|
||||||
// TODO: maybe check that scalar is less than group order? wNAF will fail otherwise
|
// TODO: maybe check that scalar is less than group order? wNAF behavious is undefined otherwise
|
||||||
// But need to carefully remove other checks before wNAF. ORDER == bits here
|
// But need to carefully remove other checks before wNAF. ORDER == bits here
|
||||||
const { windows, windowSize } = opts(W);
|
const { windows, windowSize } = opts(W);
|
||||||
|
|
||||||
@@ -153,7 +167,7 @@ export function wNAF<T extends Group<T>>(c: GroupConstructor<T>, bits: number) {
|
|||||||
|
|
||||||
// Generic BasicCurve interface: works even for polynomial fields (BLS): P, n, h would be ok.
|
// Generic BasicCurve interface: works even for polynomial fields (BLS): P, n, h would be ok.
|
||||||
// Though generator can be different (Fp2 / Fp6 for BLS).
|
// Though generator can be different (Fp2 / Fp6 for BLS).
|
||||||
export type AbstractCurve<T> = {
|
export type BasicCurve<T> = {
|
||||||
Fp: Field<T>; // Field over which we'll do calculations (Fp)
|
Fp: Field<T>; // Field over which we'll do calculations (Fp)
|
||||||
n: bigint; // Curve order, total count of valid points in the field
|
n: bigint; // Curve order, total count of valid points in the field
|
||||||
nBitLength?: number; // bit length of curve order
|
nBitLength?: number; // bit length of curve order
|
||||||
@@ -162,24 +176,24 @@ export type AbstractCurve<T> = {
|
|||||||
hEff?: bigint; // Number to multiply to clear cofactor
|
hEff?: bigint; // Number to multiply to clear cofactor
|
||||||
Gx: T; // base point X coordinate
|
Gx: T; // base point X coordinate
|
||||||
Gy: T; // base point Y coordinate
|
Gy: T; // base point Y coordinate
|
||||||
wrapPrivateKey?: boolean; // bls12-381 requires mod(n) instead of rejecting keys >= n
|
|
||||||
allowInfinityPoint?: boolean; // bls12-381 requires it. ZERO point is valid, but invalid pubkey
|
allowInfinityPoint?: boolean; // bls12-381 requires it. ZERO point is valid, but invalid pubkey
|
||||||
};
|
};
|
||||||
|
|
||||||
export function validateAbsOpts<FP, T>(curve: AbstractCurve<FP> & T) {
|
export function validateBasic<FP, T>(curve: BasicCurve<FP> & T) {
|
||||||
validateField(curve.Fp);
|
validateField(curve.Fp);
|
||||||
for (const i of ['n', 'h'] as const) {
|
validateObject(
|
||||||
const val = curve[i];
|
curve,
|
||||||
if (typeof val !== 'bigint') throw new Error(`Invalid curve param ${i}=${val} (${typeof val})`);
|
{
|
||||||
}
|
n: 'bigint',
|
||||||
if (!curve.Fp.isValid(curve.Gx)) throw new Error('Invalid generator X coordinate Fp element');
|
h: 'bigint',
|
||||||
if (!curve.Fp.isValid(curve.Gy)) throw new Error('Invalid generator Y coordinate Fp element');
|
Gx: 'field',
|
||||||
|
Gy: 'field',
|
||||||
for (const i of ['nBitLength', 'nByteLength'] as const) {
|
},
|
||||||
const val = curve[i];
|
{
|
||||||
if (val === undefined) continue; // Optional
|
nBitLength: 'isSafeInteger',
|
||||||
if (!Number.isSafeInteger(val)) throw new Error(`Invalid param ${i}=${val} (${typeof val})`);
|
nByteLength: 'isSafeInteger',
|
||||||
}
|
}
|
||||||
|
);
|
||||||
// Set defaults
|
// Set defaults
|
||||||
return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const);
|
return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
// Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y²
|
// Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y²
|
||||||
import { mod } from './modular.js';
|
import { mod } from './modular.js';
|
||||||
import {
|
import * as ut from './utils.js';
|
||||||
bytesToHex,
|
import { ensureBytes, FHash, Hex } from './utils.js';
|
||||||
bytesToNumberLE,
|
import { Group, GroupConstructor, wNAF, BasicCurve, validateBasic, AffinePoint } from './curve.js';
|
||||||
concatBytes,
|
|
||||||
ensureBytes,
|
|
||||||
FHash,
|
|
||||||
Hex,
|
|
||||||
numberToBytesLE,
|
|
||||||
} from './utils.js';
|
|
||||||
import {
|
|
||||||
Group,
|
|
||||||
GroupConstructor,
|
|
||||||
wNAF,
|
|
||||||
AbstractCurve,
|
|
||||||
validateAbsOpts,
|
|
||||||
AffinePoint,
|
|
||||||
} from './curve.js';
|
|
||||||
|
|
||||||
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
|
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
|
||||||
const _0n = BigInt(0);
|
const _0n = BigInt(0);
|
||||||
@@ -26,7 +12,7 @@ const _2n = BigInt(2);
|
|||||||
const _8n = BigInt(8);
|
const _8n = BigInt(8);
|
||||||
|
|
||||||
// Edwards curves must declare params a & d.
|
// Edwards curves must declare params a & d.
|
||||||
export type CurveType = AbstractCurve<bigint> & {
|
export type CurveType = BasicCurve<bigint> & {
|
||||||
a: bigint; // curve param a
|
a: bigint; // curve param a
|
||||||
d: bigint; // curve param d
|
d: bigint; // curve param d
|
||||||
hash: FHash; // Hashing
|
hash: FHash; // Hashing
|
||||||
@@ -39,19 +25,22 @@ export type CurveType = AbstractCurve<bigint> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function validateOpts(curve: CurveType) {
|
function validateOpts(curve: CurveType) {
|
||||||
const opts = validateAbsOpts(curve);
|
const opts = validateBasic(curve);
|
||||||
if (typeof opts.hash !== 'function') throw new Error('Invalid hash function');
|
ut.validateObject(
|
||||||
for (const i of ['a', 'd'] as const) {
|
curve,
|
||||||
const val = opts[i];
|
{
|
||||||
if (typeof val !== 'bigint') throw new Error(`Invalid curve param ${i}=${val} (${typeof val})`);
|
hash: 'function',
|
||||||
}
|
a: 'bigint',
|
||||||
for (const fn of ['randomBytes'] as const) {
|
d: 'bigint',
|
||||||
if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
randomBytes: 'function',
|
||||||
}
|
},
|
||||||
for (const fn of ['adjustScalarBytes', 'domain', 'uvRatio', 'mapToCurve'] as const) {
|
{
|
||||||
if (opts[fn] === undefined) continue; // Optional
|
adjustScalarBytes: 'function',
|
||||||
if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
domain: 'function',
|
||||||
|
uvRatio: 'function',
|
||||||
|
mapToCurve: 'function',
|
||||||
}
|
}
|
||||||
|
);
|
||||||
// Set defaults
|
// Set defaults
|
||||||
return Object.freeze({ ...opts } as const);
|
return Object.freeze({ ...opts } as const);
|
||||||
}
|
}
|
||||||
@@ -75,7 +64,7 @@ export interface ExtPointConstructor extends GroupConstructor<ExtPointType> {
|
|||||||
new (x: bigint, y: bigint, z: bigint, t: bigint): ExtPointType;
|
new (x: bigint, y: bigint, z: bigint, t: bigint): ExtPointType;
|
||||||
fromAffine(p: AffinePoint<bigint>): ExtPointType;
|
fromAffine(p: AffinePoint<bigint>): ExtPointType;
|
||||||
fromHex(hex: Hex): ExtPointType;
|
fromHex(hex: Hex): ExtPointType;
|
||||||
fromPrivateKey(privateKey: Hex): ExtPointType; // TODO: remove
|
fromPrivateKey(privateKey: Hex): ExtPointType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CurveFn = {
|
export type CurveFn = {
|
||||||
@@ -182,8 +171,27 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|||||||
this._WINDOW_SIZE = windowSize;
|
this._WINDOW_SIZE = windowSize;
|
||||||
pointPrecomputes.delete(this);
|
pointPrecomputes.delete(this);
|
||||||
}
|
}
|
||||||
|
// Not required for fromHex(), which always creates valid points.
|
||||||
assertValidity(): void {}
|
// Could be useful for fromAffine().
|
||||||
|
assertValidity(): void {
|
||||||
|
const { a, d } = CURVE;
|
||||||
|
if (this.is0()) throw new Error('bad point: ZERO'); // TODO: optimize, with vars below?
|
||||||
|
// Equation in affine coordinates: ax² + y² = 1 + dx²y²
|
||||||
|
// Equation in projective coordinates (X/Z, Y/Z, Z): (aX² + Y²)Z² = Z⁴ + dX²Y²
|
||||||
|
const { ex: X, ey: Y, ez: Z, et: T } = this;
|
||||||
|
const X2 = modP(X * X); // X²
|
||||||
|
const Y2 = modP(Y * Y); // Y²
|
||||||
|
const Z2 = modP(Z * Z); // Z²
|
||||||
|
const Z4 = modP(Z2 * Z2); // Z⁴
|
||||||
|
const aX2 = modP(X2 * a); // aX²
|
||||||
|
const left = modP(Z2 * modP(aX2 + Y2)); // (aX² + Y²)Z²
|
||||||
|
const right = modP(Z4 + modP(d * modP(X2 * Y2))); // Z⁴ + dX²Y²
|
||||||
|
if (left !== right) throw new Error('bad point: equation left != right (1)');
|
||||||
|
// In Extended coordinates we also have T, which is x*y=T/Z: check X*Y == Z*T
|
||||||
|
const XY = modP(X * Y);
|
||||||
|
const ZT = modP(Z * T);
|
||||||
|
if (XY !== ZT) throw new Error('bad point: equation left != right (2)');
|
||||||
|
}
|
||||||
|
|
||||||
// Compare one point to another.
|
// Compare one point to another.
|
||||||
equals(other: Point): boolean {
|
equals(other: Point): boolean {
|
||||||
@@ -340,7 +348,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|||||||
const normed = hex.slice(); // copy again, we'll manipulate it
|
const normed = hex.slice(); // copy again, we'll manipulate it
|
||||||
const lastByte = hex[len - 1]; // select last byte
|
const lastByte = hex[len - 1]; // select last byte
|
||||||
normed[len - 1] = lastByte & ~0x80; // clear last bit
|
normed[len - 1] = lastByte & ~0x80; // clear last bit
|
||||||
const y = bytesToNumberLE(normed);
|
const y = ut.bytesToNumberLE(normed);
|
||||||
if (y === _0n) {
|
if (y === _0n) {
|
||||||
// y=0 is allowed
|
// y=0 is allowed
|
||||||
} else {
|
} else {
|
||||||
@@ -366,12 +374,12 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|||||||
}
|
}
|
||||||
toRawBytes(): Uint8Array {
|
toRawBytes(): Uint8Array {
|
||||||
const { x, y } = this.toAffine();
|
const { x, y } = this.toAffine();
|
||||||
const bytes = numberToBytesLE(y, Fp.BYTES); // each y has 2 x values (x, -y)
|
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
|
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
|
return bytes; // and use the last byte to encode sign of x
|
||||||
}
|
}
|
||||||
toHex(): string {
|
toHex(): string {
|
||||||
return bytesToHex(this.toRawBytes()); // Same as toRawBytes, but returns string.
|
return ut.bytesToHex(this.toRawBytes()); // Same as toRawBytes, but returns string.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { BASE: G, ZERO: I } = Point;
|
const { BASE: G, ZERO: I } = Point;
|
||||||
@@ -382,7 +390,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|||||||
}
|
}
|
||||||
// Little-endian SHA512 with modulo n
|
// Little-endian SHA512 with modulo n
|
||||||
function modN_LE(hash: Uint8Array): bigint {
|
function modN_LE(hash: Uint8Array): bigint {
|
||||||
return modN(bytesToNumberLE(hash));
|
return modN(ut.bytesToNumberLE(hash));
|
||||||
}
|
}
|
||||||
function isHex(item: Hex, err: string) {
|
function isHex(item: Hex, err: string) {
|
||||||
if (typeof item !== 'string' && !(item instanceof Uint8Array))
|
if (typeof item !== 'string' && !(item instanceof Uint8Array))
|
||||||
@@ -411,7 +419,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|||||||
|
|
||||||
// int('LE', SHA512(dom2(F, C) || msgs)) mod N
|
// int('LE', SHA512(dom2(F, C) || msgs)) mod N
|
||||||
function hashDomainToScalar(context: Hex = new Uint8Array(), ...msgs: Uint8Array[]) {
|
function hashDomainToScalar(context: Hex = new Uint8Array(), ...msgs: Uint8Array[]) {
|
||||||
const msg = concatBytes(...msgs);
|
const msg = ut.concatBytes(...msgs);
|
||||||
return modN_LE(cHash(domain(msg, ensureBytes(context), !!preHash)));
|
return modN_LE(cHash(domain(msg, ensureBytes(context), !!preHash)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +434,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|||||||
const k = hashDomainToScalar(context, R, pointBytes, msg); // R || A || PH(M)
|
const k = hashDomainToScalar(context, R, pointBytes, msg); // R || A || PH(M)
|
||||||
const s = modN(r + k * scalar); // S = (r + k * s) mod L
|
const s = modN(r + k * scalar); // S = (r + k * s) mod L
|
||||||
assertGE0(s); // 0 <= s < l
|
assertGE0(s); // 0 <= s < l
|
||||||
const res = concatBytes(R, numberToBytesLE(s, Fp.BYTES));
|
const res = ut.concatBytes(R, ut.numberToBytesLE(s, Fp.BYTES));
|
||||||
return ensureBytes(res, nByteLength * 2); // 64-byte signature
|
return ensureBytes(res, nByteLength * 2); // 64-byte signature
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +447,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|||||||
if (preHash) msg = preHash(msg); // for ed25519ph, etc
|
if (preHash) msg = preHash(msg); // for ed25519ph, etc
|
||||||
const A = Point.fromHex(publicKey, false); // Check for s bounds, hex validity
|
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 R = Point.fromHex(sig.slice(0, len), false); // 0 <= R < 2^256: ZIP215 R can be >= P
|
||||||
const s = bytesToNumberLE(sig.slice(len, 2 * len)); // 0 <= s < l
|
const s = ut.bytesToNumberLE(sig.slice(len, 2 * len)); // 0 <= s < l
|
||||||
const SB = G.multiplyUnsafe(s);
|
const SB = G.multiplyUnsafe(s);
|
||||||
const k = hashDomainToScalar(context, R.toRawBytes(), A.toRawBytes(), msg);
|
const k = hashDomainToScalar(context, R.toRawBytes(), A.toRawBytes(), msg);
|
||||||
const RkA = R.add(A.multiplyUnsafe(k));
|
const RkA = R.add(A.multiplyUnsafe(k));
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
import type { Group, GroupConstructor, AffinePoint } from './curve.js';
|
import type { Group, GroupConstructor, AffinePoint } from './curve.js';
|
||||||
import { mod, Field } from './modular.js';
|
import { mod, Field } from './modular.js';
|
||||||
import { CHash, Hex, concatBytes, ensureBytes } from './utils.js';
|
import { CHash, Hex, concatBytes, ensureBytes, validateObject } from './utils.js';
|
||||||
|
|
||||||
export type Opts = {
|
export type Opts = {
|
||||||
// DST: a domain separation tag
|
DST: string; // DST: a domain separation tag, defined in section 2.2.5
|
||||||
// defined in section 2.2.5
|
|
||||||
DST: string;
|
|
||||||
encodeDST: string;
|
encodeDST: string;
|
||||||
// p: the characteristic of F
|
p: bigint; // characteristic of F, where F is a finite field of characteristic p and order q = p^m
|
||||||
// where F is a finite field of characteristic p and order q = p^m
|
m: number; // extension degree of F, m >= 1
|
||||||
p: bigint;
|
k: number; // k: the target security level for the suite in bits, defined in section 5.1
|
||||||
// m: the extension degree of F, m >= 1
|
expand?: 'xmd' | 'xof'; // use a message that has already been processed by expand_message_xmd
|
||||||
// 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?: 'xmd' | 'xof';
|
|
||||||
// Hash functions for: expand_message_xmd is appropriate for use with a
|
// 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.
|
// wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others.
|
||||||
// BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247
|
// BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247
|
||||||
@@ -27,17 +17,6 @@ export type Opts = {
|
|||||||
hash: CHash;
|
hash: CHash;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function validateOpts(opts: Opts) {
|
|
||||||
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 (opts.expand !== 'xmd' && opts.expand !== 'xof' && opts.expand !== undefined)
|
|
||||||
throw new Error('Invalid htf/expand');
|
|
||||||
if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen))
|
|
||||||
throw new Error('Invalid htf/hash function');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global symbols in both browsers and Node.js since v11
|
// Global symbols in both browsers and Node.js since v11
|
||||||
// See https://github.com/microsoft/TypeScript/issues/31535
|
// See https://github.com/microsoft/TypeScript/issues/31535
|
||||||
declare const TextEncoder: any;
|
declare const TextEncoder: any;
|
||||||
@@ -45,7 +24,7 @@ declare const TextDecoder: any;
|
|||||||
|
|
||||||
export function stringToBytes(str: string): Uint8Array {
|
export function stringToBytes(str: string): Uint8Array {
|
||||||
if (typeof str !== 'string') {
|
if (typeof str !== 'string') {
|
||||||
throw new TypeError(`utf8ToBytes expected string, got ${typeof str}`);
|
throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
|
||||||
}
|
}
|
||||||
return new TextEncoder().encode(str);
|
return new TextEncoder().encode(str);
|
||||||
}
|
}
|
||||||
@@ -195,20 +174,26 @@ export interface H2CPointConstructor<T> extends GroupConstructor<H2CPoint<T>> {
|
|||||||
|
|
||||||
export type MapToCurve<T> = (scalar: bigint[]) => AffinePoint<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!)
|
// Separated from initialization opts, so users won't accidentally change per-curve parameters
|
||||||
export type htfBasicOpts = {
|
// (changing DST is ok!)
|
||||||
DST: string;
|
export type htfBasicOpts = { DST: string };
|
||||||
};
|
|
||||||
|
|
||||||
export function hashToCurve<T>(
|
export function hashToCurve<T>(
|
||||||
Point: H2CPointConstructor<T>,
|
Point: H2CPointConstructor<T>,
|
||||||
mapToCurve: MapToCurve<T>,
|
mapToCurve: MapToCurve<T>,
|
||||||
def: Opts
|
def: Opts
|
||||||
) {
|
) {
|
||||||
validateOpts(def);
|
validateObject(def, {
|
||||||
|
DST: 'string',
|
||||||
|
p: 'bigint',
|
||||||
|
m: 'isSafeInteger',
|
||||||
|
k: 'isSafeInteger',
|
||||||
|
hash: 'hash',
|
||||||
|
});
|
||||||
|
if (def.expand !== 'xmd' && def.expand !== 'xof' && def.expand !== undefined)
|
||||||
|
throw new Error('Invalid htf/expand');
|
||||||
if (typeof mapToCurve !== 'function')
|
if (typeof mapToCurve !== 'function')
|
||||||
throw new Error('hashToCurve: mapToCurve() has not been defined');
|
throw new Error('hashToCurve: mapToCurve() has not been defined');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Encodes byte string to elliptic curve
|
// Encodes byte string to elliptic curve
|
||||||
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3
|
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
bytesToNumberBE,
|
bytesToNumberBE,
|
||||||
bytesToNumberLE,
|
bytesToNumberLE,
|
||||||
ensureBytes,
|
ensureBytes,
|
||||||
|
validateObject,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const _0n = BigInt(0), _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3);
|
const _0n = BigInt(0), _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3);
|
||||||
@@ -40,7 +41,6 @@ export function pow(num: bigint, power: bigint, modulo: bigint): bigint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Does x ^ (2 ^ power) mod p. pow2(30, 4) == 30 ^ (2 ^ 4)
|
// Does x ^ (2 ^ power) mod p. pow2(30, 4) == 30 ^ (2 ^ 4)
|
||||||
// TODO: Fp version?
|
|
||||||
export function pow2(x: bigint, power: bigint, modulo: bigint): bigint {
|
export function pow2(x: bigint, power: bigint, modulo: bigint): bigint {
|
||||||
let res = x;
|
let res = x;
|
||||||
while (power-- > _0n) {
|
while (power-- > _0n) {
|
||||||
@@ -249,18 +249,17 @@ const FIELD_FIELDS = [
|
|||||||
'addN', 'subN', 'mulN', 'sqrN'
|
'addN', 'subN', 'mulN', 'sqrN'
|
||||||
] as const;
|
] as const;
|
||||||
export function validateField<T>(field: Field<T>) {
|
export function validateField<T>(field: Field<T>) {
|
||||||
for (const i of ['ORDER', 'MASK'] as const) {
|
const initial = {
|
||||||
if (typeof field[i] !== 'bigint')
|
ORDER: 'bigint',
|
||||||
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`);
|
MASK: 'bigint',
|
||||||
}
|
BYTES: 'isSafeInteger',
|
||||||
for (const i of ['BYTES', 'BITS'] as const) {
|
BITS: 'isSafeInteger',
|
||||||
if (typeof field[i] !== 'number')
|
} as Record<string, string>;
|
||||||
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`);
|
const opts = FIELD_FIELDS.reduce((map, val: string) => {
|
||||||
}
|
map[val] = 'function';
|
||||||
for (const i of FIELD_FIELDS) {
|
return map;
|
||||||
if (typeof field[i] !== 'function')
|
}, initial);
|
||||||
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`);
|
return validateObject(field, opts);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic field functions
|
// Generic field functions
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
import { mod, pow } from './modular.js';
|
import { mod, pow } from './modular.js';
|
||||||
import { ensureBytes, numberToBytesLE, bytesToNumberLE } from './utils.js';
|
import { bytesToNumberLE, ensureBytes, numberToBytesLE, validateObject } from './utils.js';
|
||||||
|
|
||||||
const _0n = BigInt(0);
|
const _0n = BigInt(0);
|
||||||
const _1n = BigInt(1);
|
const _1n = BigInt(1);
|
||||||
type Hex = string | Uint8Array;
|
type Hex = string | Uint8Array;
|
||||||
|
|
||||||
export type CurveType = {
|
export type CurveType = {
|
||||||
// Field over which we'll do calculations. Verify with:
|
P: bigint; // finite field prime
|
||||||
P: bigint;
|
|
||||||
nByteLength: number;
|
nByteLength: number;
|
||||||
adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array;
|
adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array;
|
||||||
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
|
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
|
||||||
@@ -27,24 +26,20 @@ export type CurveFn = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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]})`);
|
{
|
||||||
}
|
a24: '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: 'string',
|
||||||
}
|
|
||||||
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
|
||||||
return Object.freeze({ ...curve } as const);
|
return Object.freeze({ ...curve } as const);
|
||||||
}
|
}
|
||||||
@@ -61,27 +56,7 @@ export function montgomery(curveDef: CurveType): CurveFn {
|
|||||||
const adjustScalarBytes = CURVE.adjustScalarBytes || ((bytes: Uint8Array) => bytes);
|
const adjustScalarBytes = CURVE.adjustScalarBytes || ((bytes: Uint8Array) => bytes);
|
||||||
const powPminus2 = CURVE.powPminus2 || ((x: bigint) => 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: 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)
|
||||||
@@ -98,6 +73,11 @@ export function montgomery(curveDef: CurveType): CurveFn {
|
|||||||
return [x_2, x_3];
|
return [x_2, x_3];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -106,11 +86,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
|
// The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519
|
||||||
const a24 = CURVE.a24;
|
const a24 = CURVE.a24;
|
||||||
const x_1 = u;
|
const x_1 = u;
|
||||||
@@ -166,28 +145,21 @@ export function montgomery(curveDef: CurveType): CurveFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function decodeUCoordinate(uEnc: Hex): bigint {
|
function decodeUCoordinate(uEnc: Hex): bigint {
|
||||||
const u = ensureBytes(uEnc, montgomeryBytes);
|
|
||||||
// Section 5: When receiving such an array, implementations of X25519
|
// Section 5: When receiving such an array, implementations of X25519
|
||||||
// MUST mask the most significant bit in the final byte.
|
// MUST mask the most significant bit in the final byte.
|
||||||
// This is very ugly way, but it works because fieldLen-1 is outside of bounds for X448, so this becomes NOOP
|
// This is very ugly way, but it works because fieldLen-1 is outside of bounds for X448, so this becomes NOOP
|
||||||
// fieldLen - scalaryBytes = 1 for X448 and = 0 for X25519
|
// fieldLen - scalaryBytes = 1 for X448 and = 0 for X25519
|
||||||
u[fieldLen - 1] &= 127; // 0b0111_1111
|
const u = ensureBytes(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(n);
|
||||||
if (bytes.length !== montgomeryBytes && bytes.length !== fieldLen)
|
if (bytes.length !== montgomeryBytes && bytes.length !== fieldLen)
|
||||||
throw new Error(`Expected ${montgomeryBytes} or ${fieldLen} bytes, got ${bytes.length}`);
|
throw new Error(`Expected ${montgomeryBytes} or ${fieldLen} bytes, got ${bytes.length}`);
|
||||||
return bytesToNumberLE(adjustScalarBytes(bytes));
|
return bytesToNumberLE(adjustScalarBytes(bytes));
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Computes shared secret between private key "scalar" and public key's "u" (x) coordinate.
|
|
||||||
* We can get 'y' coordinate from 'u',
|
|
||||||
* but Point.fromHex also wants 'x' coordinate oddity flag,
|
|
||||||
* and we cannot get 'x' without knowing 'v'.
|
|
||||||
* Need to add generic conversion between twisted edwards and complimentary curve for JubJub.
|
|
||||||
*/
|
|
||||||
function scalarMult(scalar: Hex, u: Hex): Uint8Array {
|
function scalarMult(scalar: Hex, u: Hex): Uint8Array {
|
||||||
const pointU = decodeUCoordinate(u);
|
const pointU = decodeUCoordinate(u);
|
||||||
const _scalar = decodeScalar(scalar);
|
const _scalar = decodeScalar(scalar);
|
||||||
@@ -197,12 +169,7 @@ export function montgomery(curveDef: CurveType): CurveFn {
|
|||||||
if (pu === _0n) throw new Error('Invalid private or public key received');
|
if (pu === _0n) throw new Error('Invalid private or public key received');
|
||||||
return encodeUCoordinate(pu);
|
return encodeUCoordinate(pu);
|
||||||
}
|
}
|
||||||
/**
|
// Computes public key from private. By doing scalar multiplication of base point.
|
||||||
* Computes public key from private.
|
|
||||||
* Executes scalar multiplication of curve's base point by scalar.
|
|
||||||
* @param scalar private key
|
|
||||||
* @returns new public key
|
|
||||||
*/
|
|
||||||
function scalarMultBase(scalar: Hex): Uint8Array {
|
function scalarMultBase(scalar: Hex): Uint8Array {
|
||||||
return scalarMult(scalar, CURVE.Gu);
|
return scalarMult(scalar, CURVE.Gu);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
// Poseidon Hash: https://eprint.iacr.org/2019/458.pdf, https://www.poseidon-hash.info
|
// Poseidon Hash: https://eprint.iacr.org/2019/458.pdf, https://www.poseidon-hash.info
|
||||||
import { Field, validateField, FpPow } from './modular.js';
|
import { Field, FpPow, validateField } from './modular.js';
|
||||||
// We don't provide any constants, since different implementations use different constants.
|
// We don't provide any constants, since different implementations use different constants.
|
||||||
// For reference constants see './test/poseidon.test.js'.
|
// For reference constants see './test/poseidon.test.js'.
|
||||||
export type PoseidonOpts = {
|
export type PoseidonOpts = {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export type FHash = (message: Uint8Array | string) => Uint8Array;
|
|||||||
|
|
||||||
const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0'));
|
const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0'));
|
||||||
export function bytesToHex(bytes: Uint8Array): string {
|
export function bytesToHex(bytes: Uint8Array): string {
|
||||||
if (!u8a(bytes)) throw new Error('Expected Uint8Array');
|
if (!u8a(bytes)) throw new Error('Uint8Array expected');
|
||||||
// pre-caching improves the speed 6x
|
// pre-caching improves the speed 6x
|
||||||
let hex = '';
|
let hex = '';
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
@@ -33,21 +33,21 @@ export function numberToHexUnpadded(num: number | bigint): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hexToNumber(hex: string): bigint {
|
export function hexToNumber(hex: string): bigint {
|
||||||
if (typeof hex !== 'string') throw new Error('hexToNumber: expected string, got ' + typeof hex);
|
if (typeof hex !== 'string') throw new Error('string expected, got ' + typeof hex);
|
||||||
// Big Endian
|
// Big Endian
|
||||||
return BigInt(`0x${hex}`);
|
return BigInt(hex === '' ? '0' : `0x${hex}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caching slows it down 2-3x
|
// Caching slows it down 2-3x
|
||||||
export function hexToBytes(hex: string): Uint8Array {
|
export function hexToBytes(hex: string): Uint8Array {
|
||||||
if (typeof hex !== 'string') throw new Error('hexToBytes: expected string, got ' + typeof hex);
|
if (typeof hex !== 'string') throw new Error('string expected, got ' + typeof hex);
|
||||||
if (hex.length % 2) throw new Error('hexToBytes: received invalid unpadded hex ' + hex.length);
|
if (hex.length % 2) throw new Error('hex string is invalid: unpadded ' + hex.length);
|
||||||
const array = new Uint8Array(hex.length / 2);
|
const array = new Uint8Array(hex.length / 2);
|
||||||
for (let i = 0; i < array.length; i++) {
|
for (let i = 0; i < array.length; i++) {
|
||||||
const j = i * 2;
|
const j = i * 2;
|
||||||
const hexByte = hex.slice(j, j + 2);
|
const hexByte = hex.slice(j, j + 2);
|
||||||
const byte = Number.parseInt(hexByte, 16);
|
const byte = Number.parseInt(hexByte, 16);
|
||||||
if (Number.isNaN(byte) || byte < 0) throw new Error('Invalid byte sequence');
|
if (Number.isNaN(byte) || byte < 0) throw new Error('invalid byte sequence');
|
||||||
array[i] = byte;
|
array[i] = byte;
|
||||||
}
|
}
|
||||||
return array;
|
return array;
|
||||||
@@ -58,7 +58,7 @@ export function bytesToNumberBE(bytes: Uint8Array): bigint {
|
|||||||
return hexToNumber(bytesToHex(bytes));
|
return hexToNumber(bytesToHex(bytes));
|
||||||
}
|
}
|
||||||
export function bytesToNumberLE(bytes: Uint8Array): bigint {
|
export function bytesToNumberLE(bytes: Uint8Array): bigint {
|
||||||
if (!u8a(bytes)) throw new Error('Expected Uint8Array');
|
if (!u8a(bytes)) throw new Error('Uint8Array expected');
|
||||||
return hexToNumber(bytesToHex(Uint8Array.from(bytes).reverse()));
|
return hexToNumber(bytesToHex(Uint8Array.from(bytes).reverse()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,11 +66,7 @@ export const numberToBytesBE = (n: bigint, len: number) =>
|
|||||||
hexToBytes(n.toString(16).padStart(len * 2, '0'));
|
hexToBytes(n.toString(16).padStart(len * 2, '0'));
|
||||||
export const numberToBytesLE = (n: bigint, len: number) => numberToBytesBE(n, len).reverse();
|
export const numberToBytesLE = (n: bigint, len: number) => numberToBytesBE(n, len).reverse();
|
||||||
// Returns variable number bytes (minimal bigint encoding?)
|
// Returns variable number bytes (minimal bigint encoding?)
|
||||||
export const numberToVarBytesBE = (n: bigint) => {
|
export const numberToVarBytesBE = (n: bigint) => hexToBytes(numberToHexUnpadded(n));
|
||||||
let hex = n.toString(16);
|
|
||||||
if (hex.length & 1) hex = '0' + hex;
|
|
||||||
return hexToBytes(hex);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ensureBytes(hex: Hex, expectedLength?: number): Uint8Array {
|
export function ensureBytes(hex: Hex, expectedLength?: number): Uint8Array {
|
||||||
// Uint8Array.from() instead of hash.slice() because node.js Buffer
|
// Uint8Array.from() instead of hash.slice() because node.js Buffer
|
||||||
@@ -82,17 +78,15 @@ export function ensureBytes(hex: Hex, expectedLength?: number): Uint8Array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copies several Uint8Arrays into one.
|
// Copies several Uint8Arrays into one.
|
||||||
export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
|
export function concatBytes(...arrs: Uint8Array[]): Uint8Array {
|
||||||
if (!arrays.every((b) => u8a(b))) throw new Error('Uint8Array list expected');
|
const r = new Uint8Array(arrs.reduce((sum, a) => sum + a.length, 0));
|
||||||
if (arrays.length === 1) return arrays[0];
|
let pad = 0; // walk through each item, ensure they have proper type
|
||||||
const length = arrays.reduce((a, arr) => a + arr.length, 0);
|
arrs.forEach((a) => {
|
||||||
const result = new Uint8Array(length);
|
if (!u8a(a)) throw new Error('Uint8Array expected');
|
||||||
for (let i = 0, pad = 0; i < arrays.length; i++) {
|
r.set(a, pad);
|
||||||
const arr = arrays[i];
|
pad += a.length;
|
||||||
result.set(arr, pad);
|
});
|
||||||
pad += arr.length;
|
return r;
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function equalBytes(b1: Uint8Array, b2: Uint8Array) {
|
export function equalBytes(b1: Uint8Array, b2: Uint8Array) {
|
||||||
@@ -119,3 +113,48 @@ export const bitSet = (n: bigint, pos: number, value: boolean) =>
|
|||||||
// Return mask for N bits (Same as BigInt(`0b${Array(i).fill('1').join('')}`))
|
// Return mask for N bits (Same as BigInt(`0b${Array(i).fill('1').join('')}`))
|
||||||
// Not using ** operator with bigints for old engines.
|
// Not using ** operator with bigints for old engines.
|
||||||
export const bitMask = (n: number) => (_2n << BigInt(n - 1)) - _1n;
|
export const bitMask = (n: number) => (_2n << BigInt(n - 1)) - _1n;
|
||||||
|
|
||||||
|
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' });
|
||||||
|
|||||||
@@ -2,15 +2,8 @@
|
|||||||
// Short Weierstrass curve. The formula is: y² = x³ + ax + b
|
// Short Weierstrass curve. The formula is: y² = x³ + ax + b
|
||||||
import * as mod from './modular.js';
|
import * as mod from './modular.js';
|
||||||
import * as ut from './utils.js';
|
import * as ut from './utils.js';
|
||||||
import { Hex, PrivKey, ensureBytes, CHash } from './utils.js';
|
import { CHash, Hex, PrivKey, ensureBytes } from './utils.js';
|
||||||
import {
|
import { Group, GroupConstructor, wNAF, BasicCurve, validateBasic, AffinePoint } from './curve.js';
|
||||||
Group,
|
|
||||||
GroupConstructor,
|
|
||||||
wNAF,
|
|
||||||
AbstractCurve,
|
|
||||||
validateAbsOpts,
|
|
||||||
AffinePoint,
|
|
||||||
} from './curve.js';
|
|
||||||
|
|
||||||
export type { AffinePoint };
|
export type { AffinePoint };
|
||||||
type HmacFnSync = (key: Uint8Array, ...messages: Uint8Array[]) => Uint8Array;
|
type HmacFnSync = (key: Uint8Array, ...messages: Uint8Array[]) => Uint8Array;
|
||||||
@@ -18,18 +11,15 @@ type EndomorphismOpts = {
|
|||||||
beta: bigint;
|
beta: bigint;
|
||||||
splitScalar: (k: bigint) => { k1neg: boolean; k1: bigint; k2neg: boolean; k2: bigint };
|
splitScalar: (k: bigint) => { k1neg: boolean; k1: bigint; k2neg: boolean; k2: bigint };
|
||||||
};
|
};
|
||||||
export type BasicCurve<T> = AbstractCurve<T> & {
|
export type BasicWCurve<T> = BasicCurve<T> & {
|
||||||
// Params: a, b
|
// Params: a, b
|
||||||
a: T;
|
a: T;
|
||||||
b: T;
|
b: T;
|
||||||
|
|
||||||
// Optional params
|
// Optional params
|
||||||
// Executed before privkey validation. Useful for P521 with var-length priv key
|
allowedPrivateKeyLengths?: readonly number[]; // for P521
|
||||||
normalizePrivateKey?: (key: PrivKey) => PrivKey;
|
wrapPrivateKey?: boolean; // bls12-381 requires mod(n) instead of rejecting keys >= n
|
||||||
// Whether to execute modular division on a private key, useful for bls curves with cofactor > 1
|
endo?: EndomorphismOpts; // Endomorphism options for Koblitz curves
|
||||||
wrapPrivateKey?: boolean;
|
|
||||||
// Endomorphism options for Koblitz curves
|
|
||||||
endo?: EndomorphismOpts;
|
|
||||||
// When a cofactor != 1, there can be an effective methods to:
|
// When a cofactor != 1, there can be an effective methods to:
|
||||||
// 1. Determine whether a point is torsion-free
|
// 1. Determine whether a point is torsion-free
|
||||||
isTorsionFree?: (c: ProjConstructor<T>, point: ProjPointType<T>) => boolean;
|
isTorsionFree?: (c: ProjConstructor<T>, point: ProjPointType<T>) => boolean;
|
||||||
@@ -89,26 +79,33 @@ export interface ProjConstructor<T> extends GroupConstructor<ProjPointType<T>> {
|
|||||||
normalizeZ(points: ProjPointType<T>[]): ProjPointType<T>[];
|
normalizeZ(points: ProjPointType<T>[]): ProjPointType<T>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CurvePointsType<T> = BasicCurve<T> & {
|
export type CurvePointsType<T> = BasicWCurve<T> & {
|
||||||
// Bytes
|
// Bytes
|
||||||
fromBytes: (bytes: Uint8Array) => AffinePoint<T>;
|
fromBytes: (bytes: Uint8Array) => AffinePoint<T>;
|
||||||
toBytes: (c: ProjConstructor<T>, point: ProjPointType<T>, compressed: boolean) => Uint8Array;
|
toBytes: (c: ProjConstructor<T>, point: ProjPointType<T>, compressed: boolean) => Uint8Array;
|
||||||
};
|
};
|
||||||
|
|
||||||
function validatePointOpts<T>(curve: CurvePointsType<T>) {
|
function validatePointOpts<T>(curve: CurvePointsType<T>) {
|
||||||
const opts = validateAbsOpts(curve);
|
const opts = validateBasic(curve);
|
||||||
const Fp = opts.Fp;
|
ut.validateObject(
|
||||||
for (const i of ['a', 'b'] as const) {
|
opts,
|
||||||
if (!Fp.isValid(curve[i]))
|
{
|
||||||
throw new Error(`Invalid curve param ${i}=${opts[i]} (${typeof opts[i]})`);
|
a: 'field',
|
||||||
|
b: 'field',
|
||||||
|
fromBytes: 'function',
|
||||||
|
toBytes: 'function',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
allowedPrivateKeyLengths: 'array',
|
||||||
|
wrapPrivateKey: 'boolean',
|
||||||
|
isTorsionFree: 'function',
|
||||||
|
clearCofactor: 'function',
|
||||||
|
allowInfinityPoint: 'boolean',
|
||||||
}
|
}
|
||||||
for (const i of ['isTorsionFree', 'clearCofactor'] as const) {
|
);
|
||||||
if (curve[i] === undefined) continue; // Optional
|
const { endo, Fp, a } = opts;
|
||||||
if (typeof curve[i] !== 'function') throw new Error(`Invalid ${i} function`);
|
|
||||||
}
|
|
||||||
const endo = opts.endo;
|
|
||||||
if (endo) {
|
if (endo) {
|
||||||
if (!Fp.eql(opts.a, Fp.ZERO)) {
|
if (!Fp.eql(a, Fp.ZERO)) {
|
||||||
throw new Error('Endomorphism can only be defined for Koblitz curves that have a=0');
|
throw new Error('Endomorphism can only be defined for Koblitz curves that have a=0');
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -119,9 +116,6 @@ function validatePointOpts<T>(curve: CurvePointsType<T>) {
|
|||||||
throw new Error('Expected endomorphism with beta: bigint and splitScalar: function');
|
throw new Error('Expected endomorphism with beta: bigint and splitScalar: function');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof opts.fromBytes !== 'function') throw new Error('Invalid fromBytes function');
|
|
||||||
if (typeof opts.toBytes !== 'function') throw new Error('Invalid fromBytes function');
|
|
||||||
// Set defaults
|
|
||||||
return Object.freeze({ ...opts } as const);
|
return Object.freeze({ ...opts } as const);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,32 +201,24 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
|
|||||||
function assertGE(num: bigint) {
|
function assertGE(num: bigint) {
|
||||||
if (!isWithinCurveOrder(num)) throw new Error('Expected valid bigint: 0 < bigint < curve.n');
|
if (!isWithinCurveOrder(num)) throw new Error('Expected valid bigint: 0 < bigint < curve.n');
|
||||||
}
|
}
|
||||||
/**
|
// Validates if priv key is valid and converts it to bigint.
|
||||||
* Validates if a private key is valid and converts it to bigint form.
|
// Supports options CURVE.normalizePrivateKey and CURVE.wrapPrivateKey.
|
||||||
* Supports two options, that are passed when CURVE is initialized:
|
|
||||||
* - `normalizePrivateKey()` executed before all checks
|
|
||||||
* - `wrapPrivateKey` when true, executed after most checks, but before `0 < key < n`
|
|
||||||
*/
|
|
||||||
function normalizePrivateKey(key: PrivKey): bigint {
|
function normalizePrivateKey(key: PrivKey): bigint {
|
||||||
const { normalizePrivateKey: custom, nByteLength: groupLen, wrapPrivateKey, n } = CURVE;
|
const { allowedPrivateKeyLengths: lengths, nByteLength, wrapPrivateKey, n } = CURVE;
|
||||||
if (typeof custom === 'function') key = custom(key);
|
if (lengths && typeof key !== 'bigint') {
|
||||||
let num: bigint;
|
if (key instanceof Uint8Array) key = ut.bytesToHex(key);
|
||||||
if (typeof key === 'bigint') {
|
// Normalize to hex string, pad. E.g. P521 would norm 130-132 char hex to 132-char bytes
|
||||||
// Curve order check is done below
|
if (typeof key !== 'string' || !lengths.includes(key.length)) throw new Error('Invalid key');
|
||||||
num = key;
|
key = key.padStart(nByteLength * 2, '0');
|
||||||
} else if (typeof key === 'string') {
|
|
||||||
if (key.length !== 2 * groupLen) throw new Error(`must be ${groupLen} bytes`);
|
|
||||||
// Validates individual octets
|
|
||||||
num = ut.bytesToNumberBE(ensureBytes(key));
|
|
||||||
} else if (key instanceof Uint8Array) {
|
|
||||||
if (key.length !== groupLen) throw new Error(`must be ${groupLen} bytes`);
|
|
||||||
num = ut.bytesToNumberBE(key);
|
|
||||||
} else {
|
|
||||||
throw new Error('private key must be bytes, hex or bigint, not ' + typeof key);
|
|
||||||
}
|
}
|
||||||
// Useful for curves with cofactor != 1
|
let num: bigint;
|
||||||
if (wrapPrivateKey) num = mod.mod(num, n);
|
try {
|
||||||
assertGE(num);
|
num = typeof key === 'bigint' ? key : ut.bytesToNumberBE(ensureBytes(key, nByteLength));
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`private key must be ${nByteLength} bytes, hex or bigint, not ${typeof key}`);
|
||||||
|
}
|
||||||
|
if (wrapPrivateKey) num = mod.mod(num, n); // disabled by default, enabled for BLS
|
||||||
|
assertGE(num); // num in range [1..N-1]
|
||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,6 +241,8 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
|
|||||||
if (pz == null || !Fp.isValid(pz)) throw new Error('z required');
|
if (pz == null || !Fp.isValid(pz)) throw new Error('z required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Does not validate if the point is on-curve.
|
||||||
|
// Use fromHex instead, or call assertValidity() later.
|
||||||
static fromAffine(p: AffinePoint<T>): Point {
|
static fromAffine(p: AffinePoint<T>): Point {
|
||||||
const { x, y } = p || {};
|
const { x, y } = p || {};
|
||||||
if (!p || !Fp.isValid(x) || !Fp.isValid(y)) throw new Error('invalid affine point');
|
if (!p || !Fp.isValid(x) || !Fp.isValid(y)) throw new Error('invalid affine point');
|
||||||
@@ -612,25 +600,30 @@ type SignatureLike = { r: bigint; s: bigint };
|
|||||||
|
|
||||||
export type PubKey = Hex | ProjPointType<bigint>;
|
export type PubKey = Hex | ProjPointType<bigint>;
|
||||||
|
|
||||||
export type CurveType = BasicCurve<bigint> & {
|
export type CurveType = BasicWCurve<bigint> & {
|
||||||
// Default options
|
hash: CHash; // CHash not FHash because we need outputLen for DRBG
|
||||||
lowS?: boolean;
|
|
||||||
// Hashes
|
|
||||||
hash: CHash; // Because we need outputLen for DRBG
|
|
||||||
hmac: HmacFnSync;
|
hmac: HmacFnSync;
|
||||||
randomBytes: (bytesLength?: number) => Uint8Array;
|
randomBytes: (bytesLength?: number) => Uint8Array;
|
||||||
// truncateHash?: (hash: Uint8Array, truncateOnly?: boolean) => Uint8Array;
|
lowS?: boolean;
|
||||||
bits2int?: (bytes: Uint8Array) => bigint;
|
bits2int?: (bytes: Uint8Array) => bigint;
|
||||||
bits2int_modN?: (bytes: Uint8Array) => bigint;
|
bits2int_modN?: (bytes: Uint8Array) => bigint;
|
||||||
};
|
};
|
||||||
|
|
||||||
function validateOpts(curve: CurveType) {
|
function validateOpts(curve: CurveType) {
|
||||||
const opts = validateAbsOpts(curve);
|
const opts = validateBasic(curve);
|
||||||
if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen))
|
ut.validateObject(
|
||||||
throw new Error('Invalid hash function');
|
opts,
|
||||||
if (typeof opts.hmac !== 'function') throw new Error('Invalid hmac function');
|
{
|
||||||
if (typeof opts.randomBytes !== 'function') throw new Error('Invalid randomBytes function');
|
hash: 'hash',
|
||||||
// Set defaults
|
hmac: 'function',
|
||||||
|
randomBytes: 'function',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bits2int: 'function',
|
||||||
|
bits2int_modN: 'function',
|
||||||
|
lowS: 'boolean',
|
||||||
|
}
|
||||||
|
);
|
||||||
return Object.freeze({ lowS: true, ...opts } as const);
|
return Object.freeze({ lowS: true, ...opts } as const);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,10 +636,11 @@ export type CurveFn = {
|
|||||||
ProjectivePoint: ProjConstructor<bigint>;
|
ProjectivePoint: ProjConstructor<bigint>;
|
||||||
Signature: SignatureConstructor;
|
Signature: SignatureConstructor;
|
||||||
utils: {
|
utils: {
|
||||||
_normalizePrivateKey: (key: PrivKey) => bigint;
|
normPrivateKeyToScalar: (key: PrivKey) => bigint;
|
||||||
isValidPrivateKey(privateKey: PrivKey): boolean;
|
isValidPrivateKey(privateKey: PrivKey): boolean;
|
||||||
hashToPrivateKey: (hash: Hex) => Uint8Array;
|
hashToPrivateKey: (hash: Hex) => Uint8Array;
|
||||||
randomPrivateKey: () => Uint8Array;
|
randomPrivateKey: () => Uint8Array;
|
||||||
|
precompute: (windowSize?: number, point?: ProjPointType<bigint>) => ProjPointType<bigint>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -759,7 +753,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
|||||||
return { x, y };
|
return { x, y };
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Point.fromHex: received invalid point. Expected ${compressedLen} compressed bytes or ${uncompressedLen} uncompressed bytes, not ${len}`
|
`Point of length ${len} was invalid. Expected ${compressedLen} compressed bytes or ${uncompressedLen} uncompressed bytes`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -865,7 +859,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_normalizePrivateKey: normalizePrivateKey,
|
normPrivateKeyToScalar: normalizePrivateKey,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts some bytes to a valid private key. Needs at least (nBitLength+64) bytes.
|
* Converts some bytes to a valid private key. Needs at least (nBitLength+64) bytes.
|
||||||
@@ -954,9 +948,10 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
|||||||
// NOTE: pads output with zero as per spec
|
// NOTE: pads output with zero as per spec
|
||||||
const ORDER_MASK = ut.bitMask(CURVE.nBitLength);
|
const ORDER_MASK = ut.bitMask(CURVE.nBitLength);
|
||||||
function int2octets(num: bigint): Uint8Array {
|
function int2octets(num: bigint): Uint8Array {
|
||||||
if (typeof num !== 'bigint') throw new Error('Expected bigint');
|
if (typeof num !== 'bigint') throw new Error('bigint expected');
|
||||||
if (!(_0n <= num && num < ORDER_MASK))
|
if (!(_0n <= num && num < ORDER_MASK))
|
||||||
throw new Error(`Expected number < 2^${CURVE.nBitLength}`);
|
// n in [0..ORDER_MASK-1]
|
||||||
|
throw new Error(`bigint expected < 2^${CURVE.nBitLength}`);
|
||||||
// works with order, can have different size than numToField!
|
// works with order, can have different size than numToField!
|
||||||
return ut.numberToBytesBE(num, CURVE.nByteLength);
|
return ut.numberToBytesBE(num, CURVE.nByteLength);
|
||||||
}
|
}
|
||||||
@@ -967,32 +962,26 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
|||||||
// NOTE: we cannot assume here that msgHash has same amount of bytes as curve order, this will be wrong at least for P521.
|
// NOTE: we cannot assume here that msgHash has same amount of bytes as curve order, this will be wrong at least for P521.
|
||||||
// Also it can be bigger for P224 + SHA256
|
// Also it can be bigger for P224 + SHA256
|
||||||
function prepSig(msgHash: Hex, privateKey: PrivKey, opts = defaultSigOpts) {
|
function prepSig(msgHash: Hex, privateKey: PrivKey, opts = defaultSigOpts) {
|
||||||
|
const { hash, randomBytes } = CURVE;
|
||||||
if (msgHash == null) throw new Error(`sign: expected valid message hash, not "${msgHash}"`);
|
if (msgHash == null) throw new Error(`sign: expected valid message hash, not "${msgHash}"`);
|
||||||
if (['recovered', 'canonical'].some((k) => k in opts))
|
if (['recovered', 'canonical'].some((k) => k in opts))
|
||||||
// Ban legacy options
|
// Ban legacy options
|
||||||
throw new Error('sign() legacy options not supported');
|
throw new Error('sign() legacy options not supported');
|
||||||
let { lowS, prehash, extraEntropy: ent } = opts; // generates low-s sigs by default
|
let { lowS, prehash, extraEntropy: ent } = opts; // generates low-s sigs by default
|
||||||
if (prehash) msgHash = CURVE.hash(ensureBytes(msgHash));
|
if (prehash) msgHash = hash(ensureBytes(msgHash));
|
||||||
if (lowS == null) lowS = true; // RFC6979 3.2: we skip step A, because
|
if (lowS == null) lowS = true; // RFC6979 3.2: we skip step A, because we already provide hash
|
||||||
// Step A is ignored, since we already provide hash instead of msg
|
|
||||||
|
|
||||||
// NOTE: instead of bits2int, we calling here truncateHash, since we need
|
// We can't later call bits2octets, since nested bits2int is broken for curves
|
||||||
// custom truncation for stark. For other curves it is essentially same as calling bits2int + mod
|
// with nBitLength % 8 !== 0. Because of that, we unwrap it here as int2octets call.
|
||||||
// However, we cannot later call bits2octets (which is truncateHash + int2octets), since nested bits2int is broken
|
// const bits2octets = (bits) => int2octets(bits2int_modN(bits))
|
||||||
// for curves where nBitLength % 8 !== 0, so we unwrap it here as int2octets call.
|
|
||||||
// const bits2octets = (bits)=>int2octets(bytesToNumberBE(truncateHash(bits)))
|
|
||||||
const h1int = bits2int_modN(ensureBytes(msgHash));
|
const h1int = bits2int_modN(ensureBytes(msgHash));
|
||||||
const h1octets = int2octets(h1int);
|
const d = normalizePrivateKey(privateKey); // validate private key, convert to bigint
|
||||||
|
const seedArgs = [int2octets(d), int2octets(h1int)];
|
||||||
const d = normalizePrivateKey(privateKey);
|
// extraEntropy. RFC6979 3.6: additional k' (optional).
|
||||||
// K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k')
|
|
||||||
const seedArgs = [int2octets(d), h1octets];
|
|
||||||
if (ent != null) {
|
if (ent != null) {
|
||||||
// RFC6979 3.6: additional k' (optional)
|
// K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k')
|
||||||
if (ent === true) ent = CURVE.randomBytes(Fp.BYTES);
|
// Either pass as-is, or generate random bytes. Then validate for being ui8a of size BYTES
|
||||||
const e = ensureBytes(ent);
|
seedArgs.push(ensureBytes(ent === true ? randomBytes(Fp.BYTES) : ent, Fp.BYTES));
|
||||||
if (e.length !== Fp.BYTES) throw new Error(`sign: Expected ${Fp.BYTES} bytes of extra data`);
|
|
||||||
seedArgs.push(e);
|
|
||||||
}
|
}
|
||||||
const seed = ut.concatBytes(...seedArgs); // Step D of RFC6979 3.2
|
const seed = ut.concatBytes(...seedArgs); // Step D of RFC6979 3.2
|
||||||
const m = h1int; // NOTE: no need to call bits2int second time here, it is inside truncateHash!
|
const m = h1int; // NOTE: no need to call bits2int second time here, it is inside truncateHash!
|
||||||
@@ -1005,7 +994,16 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
|||||||
const q = Point.BASE.multiply(k).toAffine(); // q = Gk
|
const q = Point.BASE.multiply(k).toAffine(); // q = Gk
|
||||||
const r = modN(q.x); // r = q.x mod n
|
const r = modN(q.x); // r = q.x mod n
|
||||||
if (r === _0n) return;
|
if (r === _0n) return;
|
||||||
const s = modN(ik * modN(m + modN(d * r))); // s = k^-1(m + rd) mod n
|
// X blinding according to https://tches.iacr.org/index.php/TCHES/article/view/7337/6509
|
||||||
|
// b * m + b * r * d ∈ [0,q−1] exposed via side-channel, but d (private scalar) is not.
|
||||||
|
// NOTE: there is still probable some leak in multiplication, since it is not constant-time
|
||||||
|
const b = ut.bytesToNumberBE(utils.randomPrivateKey()); // random scalar, b ∈ [1,q−1]
|
||||||
|
const bi = invN(b); // b^-1
|
||||||
|
const bdr = modN(b * d * r); // b * d * r
|
||||||
|
const bm = modN(b * m); // b * m
|
||||||
|
const mrx = modN(bi * modN(bdr + bm)); // b^-1(bm + bdr) -> m + rd
|
||||||
|
|
||||||
|
const s = modN(ik * mrx); // s = k^-1(m + rd) mod n
|
||||||
if (s === _0n) return;
|
if (s === _0n) return;
|
||||||
let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n); // recovery bit (2 or 3, when q.x > n)
|
let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n); // recovery bit (2 or 3, when q.x > n)
|
||||||
let normS = s;
|
let normS = s;
|
||||||
@@ -1054,7 +1052,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
function verify(
|
function verify(
|
||||||
signature: Hex | { r: bigint; s: bigint },
|
signature: Hex | SignatureLike,
|
||||||
msgHash: Hex,
|
msgHash: Hex,
|
||||||
publicKey: Hex,
|
publicKey: Hex,
|
||||||
opts = defaultVerOpts
|
opts = defaultVerOpts
|
||||||
@@ -1099,7 +1097,6 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
|||||||
getSharedSecret,
|
getSharedSecret,
|
||||||
sign,
|
sign,
|
||||||
verify,
|
verify,
|
||||||
// Point,
|
|
||||||
ProjectivePoint: Point,
|
ProjectivePoint: Point,
|
||||||
Signature,
|
Signature,
|
||||||
utils,
|
utils,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const P224 = createCurve(
|
|||||||
// Params: a, b
|
// Params: a, b
|
||||||
a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffffffffffe'),
|
a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffffffffffe'),
|
||||||
b: BigInt('0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4'),
|
b: BigInt('0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4'),
|
||||||
// Field over which we'll do calculations; 2n**224n - 2n**96n + 1n
|
// Field over which we'll do calculations;
|
||||||
Fp: Fp(BigInt('0xffffffffffffffffffffffffffffffff000000000000000000000001')),
|
Fp: Fp(BigInt('0xffffffffffffffffffffffffffffffff000000000000000000000001')),
|
||||||
// Curve order, total count of valid points in the field
|
// Curve order, total count of valid points in the field
|
||||||
n: BigInt('0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d'),
|
n: BigInt('0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d'),
|
||||||
|
|||||||
12
src/p521.ts
12
src/p521.ts
@@ -1,7 +1,6 @@
|
|||||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
import { createCurve } from './_shortw_utils.js';
|
import { createCurve } from './_shortw_utils.js';
|
||||||
import { sha512 } from '@noble/hashes/sha512';
|
import { sha512 } from '@noble/hashes/sha512';
|
||||||
import { bytesToHex, PrivKey } from './abstract/utils.js';
|
|
||||||
import { Fp as Field } from './abstract/modular.js';
|
import { Fp as Field } from './abstract/modular.js';
|
||||||
import { mapToCurveSimpleSWU } from './abstract/weierstrass.js';
|
import { mapToCurveSimpleSWU } from './abstract/weierstrass.js';
|
||||||
import * as htf from './abstract/hash-to-curve.js';
|
import * as htf from './abstract/hash-to-curve.js';
|
||||||
@@ -38,16 +37,7 @@ export const P521 = createCurve({
|
|||||||
Gy: BigInt('0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650'),
|
Gy: BigInt('0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650'),
|
||||||
h: BigInt(1),
|
h: BigInt(1),
|
||||||
lowS: false,
|
lowS: false,
|
||||||
// P521 keys could be 130, 131, 132 bytes. We normalize to 132 bytes.
|
allowedPrivateKeyLengths: [130, 131, 132] // P521 keys are variable-length. Normalize to 132b
|
||||||
// Does not replace validation; invalid keys would still be rejected.
|
|
||||||
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'); // ensure it's always 132 bytes
|
|
||||||
},
|
|
||||||
} as const, sha512);
|
} as const, sha512);
|
||||||
export const secp521r1 = P521;
|
export const secp521r1 = P521;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
ensureBytes,
|
ensureBytes,
|
||||||
concatBytes,
|
concatBytes,
|
||||||
Hex,
|
Hex,
|
||||||
bytesToNumberBE as bytesToNum,
|
bytesToNumberBE as bytesToInt,
|
||||||
PrivKey,
|
PrivKey,
|
||||||
numberToBytesBE,
|
numberToBytesBE,
|
||||||
} from './abstract/utils.js';
|
} from './abstract/utils.js';
|
||||||
@@ -130,57 +130,53 @@ function taggedHash(tag: string, ...messages: Uint8Array[]): Uint8Array {
|
|||||||
return sha256(concatBytes(tagP, ...messages));
|
return sha256(concatBytes(tagP, ...messages));
|
||||||
}
|
}
|
||||||
|
|
||||||
const toRawX = (point: PointType<bigint>) => point.toRawBytes(true).slice(1);
|
const pointToBytes = (point: PointType<bigint>) => point.toRawBytes(true).slice(1);
|
||||||
const numTo32b = (n: bigint) => numberToBytesBE(n, 32);
|
const numTo32b = (n: bigint) => numberToBytesBE(n, 32);
|
||||||
const modN = (x: bigint) => mod(x, secp256k1N);
|
const modN = (x: bigint) => mod(x, secp256k1N);
|
||||||
const _Point = secp256k1.ProjectivePoint;
|
const Point = secp256k1.ProjectivePoint;
|
||||||
const Gmul = (priv: PrivKey) => _Point.fromPrivateKey(priv);
|
|
||||||
const GmulAdd = (Q: PointType<bigint>, a: bigint, b: bigint) =>
|
const GmulAdd = (Q: PointType<bigint>, a: bigint, b: bigint) =>
|
||||||
_Point.BASE.multiplyAndAddUnsafe(Q, a, b);
|
Point.BASE.multiplyAndAddUnsafe(Q, a, b);
|
||||||
function schnorrGetScalar(priv: bigint) {
|
const hex32ToInt = (key: Hex) => bytesToInt(ensureBytes(key, 32));
|
||||||
// Let d' = int(sk)
|
function schnorrGetExtPubKey(priv: PrivKey) {
|
||||||
// Fail if d' = 0 or d' ≥ n
|
let d = typeof priv === 'bigint' ? priv : hex32ToInt(priv);
|
||||||
// Let P = d'⋅G
|
const point = Point.fromPrivateKey(d); // P = d'⋅G; 0 < d' < n check is done inside
|
||||||
// Let d = d' if has_even_y(P), otherwise let d = n - d' .
|
const scalar = point.hasEvenY() ? d : modN(-d); // d = d' if has_even_y(P), otherwise d = n-d'
|
||||||
const point = Gmul(priv);
|
return { point, scalar, bytes: pointToBytes(point) };
|
||||||
const scalar = point.hasEvenY() ? priv : modN(-priv);
|
|
||||||
return { point, scalar, x: toRawX(point) };
|
|
||||||
}
|
}
|
||||||
function lift_x(x: bigint): PointType<bigint> {
|
function lift_x(x: bigint): PointType<bigint> {
|
||||||
if (!fe(x)) throw new Error('bad x: need 0 < x < p'); // Fail if x ≥ p.
|
if (!fe(x)) throw new Error('bad x: need 0 < x < p'); // Fail if x ≥ p.
|
||||||
const c = mod(x * x * x + BigInt(7), secp256k1P); // Let c = x³ + 7 mod p.
|
const c = mod(x * x * x + BigInt(7), secp256k1P); // Let c = x³ + 7 mod p.
|
||||||
let y = sqrtMod(c); // Let y = c^(p+1)/4 mod p.
|
let y = sqrtMod(c); // Let y = c^(p+1)/4 mod p.
|
||||||
if (y % 2n !== 0n) y = mod(-y, secp256k1P); // Return the unique point P such that x(P) = x and
|
if (y % 2n !== 0n) y = mod(-y, secp256k1P); // 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.
|
const p = new Point(x, y, _1n); // y(P) = y if y mod 2 = 0 or y(P) = p-y otherwise.
|
||||||
p.assertValidity();
|
p.assertValidity();
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
function challenge(...args: Uint8Array[]): bigint {
|
function challenge(...args: Uint8Array[]): bigint {
|
||||||
return modN(bytesToNum(taggedHash(TAGS.challenge, ...args)));
|
return modN(bytesToInt(taggedHash(TAGS.challenge, ...args)));
|
||||||
}
|
}
|
||||||
function schnorrGetPublicKey(privateKey: PrivKey): Uint8Array {
|
|
||||||
return toRawX(Gmul(privateKey)); // Let d' = int(sk). Fail if d' = 0 or d' ≥ n. Return bytes(d'⋅G)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 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: Hex, auxRand: Hex = randomBytes(32)): Uint8Array {
|
|
||||||
if (message == null) throw new Error(`sign: Expected valid message, not "${message}"`);
|
|
||||||
const m = ensureBytes(message);
|
|
||||||
// checks for isWithinCurveOrder
|
|
||||||
|
|
||||||
const { x: px, scalar: d } = schnorrGetScalar(bytesToNum(ensureBytes(privateKey, 32)));
|
// Schnorr's pubkey is just `x` of Point (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 {
|
||||||
|
if (message == null) throw new Error(`sign: Expected valid message, not "${message}"`);
|
||||||
|
const m = ensureBytes(message); // checks for isWithinCurveOrder
|
||||||
|
const { bytes: px, scalar: d } = schnorrGetExtPubKey(privateKey);
|
||||||
const a = ensureBytes(auxRand, 32); // Auxiliary random data a: a 32-byte array
|
const a = ensureBytes(auxRand, 32); // Auxiliary random data a: a 32-byte array
|
||||||
// TODO: replace with proper xor?
|
const t = numTo32b(d ^ bytesToInt(taggedHash(TAGS.aux, a))); // Let t be the byte-wise xor of bytes(d) and hash/aux(a)
|
||||||
const t = numTo32b(d ^ bytesToNum(taggedHash(TAGS.aux, a))); // Let t be the byte-wise xor of bytes(d) and hash/aux(a)
|
|
||||||
const rand = taggedHash(TAGS.nonce, t, px, m); // Let rand = hash/nonce(t || bytes(P) || m)
|
const rand = taggedHash(TAGS.nonce, t, px, m); // Let rand = hash/nonce(t || bytes(P) || m)
|
||||||
const k_ = modN(bytesToNum(rand)); // Let k' = int(rand) mod n
|
const k_ = modN(bytesToInt(rand)); // Let k' = int(rand) mod n
|
||||||
if (k_ === _0n) throw new Error('sign failed: k is zero'); // Fail if k' = 0.
|
if (k_ === _0n) throw new Error('sign failed: k is zero'); // Fail if k' = 0.
|
||||||
const { point: R, x: rx, scalar: k } = schnorrGetScalar(k_); // Let R = k'⋅G.
|
const { point: R, 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 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).
|
const sig = new Uint8Array(64); // Let sig = bytes(R) || bytes((k + ed) mod n).
|
||||||
sig.set(numTo32b(R.px), 0);
|
sig.set(numTo32b(R.px), 0);
|
||||||
@@ -195,14 +191,14 @@ function schnorrSign(message: Hex, privateKey: Hex, auxRand: Hex = randomBytes(3
|
|||||||
*/
|
*/
|
||||||
function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): boolean {
|
function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): boolean {
|
||||||
try {
|
try {
|
||||||
const P = lift_x(bytesToNum(ensureBytes(publicKey, 32))); // P = lift_x(int(pk)); fail if that fails
|
const P = lift_x(hex32ToInt(publicKey)); // P = lift_x(int(pk)); fail if that fails
|
||||||
const sig = ensureBytes(signature, 64);
|
const sig = ensureBytes(signature, 64);
|
||||||
const r = bytesToNum(sig.subarray(0, 32)); // Let r = int(sig[0:32]); fail if r ≥ p.
|
const r = bytesToInt(sig.subarray(0, 32)); // Let r = int(sig[0:32]); fail if r ≥ p.
|
||||||
if (!fe(r)) return false;
|
if (!fe(r)) return false;
|
||||||
const s = bytesToNum(sig.subarray(32, 64)); // Let s = int(sig[32:64]); fail if s ≥ n.
|
const s = bytesToInt(sig.subarray(32, 64)); // Let s = int(sig[32:64]); fail if s ≥ n.
|
||||||
if (!ge(s)) return false;
|
if (!ge(s)) return false;
|
||||||
const m = ensureBytes(message);
|
const m = ensureBytes(message);
|
||||||
const e = challenge(numTo32b(r), toRawX(P), m); // int(challenge(bytes(r)||bytes(P)||m)) mod n
|
const e = challenge(numTo32b(r), pointToBytes(P), m); // int(challenge(bytes(r)||bytes(P)||m)) mod n
|
||||||
const R = GmulAdd(P, s, modN(-e)); // R = s⋅G - e⋅P
|
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
|
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.
|
return true; // Fail if is_infinite(R) / not has_even_y(R) / x(R) ≠ r.
|
||||||
@@ -212,11 +208,18 @@ function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const schnorr = {
|
export const schnorr = {
|
||||||
// Schnorr's pubkey is just `x` of Point (BIP340)
|
|
||||||
getPublicKey: schnorrGetPublicKey,
|
getPublicKey: schnorrGetPublicKey,
|
||||||
sign: schnorrSign,
|
sign: schnorrSign,
|
||||||
verify: schnorrVerify,
|
verify: schnorrVerify,
|
||||||
utils: { lift_x, int: bytesToNum, taggedHash },
|
utils: {
|
||||||
|
getExtendedPublicKey: schnorrGetExtPubKey,
|
||||||
|
lift_x,
|
||||||
|
pointToBytes,
|
||||||
|
numberToBytesBE,
|
||||||
|
bytesToNumberBE: bytesToInt,
|
||||||
|
taggedHash,
|
||||||
|
mod,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const isoMap = htf.isogenyMap(
|
const isoMap = htf.isogenyMap(
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ function ensureBytes0x(hex: Hex): Uint8Array {
|
|||||||
function normalizePrivateKey(privKey: Hex) {
|
function normalizePrivateKey(privKey: Hex) {
|
||||||
return cutils.bytesToHex(ensureBytes0x(privKey)).padStart(64, '0');
|
return cutils.bytesToHex(ensureBytes0x(privKey)).padStart(64, '0');
|
||||||
}
|
}
|
||||||
function getPublicKey0x(privKey: Hex, isCompressed?: boolean) {
|
function getPublicKey0x(privKey: Hex, isCompressed = false) {
|
||||||
return starkCurve.getPublicKey(normalizePrivateKey(privKey), isCompressed);
|
return starkCurve.getPublicKey(normalizePrivateKey(privKey), isCompressed);
|
||||||
}
|
}
|
||||||
function getSharedSecret0x(privKeyA: Hex, pubKeyB: Hex) {
|
function getSharedSecret0x(privKeyA: Hex, pubKeyB: Hex) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { secp256r1 } from '../lib/esm/p256.js';
|
|||||||
import { secp384r1 } from '../lib/esm/p384.js';
|
import { secp384r1 } from '../lib/esm/p384.js';
|
||||||
import { secp521r1 } from '../lib/esm/p521.js';
|
import { secp521r1 } from '../lib/esm/p521.js';
|
||||||
import { secp256k1 } from '../lib/esm/secp256k1.js';
|
import { secp256k1 } from '../lib/esm/secp256k1.js';
|
||||||
import { ed25519, ed25519ctx, ed25519ph } from '../lib/esm/ed25519.js';
|
import { ed25519, ed25519ctx, ed25519ph, x25519 } from '../lib/esm/ed25519.js';
|
||||||
import { ed448, ed448ph } from '../lib/esm/ed448.js';
|
import { ed448, ed448ph } from '../lib/esm/ed448.js';
|
||||||
import { starkCurve } from '../lib/esm/stark.js';
|
import { starkCurve } from '../lib/esm/stark.js';
|
||||||
import { pallas, vesta } from '../lib/esm/pasta.js';
|
import { pallas, vesta } from '../lib/esm/pasta.js';
|
||||||
@@ -239,6 +239,11 @@ for (const c in FIELDS) {
|
|||||||
deepStrictEqual(isSquare(a), true);
|
deepStrictEqual(isSquare(a), true);
|
||||||
deepStrictEqual(Fp.eql(Fp.sqr(root), a), true, 'sqrt(a)^2 == a');
|
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');
|
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);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -261,6 +266,9 @@ for (const c in FIELDS) {
|
|||||||
if (Fp.eql(a, Fp.ZERO)) return; // No division by zero
|
if (Fp.eql(a, Fp.ZERO)) return; // No division by zero
|
||||||
deepStrictEqual(Fp.div(a, Fp.ONE), a);
|
deepStrictEqual(Fp.div(a, Fp.ONE), a);
|
||||||
deepStrictEqual(Fp.div(a, a), Fp.ONE);
|
deepStrictEqual(Fp.div(a, a), Fp.ONE);
|
||||||
|
// FpDiv tests
|
||||||
|
deepStrictEqual(mod.FpDiv(Fp, a, Fp.ONE), a);
|
||||||
|
deepStrictEqual(mod.FpDiv(Fp, a, a), Fp.ONE);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -269,6 +277,7 @@ for (const c in FIELDS) {
|
|||||||
fc.property(FC_BIGINT, (num) => {
|
fc.property(FC_BIGINT, (num) => {
|
||||||
const a = create(num);
|
const a = create(num);
|
||||||
deepStrictEqual(Fp.div(Fp.ZERO, a), Fp.ZERO);
|
deepStrictEqual(Fp.div(Fp.ZERO, a), Fp.ZERO);
|
||||||
|
deepStrictEqual(mod.FpDiv(Fp, Fp.ZERO, a), Fp.ZERO);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -279,6 +288,10 @@ for (const c in FIELDS) {
|
|||||||
const b = create(num2);
|
const b = create(num2);
|
||||||
const c = create(num3);
|
const c = create(num3);
|
||||||
deepStrictEqual(Fp.div(Fp.add(a, b), c), Fp.add(Fp.div(a, c), Fp.div(b, c)));
|
deepStrictEqual(Fp.div(Fp.add(a, b), c), Fp.add(Fp.div(a, c), Fp.div(b, c)));
|
||||||
|
deepStrictEqual(
|
||||||
|
mod.FpDiv(Fp, Fp.add(a, b), c),
|
||||||
|
Fp.add(mod.FpDiv(Fp, a, c), mod.FpDiv(Fp, b, c))
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -436,9 +449,16 @@ for (const name in CURVES) {
|
|||||||
throws(() => G[1][op](0n), '0n');
|
throws(() => G[1][op](0n), '0n');
|
||||||
G[1][op](G[2]);
|
G[1][op](G[2]);
|
||||||
throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER');
|
throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER');
|
||||||
|
throws(() => G[1][op](-123n), '-123n');
|
||||||
|
throws(() => G[1][op](123), '123');
|
||||||
throws(() => G[1][op](123.456), '123.456');
|
throws(() => G[1][op](123.456), '123.456');
|
||||||
throws(() => G[1][op](true), 'true');
|
throws(() => G[1][op](true), 'true');
|
||||||
|
throws(() => G[1][op](false), 'false');
|
||||||
|
throws(() => G[1][op](null), 'null');
|
||||||
|
throws(() => G[1][op](undefined), 'undefined');
|
||||||
throws(() => G[1][op]('1'), "'1'");
|
throws(() => G[1][op]('1'), "'1'");
|
||||||
|
throws(() => G[1][op]({ x: 1n, y: 1n }), '{ x: 1n, y: 1n }');
|
||||||
|
throws(() => G[1][op]({ x: 1n, y: 1n, z: 1n }), '{ x: 1n, y: 1n, z: 1n }');
|
||||||
throws(
|
throws(
|
||||||
() => G[1][op]({ x: 1n, y: 1n, z: 1n, t: 1n }),
|
() => G[1][op]({ x: 1n, y: 1n, z: 1n, t: 1n }),
|
||||||
'{ x: 1n, y: 1n, z: 1n, t: 1n }'
|
'{ x: 1n, y: 1n, z: 1n, t: 1n }'
|
||||||
@@ -514,8 +534,22 @@ for (const name in CURVES) {
|
|||||||
should('fromHex(toHex()) roundtrip', () => {
|
should('fromHex(toHex()) roundtrip', () => {
|
||||||
fc.assert(
|
fc.assert(
|
||||||
fc.property(FC_BIGINT, (x) => {
|
fc.property(FC_BIGINT, (x) => {
|
||||||
const hex = p.BASE.multiply(x).toHex();
|
const point = p.BASE.multiply(x);
|
||||||
|
const hex = point.toHex();
|
||||||
|
const bytes = point.toRawBytes();
|
||||||
deepStrictEqual(p.fromHex(hex).toHex(), hex);
|
deepStrictEqual(p.fromHex(hex).toHex(), hex);
|
||||||
|
deepStrictEqual(p.fromHex(bytes).toHex(), hex);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
should('fromHex(toHex(compressed=true)) roundtrip', () => {
|
||||||
|
fc.assert(
|
||||||
|
fc.property(FC_BIGINT, (x) => {
|
||||||
|
const point = p.BASE.multiply(x);
|
||||||
|
const hex = point.toHex(true);
|
||||||
|
const bytes = point.toRawBytes(true);
|
||||||
|
deepStrictEqual(p.fromHex(hex).toHex(true), hex);
|
||||||
|
deepStrictEqual(p.fromHex(bytes).toHex(true), hex);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -527,10 +561,13 @@ for (const name in CURVES) {
|
|||||||
should('.getPublicKey() type check', () => {
|
should('.getPublicKey() type check', () => {
|
||||||
throws(() => C.getPublicKey(0), '0');
|
throws(() => C.getPublicKey(0), '0');
|
||||||
throws(() => C.getPublicKey(0n), '0n');
|
throws(() => C.getPublicKey(0n), '0n');
|
||||||
throws(() => C.getPublicKey(false), 'false');
|
throws(() => C.getPublicKey(-123n), '-123n');
|
||||||
throws(() => C.getPublicKey(123), '123');
|
throws(() => C.getPublicKey(123), '123');
|
||||||
throws(() => C.getPublicKey(123.456), '123.456');
|
throws(() => C.getPublicKey(123.456), '123.456');
|
||||||
throws(() => C.getPublicKey(true), 'true');
|
throws(() => C.getPublicKey(true), 'true');
|
||||||
|
throws(() => C.getPublicKey(false), 'false');
|
||||||
|
throws(() => C.getPublicKey(null), 'null');
|
||||||
|
throws(() => C.getPublicKey(undefined), 'undefined');
|
||||||
throws(() => C.getPublicKey(''), "''");
|
throws(() => C.getPublicKey(''), "''");
|
||||||
// NOTE: passes because of disabled hex padding checks for starknet, maybe enable?
|
// NOTE: passes because of disabled hex padding checks for starknet, maybe enable?
|
||||||
// throws(() => C.getPublicKey('1'), "'1'");
|
// throws(() => C.getPublicKey('1'), "'1'");
|
||||||
@@ -551,39 +588,96 @@ for (const name in CURVES) {
|
|||||||
deepStrictEqual(
|
deepStrictEqual(
|
||||||
C.verify(sig, msg, pub),
|
C.verify(sig, msg, pub),
|
||||||
true,
|
true,
|
||||||
'priv=${toHex(priv)},pub=${toHex(pub)},msg=${msg}'
|
`priv=${toHex(priv)},pub=${toHex(pub)},msg=${msg}`
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
{ numRuns: NUM_RUNS }
|
{ numRuns: NUM_RUNS }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
should('.verify() should verify empty signatures', () => {
|
||||||
|
const msg = new Uint8Array([]);
|
||||||
|
const priv = C.utils.randomPrivateKey();
|
||||||
|
const pub = C.getPublicKey(priv);
|
||||||
|
const sig = C.sign(msg, priv);
|
||||||
|
deepStrictEqual(
|
||||||
|
C.verify(sig, msg, pub),
|
||||||
|
true,
|
||||||
|
'priv=${toHex(priv)},pub=${toHex(pub)},msg=${msg}'
|
||||||
|
);
|
||||||
|
});
|
||||||
should('.sign() edge cases', () => {
|
should('.sign() edge cases', () => {
|
||||||
throws(() => C.sign());
|
throws(() => C.sign());
|
||||||
throws(() => C.sign(''));
|
throws(() => C.sign(''));
|
||||||
|
throws(() => C.sign('', ''));
|
||||||
|
throws(() => C.sign(new Uint8Array(), new Uint8Array()));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('verify()', () => {
|
describe('verify()', () => {
|
||||||
should('true for proper signatures', () => {
|
|
||||||
const msg = '01'.repeat(32);
|
const msg = '01'.repeat(32);
|
||||||
|
should('true for proper signatures', () => {
|
||||||
const priv = C.utils.randomPrivateKey();
|
const priv = C.utils.randomPrivateKey();
|
||||||
const sig = C.sign(msg, priv);
|
const sig = C.sign(msg, priv);
|
||||||
const pub = C.getPublicKey(priv);
|
const pub = C.getPublicKey(priv);
|
||||||
deepStrictEqual(C.verify(sig, msg, pub), true);
|
deepStrictEqual(C.verify(sig, msg, pub), true);
|
||||||
});
|
});
|
||||||
should('false for wrong messages', () => {
|
should('false for wrong messages', () => {
|
||||||
const msg = '01'.repeat(32);
|
|
||||||
const priv = C.utils.randomPrivateKey();
|
const priv = C.utils.randomPrivateKey();
|
||||||
const sig = C.sign(msg, priv);
|
const sig = C.sign(msg, priv);
|
||||||
const pub = C.getPublicKey(priv);
|
const pub = C.getPublicKey(priv);
|
||||||
deepStrictEqual(C.verify(sig, '11'.repeat(32), pub), false);
|
deepStrictEqual(C.verify(sig, '11'.repeat(32), pub), false);
|
||||||
});
|
});
|
||||||
should('false for wrong keys', () => {
|
should('false for wrong keys', () => {
|
||||||
const msg = '01'.repeat(32);
|
|
||||||
const priv = C.utils.randomPrivateKey();
|
const priv = C.utils.randomPrivateKey();
|
||||||
const sig = C.sign(msg, priv);
|
const sig = C.sign(msg, priv);
|
||||||
deepStrictEqual(C.verify(sig, msg, C.getPublicKey(C.utils.randomPrivateKey())), false);
|
deepStrictEqual(C.verify(sig, msg, C.getPublicKey(C.utils.randomPrivateKey())), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
if (C.Signature) {
|
||||||
|
should('Signature serialization roundtrip', () =>
|
||||||
|
fc.assert(
|
||||||
|
fc.property(fc.hexaString({ minLength: 64, maxLength: 64 }), (msg) => {
|
||||||
|
const priv = C.utils.randomPrivateKey();
|
||||||
|
const sig = C.sign(msg, priv);
|
||||||
|
const sigRS = (sig) => ({ s: sig.s, r: sig.r });
|
||||||
|
// Compact
|
||||||
|
deepStrictEqual(sigRS(C.Signature.fromCompact(sig.toCompactHex())), sigRS(sig));
|
||||||
|
deepStrictEqual(sigRS(C.Signature.fromCompact(sig.toCompactRawBytes())), sigRS(sig));
|
||||||
|
// DER
|
||||||
|
deepStrictEqual(sigRS(C.Signature.fromDER(sig.toDERHex())), sigRS(sig));
|
||||||
|
deepStrictEqual(sigRS(C.Signature.fromDER(sig.toDERRawBytes())), sigRS(sig));
|
||||||
|
}),
|
||||||
|
{ numRuns: NUM_RUNS }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
should('Signature.addRecoveryBit/Signature.recoveryPublicKey', () =>
|
||||||
|
fc.assert(
|
||||||
|
fc.property(fc.hexaString({ minLength: 64, maxLength: 64 }), (msg) => {
|
||||||
|
const priv = C.utils.randomPrivateKey();
|
||||||
|
const pub = C.getPublicKey(priv);
|
||||||
|
const sig = C.sign(msg, priv);
|
||||||
|
deepStrictEqual(sig.recoverPublicKey(msg).toRawBytes(), pub);
|
||||||
|
const sig2 = C.Signature.fromCompact(sig.toCompactHex());
|
||||||
|
throws(() => sig2.recoverPublicKey(msg));
|
||||||
|
const sig3 = sig2.addRecoveryBit(sig.recovery);
|
||||||
|
deepStrictEqual(sig3.recoverPublicKey(msg).toRawBytes(), pub);
|
||||||
|
}),
|
||||||
|
{ numRuns: NUM_RUNS }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
should('Signature.normalizeS', () =>
|
||||||
|
fc.assert(
|
||||||
|
fc.property(fc.hexaString({ minLength: 64, maxLength: 64 }), (msg) => {
|
||||||
|
const priv = C.utils.randomPrivateKey();
|
||||||
|
const pub = C.getPublicKey(priv);
|
||||||
|
const sig = C.sign(msg, priv);
|
||||||
|
const sig2 = sig.normalizeS();
|
||||||
|
deepStrictEqual(sig2.hasHighS(), false);
|
||||||
|
}),
|
||||||
|
{ numRuns: NUM_RUNS }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: fails for ed, because of empty message. Since we convert it to scalar,
|
// NOTE: fails for ed, because of empty message. Since we convert it to scalar,
|
||||||
// need to check what other implementations do. Empty message != new Uint8Array([0]), but what scalar should be in that case?
|
// need to check what other implementations do. Empty message != new Uint8Array([0]), but what scalar should be in that case?
|
||||||
// should('should not verify signature with wrong message', () => {
|
// should('should not verify signature with wrong message', () => {
|
||||||
@@ -641,6 +735,16 @@ should('secp224k1 sqrt bug', () => {
|
|||||||
deepStrictEqual(Fp.sqr(sqrtMinus1), Fp.create(-1n));
|
deepStrictEqual(Fp.sqr(sqrtMinus1), Fp.create(-1n));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
should('bigInt private keys', () => {
|
||||||
|
// Doesn't support bigints anymore
|
||||||
|
throws(() => ed25519.sign('', 123n));
|
||||||
|
throws(() => ed25519.getPublicKey(123n));
|
||||||
|
throws(() => x25519.getPublicKey(123n));
|
||||||
|
// Weierstrass still supports
|
||||||
|
secp256k1.getPublicKey(123n);
|
||||||
|
secp256k1.sign('', 123n);
|
||||||
|
});
|
||||||
|
|
||||||
// ESM is broken.
|
// ESM is broken.
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
||||||
|
|||||||
290
test/ed25519-addons.test.js
Normal file
290
test/ed25519-addons.test.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
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 { numberToBytesLE } from '../lib/esm/abstract/utils.js';
|
||||||
|
import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' };
|
||||||
|
import { ed25519ctx, ed25519ph, RistrettoPoint, x25519 } from '../lib/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(hex(numberToBytesLE(u, 32)), x25519.Gu);
|
||||||
|
});
|
||||||
|
|
||||||
|
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.Gu;
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ESM is broken.
|
||||||
|
import url from 'url';
|
||||||
|
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 '../lib/esm/ed25519.js';
|
||||||
@@ -1,21 +1,11 @@
|
|||||||
import { deepEqual, deepStrictEqual, strictEqual, throws } from 'assert';
|
import { deepStrictEqual, strictEqual, throws } from 'assert';
|
||||||
import { describe, should } from 'micro-should';
|
|
||||||
import * as fc from 'fast-check';
|
|
||||||
import {
|
|
||||||
ed25519,
|
|
||||||
ed25519ctx,
|
|
||||||
ed25519ph,
|
|
||||||
x25519,
|
|
||||||
RistrettoPoint,
|
|
||||||
ED25519_TORSION_SUBGROUP,
|
|
||||||
} from '../lib/esm/ed25519.js';
|
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
|
|
||||||
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
||||||
import { numberToBytesLE } from '../lib/esm/abstract/utils.js';
|
import * as fc from 'fast-check';
|
||||||
import { sha512 } from '@noble/hashes/sha512';
|
import { describe, should } from 'micro-should';
|
||||||
|
import { ed25519, ED25519_TORSION_SUBGROUP } from './ed25519.helpers.js';
|
||||||
import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' };
|
import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' };
|
||||||
import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' };
|
import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
|
||||||
|
|
||||||
describe('ed25519', () => {
|
describe('ed25519', () => {
|
||||||
const ed = ed25519;
|
const ed = ed25519;
|
||||||
@@ -292,104 +282,6 @@ describe('ed25519', () => {
|
|||||||
// // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY);
|
// // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY);
|
||||||
// // expect(await ristretto25519.verify(signature, WRONG_MESSAGE, publicKey)).toBe(false);
|
// // expect(await ristretto25519.verify(signature, WRONG_MESSAGE, publicKey)).toBe(false);
|
||||||
// // });
|
// // });
|
||||||
should('ristretto255/should follow the byte encodings of small multiples', () => {
|
|
||||||
const encodingsOfSmallMultiples = [
|
|
||||||
// This is the identity point
|
|
||||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
|
||||||
// This is the basepoint
|
|
||||||
'e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76',
|
|
||||||
// These are small multiples of the basepoint
|
|
||||||
'6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919',
|
|
||||||
'94741f5d5d52755ece4f23f044ee27d5d1ea1e2bd196b462166b16152a9d0259',
|
|
||||||
'da80862773358b466ffadfe0b3293ab3d9fd53c5ea6c955358f568322daf6a57',
|
|
||||||
'e882b131016b52c1d3337080187cf768423efccbb517bb495ab812c4160ff44e',
|
|
||||||
'f64746d3c92b13050ed8d80236a7f0007c3b3f962f5ba793d19a601ebb1df403',
|
|
||||||
'44f53520926ec81fbd5a387845beb7df85a96a24ece18738bdcfa6a7822a176d',
|
|
||||||
'903293d8f2287ebe10e2374dc1a53e0bc887e592699f02d077d5263cdd55601c',
|
|
||||||
'02622ace8f7303a31cafc63f8fc48fdc16e1c8c8d234b2f0d6685282a9076031',
|
|
||||||
'20706fd788b2720a1ed2a5dad4952b01f413bcf0e7564de8cdc816689e2db95f',
|
|
||||||
'bce83f8ba5dd2fa572864c24ba1810f9522bc6004afe95877ac73241cafdab42',
|
|
||||||
'e4549ee16b9aa03099ca208c67adafcafa4c3f3e4e5303de6026e3ca8ff84460',
|
|
||||||
'aa52e000df2e16f55fb1032fc33bc42742dad6bd5a8fc0be0167436c5948501f',
|
|
||||||
'46376b80f409b29dc2b5f6f0c52591990896e5716f41477cd30085ab7f10301e',
|
|
||||||
'e0c418f7c8d9c4cdd7395b93ea124f3ad99021bb681dfc3302a9d99a2e53e64e',
|
|
||||||
];
|
|
||||||
let B = RistrettoPoint.BASE;
|
|
||||||
let P = RistrettoPoint.ZERO;
|
|
||||||
for (const encoded of encodingsOfSmallMultiples) {
|
|
||||||
deepStrictEqual(P.toHex(), encoded);
|
|
||||||
deepStrictEqual(RistrettoPoint.fromHex(encoded).toHex(), encoded);
|
|
||||||
P = P.add(B);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('ristretto255/should not convert bad bytes encoding', () => {
|
|
||||||
const badEncodings = [
|
|
||||||
// These are all bad because they're non-canonical field encodings.
|
|
||||||
'00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
|
||||||
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
||||||
'f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
||||||
'edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
||||||
// These are all bad because they're negative field elements.
|
|
||||||
'0100000000000000000000000000000000000000000000000000000000000000',
|
|
||||||
'01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
||||||
'ed57ffd8c914fb201471d1c3d245ce3c746fcbe63a3679d51b6a516ebebe0e20',
|
|
||||||
'c34c4e1826e5d403b78e246e88aa051c36ccf0aafebffe137d148a2bf9104562',
|
|
||||||
'c940e5a4404157cfb1628b108db051a8d439e1a421394ec4ebccb9ec92a8ac78',
|
|
||||||
'47cfc5497c53dc8e61c91d17fd626ffb1c49e2bca94eed052281b510b1117a24',
|
|
||||||
'f1c6165d33367351b0da8f6e4511010c68174a03b6581212c71c0e1d026c3c72',
|
|
||||||
'87260f7a2f12495118360f02c26a470f450dadf34a413d21042b43b9d93e1309',
|
|
||||||
// These are all bad because they give a nonsquare x².
|
|
||||||
'26948d35ca62e643e26a83177332e6b6afeb9d08e4268b650f1f5bbd8d81d371',
|
|
||||||
'4eac077a713c57b4f4397629a4145982c661f48044dd3f96427d40b147d9742f',
|
|
||||||
'de6a7b00deadc788eb6b6c8d20c0ae96c2f2019078fa604fee5b87d6e989ad7b',
|
|
||||||
'bcab477be20861e01e4a0e295284146a510150d9817763caf1a6f4b422d67042',
|
|
||||||
'2a292df7e32cababbd9de088d1d1abec9fc0440f637ed2fba145094dc14bea08',
|
|
||||||
'f4a9e534fc0d216c44b218fa0c42d99635a0127ee2e53c712f70609649fdff22',
|
|
||||||
'8268436f8c4126196cf64b3c7ddbda90746a378625f9813dd9b8457077256731',
|
|
||||||
'2810e5cbc2cc4d4eece54f61c6f69758e289aa7ab440b3cbeaa21995c2f4232b',
|
|
||||||
// These are all bad because they give a negative xy value.
|
|
||||||
'3eb858e78f5a7254d8c9731174a94f76755fd3941c0ac93735c07ba14579630e',
|
|
||||||
'a45fdc55c76448c049a1ab33f17023edfb2be3581e9c7aade8a6125215e04220',
|
|
||||||
'd483fe813c6ba647ebbfd3ec41adca1c6130c2beeee9d9bf065c8d151c5f396e',
|
|
||||||
'8a2e1d30050198c65a54483123960ccc38aef6848e1ec8f5f780e8523769ba32',
|
|
||||||
'32888462f8b486c68ad7dd9610be5192bbeaf3b443951ac1a8118419d9fa097b',
|
|
||||||
'227142501b9d4355ccba290404bde41575b037693cef1f438c47f8fbf35d1165',
|
|
||||||
'5c37cc491da847cfeb9281d407efc41e15144c876e0170b499a96a22ed31e01e',
|
|
||||||
'445425117cb8c90edcbc7c1cc0e74f747f2c1efa5630a967c64f287792a48a4b',
|
|
||||||
// This is s = -1, which causes y = 0.
|
|
||||||
'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
||||||
];
|
|
||||||
for (const badBytes of badEncodings) {
|
|
||||||
const b = hexToBytes(badBytes);
|
|
||||||
throws(() => RistrettoPoint.fromHex(b), badBytes);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
should('ristretto255/should create right points from uniform hash', () => {
|
|
||||||
const labels = [
|
|
||||||
'Ristretto is traditionally a short shot of espresso coffee',
|
|
||||||
'made with the normal amount of ground coffee but extracted with',
|
|
||||||
'about half the amount of water in the same amount of time',
|
|
||||||
'by using a finer grind.',
|
|
||||||
'This produces a concentrated shot of coffee per volume.',
|
|
||||||
'Just pulling a normal shot short will produce a weaker shot',
|
|
||||||
'and is not a Ristretto as some believe.',
|
|
||||||
];
|
|
||||||
const encodedHashToPoints = [
|
|
||||||
'3066f82a1a747d45120d1740f14358531a8f04bbffe6a819f86dfe50f44a0a46',
|
|
||||||
'f26e5b6f7d362d2d2a94c5d0e7602cb4773c95a2e5c31a64f133189fa76ed61b',
|
|
||||||
'006ccd2a9e6867e6a2c5cea83d3302cc9de128dd2a9a57dd8ee7b9d7ffe02826',
|
|
||||||
'f8f0c87cf237953c5890aec3998169005dae3eca1fbb04548c635953c817f92a',
|
|
||||||
'ae81e7dedf20a497e10c304a765c1767a42d6e06029758d2d7e8ef7cc4c41179',
|
|
||||||
'e2705652ff9f5e44d3e841bf1c251cf7dddb77d140870d1ab2ed64f1a9ce8628',
|
|
||||||
'80bd07262511cdde4863f8a7434cef696750681cb9510eea557088f76d9e5065',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < labels.length; i++) {
|
|
||||||
const hash = sha512(utf8ToBytes(labels[i]));
|
|
||||||
const point = RistrettoPoint.hashToCurve(hash);
|
|
||||||
deepStrictEqual(point.toHex(), encodedHashToPoints[i]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
should('input immutability: sign/verify are immutable', () => {
|
should('input immutability: sign/verify are immutable', () => {
|
||||||
const privateKey = ed.utils.randomPrivateKey();
|
const privateKey = ed.utils.randomPrivateKey();
|
||||||
@@ -432,51 +324,6 @@ describe('ed25519', () => {
|
|||||||
throws(() => ed.verify(sig, 'deadbeef', Point.BASE));
|
throws(() => ed.verify(sig, 'deadbeef', Point.BASE));
|
||||||
});
|
});
|
||||||
|
|
||||||
const rfc7748Mul = [
|
|
||||||
{
|
|
||||||
scalar: 'a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4',
|
|
||||||
u: 'e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c',
|
|
||||||
outputU: 'c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scalar: '4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d',
|
|
||||||
u: 'e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493',
|
|
||||||
outputU: '95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
for (let i = 0; i < rfc7748Mul.length; i++) {
|
|
||||||
const v = rfc7748Mul[i];
|
|
||||||
should(`RFC7748: scalarMult (${i})`, () => {
|
|
||||||
deepStrictEqual(hex(x25519.scalarMult(v.scalar, v.u)), v.outputU);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const rfc7748Iter = [
|
|
||||||
{ scalar: '422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079', iters: 1 },
|
|
||||||
{ scalar: '684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51', iters: 1000 },
|
|
||||||
// { scalar: '7c3911e0ab2586fd864497297e575e6f3bc601c0883c30df5f4dd2d24f665424', iters: 1000000 },
|
|
||||||
];
|
|
||||||
for (let i = 0; i < rfc7748Iter.length; i++) {
|
|
||||||
const { scalar, iters } = rfc7748Iter[i];
|
|
||||||
should(`RFC7748: scalarMult iteration (${i})`, () => {
|
|
||||||
let k = x25519.Gu;
|
|
||||||
for (let i = 0, u = k; i < iters; i++) [k, u] = [x25519.scalarMult(k, u), k];
|
|
||||||
deepStrictEqual(hex(k), scalar);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
should('RFC7748 getSharedKey', () => {
|
|
||||||
const alicePrivate = '77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a';
|
|
||||||
const alicePublic = '8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a';
|
|
||||||
const bobPrivate = '5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb';
|
|
||||||
const bobPublic = 'de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f';
|
|
||||||
const shared = '4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742';
|
|
||||||
deepStrictEqual(alicePublic, hex(x25519.getPublicKey(alicePrivate)));
|
|
||||||
deepStrictEqual(bobPublic, hex(x25519.getPublicKey(bobPrivate)));
|
|
||||||
deepStrictEqual(hex(x25519.scalarMult(alicePrivate, bobPublic)), shared);
|
|
||||||
deepStrictEqual(hex(x25519.scalarMult(bobPrivate, alicePublic)), shared);
|
|
||||||
});
|
|
||||||
|
|
||||||
// should('X25519/getSharedSecret() should be commutative', () => {
|
// should('X25519/getSharedSecret() should be commutative', () => {
|
||||||
// for (let i = 0; i < 512; i++) {
|
// for (let i = 0; i < 512; i++) {
|
||||||
// const asec = ed.utils.randomPrivateKey();
|
// const asec = ed.utils.randomPrivateKey();
|
||||||
@@ -499,35 +346,6 @@ describe('ed25519', () => {
|
|||||||
// );
|
// );
|
||||||
// });
|
// });
|
||||||
|
|
||||||
{
|
|
||||||
const group = x25519vectors.testGroups[0];
|
|
||||||
should(`Wycheproof/X25519`, () => {
|
|
||||||
for (let i = 0; i < group.tests.length; i++) {
|
|
||||||
const v = group.tests[i];
|
|
||||||
const comment = `(${i}, ${v.result}) ${v.comment}`;
|
|
||||||
if (v.result === 'valid' || v.result === 'acceptable') {
|
|
||||||
try {
|
|
||||||
const shared = hex(x25519.scalarMult(v.private, v.public));
|
|
||||||
deepStrictEqual(shared, v.shared, comment);
|
|
||||||
} catch (e) {
|
|
||||||
// We are more strict
|
|
||||||
if (e.message.includes('Expected valid scalar')) return;
|
|
||||||
if (e.message.includes('Invalid private or public key received')) return;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
} else if (v.result === 'invalid') {
|
|
||||||
let failed = false;
|
|
||||||
try {
|
|
||||||
x25519.scalarMult(v.private, v.public);
|
|
||||||
} catch (error) {
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
deepStrictEqual(failed, true, comment);
|
|
||||||
} else throw new Error('unknown test result');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
should(`Wycheproof/ED25519`, () => {
|
should(`Wycheproof/ED25519`, () => {
|
||||||
for (let g = 0; g < ed25519vectors.testGroups.length; g++) {
|
for (let g = 0; g < ed25519vectors.testGroups.length; g++) {
|
||||||
const group = ed25519vectors.testGroups[g];
|
const group = ed25519vectors.testGroups[g];
|
||||||
@@ -559,91 +377,6 @@ describe('ed25519', () => {
|
|||||||
deepStrictEqual(ed.verify(signature, message, publicKey), true);
|
deepStrictEqual(ed.verify(signature, message, publicKey), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const VECTORS_RFC8032_CTX = [
|
|
||||||
{
|
|
||||||
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
|
|
||||||
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
|
|
||||||
message: 'f726936d19c800494e3fdaff20b276a8',
|
|
||||||
context: '666f6f',
|
|
||||||
signature:
|
|
||||||
'55a4cc2f70a54e04288c5f4cd1e45a7b' +
|
|
||||||
'b520b36292911876cada7323198dd87a' +
|
|
||||||
'8b36950b95130022907a7fb7c4e9b2d5' +
|
|
||||||
'f6cca685a587b4b21f4b888e4e7edb0d',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
|
|
||||||
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
|
|
||||||
message: 'f726936d19c800494e3fdaff20b276a8',
|
|
||||||
context: '626172',
|
|
||||||
signature:
|
|
||||||
'fc60d5872fc46b3aa69f8b5b4351d580' +
|
|
||||||
'8f92bcc044606db097abab6dbcb1aee3' +
|
|
||||||
'216c48e8b3b66431b5b186d1d28f8ee1' +
|
|
||||||
'5a5ca2df6668346291c2043d4eb3e90d',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
|
|
||||||
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
|
|
||||||
message: '508e9e6882b979fea900f62adceaca35',
|
|
||||||
context: '666f6f',
|
|
||||||
signature:
|
|
||||||
'8b70c1cc8310e1de20ac53ce28ae6e72' +
|
|
||||||
'07f33c3295e03bb5c0732a1d20dc6490' +
|
|
||||||
'8922a8b052cf99b7c4fe107a5abb5b2c' +
|
|
||||||
'4085ae75890d02df26269d8945f84b0b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey: 'ab9c2853ce297ddab85c993b3ae14bcad39b2c682beabc27d6d4eb20711d6560',
|
|
||||||
publicKey: '0f1d1274943b91415889152e893d80e93275a1fc0b65fd71b4b0dda10ad7d772',
|
|
||||||
message: 'f726936d19c800494e3fdaff20b276a8',
|
|
||||||
context: '666f6f',
|
|
||||||
signature:
|
|
||||||
'21655b5f1aa965996b3f97b3c849eafb' +
|
|
||||||
'a922a0a62992f73b3d1b73106a84ad85' +
|
|
||||||
'e9b86a7b6005ea868337ff2d20a7f5fb' +
|
|
||||||
'd4cd10b0be49a68da2b2e0dc0ad8960f',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < VECTORS_RFC8032_CTX.length; i++) {
|
|
||||||
const v = VECTORS_RFC8032_CTX[i];
|
|
||||||
should(`RFC8032ctx/${i}`, () => {
|
|
||||||
deepStrictEqual(hex(ed25519ctx.getPublicKey(v.secretKey)), v.publicKey);
|
|
||||||
deepStrictEqual(hex(ed25519ctx.sign(v.message, v.secretKey, v.context)), v.signature);
|
|
||||||
deepStrictEqual(ed25519ctx.verify(v.signature, v.message, v.publicKey, v.context), true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const VECTORS_RFC8032_PH = [
|
|
||||||
{
|
|
||||||
secretKey: '833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42',
|
|
||||||
publicKey: 'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf',
|
|
||||||
message: '616263',
|
|
||||||
signature:
|
|
||||||
'98a70222f0b8121aa9d30f813d683f80' +
|
|
||||||
'9e462b469c7ff87639499bb94e6dae41' +
|
|
||||||
'31f85042463c2a355a2003d062adf5aa' +
|
|
||||||
'a10b8c61e636062aaad11c2a26083406',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < VECTORS_RFC8032_PH.length; i++) {
|
|
||||||
const v = VECTORS_RFC8032_PH[i];
|
|
||||||
should(`RFC8032ph/${i}`, () => {
|
|
||||||
deepStrictEqual(hex(ed25519ph.getPublicKey(v.secretKey)), v.publicKey);
|
|
||||||
deepStrictEqual(hex(ed25519ph.sign(v.message, v.secretKey)), v.signature);
|
|
||||||
deepStrictEqual(ed25519ph.verify(v.signature, v.message, v.publicKey), true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
should('X25519 base point', () => {
|
|
||||||
const { y } = ed25519.ExtendedPoint.BASE;
|
|
||||||
const { Fp } = ed25519.CURVE;
|
|
||||||
const u = Fp.create((y + 1n) * Fp.inv(1n - y));
|
|
||||||
deepStrictEqual(hex(numberToBytesLE(u, 32)), x25519.Gu);
|
|
||||||
});
|
|
||||||
|
|
||||||
should('isTorsionFree()', () => {
|
should('isTorsionFree()', () => {
|
||||||
const orig = ed.utils.getExtendedPublicKey(ed.utils.randomPrivateKey()).point;
|
const orig = ed.utils.getExtendedPublicKey(ed.utils.randomPrivateKey()).point;
|
||||||
for (const hex of ED25519_TORSION_SUBGROUP.slice(1)) {
|
for (const hex of ED25519_TORSION_SUBGROUP.slice(1)) {
|
||||||
@@ -656,6 +389,15 @@ describe('ed25519', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
should('ed25519 bug', () => {
|
||||||
|
const t = 81718630521762619991978402609047527194981150691135404693881672112315521837062n;
|
||||||
|
const point = ed25519.ExtendedPoint.fromAffine({ x: t, y: t });
|
||||||
|
throws(() => point.assertValidity());
|
||||||
|
// Otherwise (without assertValidity):
|
||||||
|
// const point2 = point.double();
|
||||||
|
// point2.toAffine(); // crash!
|
||||||
|
});
|
||||||
|
|
||||||
// ESM is broken.
|
// ESM is broken.
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import './nist.test.js';
|
|||||||
import './ed448.test.js';
|
import './ed448.test.js';
|
||||||
import './ed25519.test.js';
|
import './ed25519.test.js';
|
||||||
import './secp256k1.test.js';
|
import './secp256k1.test.js';
|
||||||
|
import './secp256k1-schnorr.test.js';
|
||||||
import './stark/index.test.js';
|
import './stark/index.test.js';
|
||||||
import './jubjub.test.js';
|
import './jubjub.test.js';
|
||||||
import './bls12-381.test.js';
|
import './bls12-381.test.js';
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ describe('wycheproof ECDH', () => {
|
|||||||
try {
|
try {
|
||||||
const pub = CURVE.ProjectivePoint.fromHex(test.public);
|
const pub = CURVE.ProjectivePoint.fromHex(test.public);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message.includes('Point.fromHex: received invalid point.')) continue;
|
// Our strict validation filter doesn't let weird-length DER vectors
|
||||||
|
if (e.message.startsWith('Point of length')) continue;
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
const shared = CURVE.getSharedSecret(test.private, test.public);
|
const shared = CURVE.getSharedSecret(test.private, test.public);
|
||||||
@@ -140,7 +141,8 @@ describe('wycheproof ECDH', () => {
|
|||||||
try {
|
try {
|
||||||
const pub = curve.ProjectivePoint.fromHex(test.public);
|
const pub = curve.ProjectivePoint.fromHex(test.public);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message.includes('Point.fromHex: received invalid point.')) continue;
|
// Our strict validation filter doesn't let weird-length DER vectors
|
||||||
|
if (e.message.includes('Point of length')) continue;
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
const shared = curve.getSharedSecret(test.private, test.public);
|
const shared = curve.getSharedSecret(test.private, test.public);
|
||||||
@@ -194,7 +196,6 @@ const WYCHEPROOF_ECDSA = {
|
|||||||
secp256k1: {
|
secp256k1: {
|
||||||
curve: secp256k1,
|
curve: secp256k1,
|
||||||
hashes: {
|
hashes: {
|
||||||
// TODO: debug why fails, can be bug
|
|
||||||
sha256: {
|
sha256: {
|
||||||
hash: sha256,
|
hash: sha256,
|
||||||
tests: [secp256k1_sha256_test],
|
tests: [secp256k1_sha256_test],
|
||||||
|
|||||||
34
test/secp256k1-schnorr.test.js
Normal file
34
test/secp256k1-schnorr.test.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { deepStrictEqual, throws } from 'assert';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { should, describe } from 'micro-should';
|
||||||
|
import { bytesToHex as hex } from '@noble/hashes/utils';
|
||||||
|
import { schnorr } from '../lib/esm/secp256k1.js';
|
||||||
|
const schCsv = readFileSync('./test/vectors/schnorr.csv', 'utf-8');
|
||||||
|
|
||||||
|
describe('schnorr.sign()', () => {
|
||||||
|
// index,secret key,public key,aux_rand,message,signature,verification result,comment
|
||||||
|
const vectors = schCsv
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.split(','))
|
||||||
|
.slice(1, -1);
|
||||||
|
for (let vec of vectors) {
|
||||||
|
const [index, sec, pub, rnd, msg, expSig, passes, comment] = vec;
|
||||||
|
should(`${comment || 'vector ' + index}`, () => {
|
||||||
|
if (sec) {
|
||||||
|
deepStrictEqual(hex(schnorr.getPublicKey(sec)), pub.toLowerCase());
|
||||||
|
const sig = schnorr.sign(msg, sec, rnd);
|
||||||
|
deepStrictEqual(hex(sig), expSig.toLowerCase());
|
||||||
|
deepStrictEqual(schnorr.verify(sig, msg, pub), true);
|
||||||
|
} else {
|
||||||
|
const passed = schnorr.verify(expSig, msg, pub);
|
||||||
|
deepStrictEqual(passed, passes === 'TRUE');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ESM is broken.
|
||||||
|
import url from 'url';
|
||||||
|
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
||||||
|
should.run();
|
||||||
|
}
|
||||||
14
test/secp256k1.helpers.js
Normal file
14
test/secp256k1.helpers.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
export { secp256k1 as secp } from '../lib/esm/secp256k1.js';
|
||||||
|
import { secp256k1 as _secp } from '../lib/esm/secp256k1.js';
|
||||||
|
export { bytesToNumberBE, numberToBytesBE } from '../lib/esm/abstract/utils.js';
|
||||||
|
export { mod } from '../lib/esm/abstract/modular.js';
|
||||||
|
export const sigFromDER = (der) => {
|
||||||
|
return _secp.Signature.fromDER(der);
|
||||||
|
};
|
||||||
|
export const sigToDER = (sig) => sig.toDERHex();
|
||||||
|
export const selectHash = (secp) => secp.CURVE.hash;
|
||||||
|
export const normVerifySig = (s) => _secp.Signature.fromDER(s);
|
||||||
|
// export const bytesToNumberBE = secp256k1.utils.bytesToNumberBE;
|
||||||
|
// export const numberToBytesBE = secp256k1.utils.numberToBytesBE;
|
||||||
|
// export const mod = mod_;
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
|
import { hexToBytes, bytesToHex as hex } from '@noble/hashes/utils';
|
||||||
|
import { deepStrictEqual, throws } from 'assert';
|
||||||
import * as fc from 'fast-check';
|
import * as fc from 'fast-check';
|
||||||
import { secp256k1, schnorr } from '../lib/esm/secp256k1.js';
|
|
||||||
import { Fp } from '../lib/esm/abstract/modular.js';
|
|
||||||
import { bytesToNumberBE, numberToBytesBE } from '../lib/esm/abstract/utils.js';
|
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
|
import { should, describe } from 'micro-should';
|
||||||
|
// prettier-ignore
|
||||||
|
import {
|
||||||
|
secp, sigFromDER, sigToDER, selectHash, normVerifySig, mod, bytesToNumberBE, numberToBytesBE
|
||||||
|
} from './secp256k1.helpers.js';
|
||||||
|
|
||||||
import { default as ecdsa } from './vectors/ecdsa.json' assert { type: 'json' };
|
import { default as ecdsa } from './vectors/ecdsa.json' assert { type: 'json' };
|
||||||
import { default as ecdh } from './vectors/ecdh.json' assert { type: 'json' };
|
import { default as ecdh } from './vectors/ecdh.json' assert { type: 'json' };
|
||||||
import { default as privates } from './vectors/privates.json' assert { type: 'json' };
|
import { default as privates } from './vectors/privates.json' assert { type: 'json' };
|
||||||
import { default as points } from './vectors/points.json' assert { type: 'json' };
|
import { default as points } from './vectors/points.json' assert { type: 'json' };
|
||||||
import { default as wp } from './vectors/wychenproof.json' assert { type: 'json' };
|
import { default as wp } from './vectors/wychenproof.json' assert { type: 'json' };
|
||||||
import { should, describe } from 'micro-should';
|
|
||||||
import { deepStrictEqual, throws } from 'assert';
|
|
||||||
import { hexToBytes, bytesToHex } from '@noble/hashes/utils';
|
|
||||||
|
|
||||||
const hex = bytesToHex;
|
|
||||||
const secp = secp256k1;
|
|
||||||
const Point = secp.ProjectivePoint;
|
const Point = secp.ProjectivePoint;
|
||||||
const privatesTxt = readFileSync('./test/vectors/privates-2.txt', 'utf-8');
|
const privatesTxt = readFileSync('./test/vectors/privates-2.txt', 'utf-8');
|
||||||
const schCsv = readFileSync('./test/vectors/schnorr.csv', 'utf-8');
|
|
||||||
|
|
||||||
const FC_BIGINT = fc.bigInt(1n + 1n, secp.CURVE.n - 1n);
|
const FC_BIGINT = fc.bigInt(1n + 1n, secp.CURVE.n - 1n);
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
@@ -193,7 +192,7 @@ describe('secp256k1', () => {
|
|||||||
fc.assert(
|
fc.assert(
|
||||||
fc.property(FC_BIGINT, FC_BIGINT, (r, s) => {
|
fc.property(FC_BIGINT, FC_BIGINT, (r, s) => {
|
||||||
const sig = new secp.Signature(r, s);
|
const sig = new secp.Signature(r, s);
|
||||||
deepStrictEqual(secp.Signature.fromDER(sig.toDERHex()), sig);
|
deepStrictEqual(sigFromDER(sigToDER(sig)), sig);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -241,9 +240,9 @@ describe('secp256k1', () => {
|
|||||||
);
|
);
|
||||||
for (const [msg, exp] of CASES) {
|
for (const [msg, exp] of CASES) {
|
||||||
const res = secp.sign(msg, privKey, { extraEntropy: undefined });
|
const res = secp.sign(msg, privKey, { extraEntropy: undefined });
|
||||||
deepStrictEqual(res.toDERHex(), exp);
|
deepStrictEqual(sigToDER(res), exp);
|
||||||
const rs = secp.Signature.fromDER(res.toDERHex()).toCompactHex();
|
const rs = sigFromDER(sigToDER(res)).toCompactHex();
|
||||||
deepStrictEqual(secp.Signature.fromCompact(rs).toDERHex(), exp);
|
deepStrictEqual(sigToDER(secp.Signature.fromCompact(rs)), exp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
should('handle {extraData} option', () => {
|
should('handle {extraData} option', () => {
|
||||||
@@ -342,7 +341,7 @@ describe('secp256k1', () => {
|
|||||||
const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n;
|
const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n;
|
||||||
const pub = new Point(x, y, 1n).toRawBytes();
|
const pub = new Point(x, y, 1n).toRawBytes();
|
||||||
const sig = new secp.Signature(r, s);
|
const sig = new secp.Signature(r, s);
|
||||||
deepStrictEqual(secp.verify(sig, msg, pub, { strict: false }), true);
|
deepStrictEqual(secp.verify(sig, msg, pub, { lowS: false }), true);
|
||||||
});
|
});
|
||||||
should('not verify invalid deterministic signatures with RFC 6979', () => {
|
should('not verify invalid deterministic signatures with RFC 6979', () => {
|
||||||
for (const vector of ecdsa.invalid.verify) {
|
for (const vector of ecdsa.invalid.verify) {
|
||||||
@@ -351,29 +350,6 @@ describe('secp256k1', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('schnorr.sign()', () => {
|
|
||||||
// index,secret key,public key,aux_rand,message,signature,verification result,comment
|
|
||||||
const vectors = schCsv
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.split(','))
|
|
||||||
.slice(1, -1);
|
|
||||||
for (let vec of vectors) {
|
|
||||||
const [index, sec, pub, rnd, msg, expSig, passes, comment] = vec;
|
|
||||||
should(`${comment || 'vector ' + index}`, () => {
|
|
||||||
if (sec) {
|
|
||||||
deepStrictEqual(hex(schnorr.getPublicKey(sec)), pub.toLowerCase());
|
|
||||||
const sig = schnorr.sign(msg, sec, rnd);
|
|
||||||
deepStrictEqual(hex(sig), expSig.toLowerCase());
|
|
||||||
deepStrictEqual(schnorr.verify(sig, msg, pub), true);
|
|
||||||
} else {
|
|
||||||
const passed = schnorr.verify(expSig, msg, pub);
|
|
||||||
deepStrictEqual(passed, passes === 'TRUE');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('recoverPublicKey()', () => {
|
describe('recoverPublicKey()', () => {
|
||||||
should('recover public key from recovery bit', () => {
|
should('recover public key from recovery bit', () => {
|
||||||
const message = '00000000000000000000000000000000000000000000000000000000deadbeef';
|
const message = '00000000000000000000000000000000000000000000000000000000deadbeef';
|
||||||
@@ -404,7 +380,7 @@ describe('secp256k1', () => {
|
|||||||
should('handle RFC 6979 vectors', () => {
|
should('handle RFC 6979 vectors', () => {
|
||||||
for (const vector of ecdsa.valid) {
|
for (const vector of ecdsa.valid) {
|
||||||
let usig = secp.sign(vector.m, vector.d);
|
let usig = secp.sign(vector.m, vector.d);
|
||||||
let sig = usig.toDERHex();
|
let sig = sigToDER(usig);
|
||||||
const vpub = secp.getPublicKey(vector.d);
|
const vpub = secp.getPublicKey(vector.d);
|
||||||
const recovered = usig.recoverPublicKey(vector.m);
|
const recovered = usig.recoverPublicKey(vector.m);
|
||||||
deepStrictEqual(recovered.toHex(), hex(vpub));
|
deepStrictEqual(recovered.toHex(), hex(vpub));
|
||||||
@@ -459,52 +435,46 @@ describe('secp256k1', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('tweak utilities (legacy)', () => {
|
describe('tweak utilities (legacy)', () => {
|
||||||
const Fn = Fp(secp.CURVE.n);
|
const normal = secp.utils.normPrivateKeyToScalar;
|
||||||
const normal = secp.utils._normalizePrivateKey;
|
|
||||||
const tweakUtils = {
|
const tweakUtils = {
|
||||||
privateAdd: (privateKey, tweak) => {
|
privateAdd: (privateKey, tweak) => {
|
||||||
const p = normal(privateKey);
|
return numberToBytesBE(mod(normal(privateKey) + normal(tweak), secp.CURVE.n), 32);
|
||||||
const t = normal(tweak);
|
|
||||||
return numberToBytesBE(Fn.create(p + t), 32);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
privateNegate: (privateKey) => {
|
privateNegate: (privateKey) => {
|
||||||
return numberToBytesBE(Fn.neg(normal(privateKey)), 32);
|
return numberToBytesBE(mod(-normal(privateKey), secp.CURVE.n), 32);
|
||||||
},
|
},
|
||||||
|
|
||||||
pointAddScalar: (p, tweak, isCompressed) => {
|
pointAddScalar: (p, tweak, isCompressed) => {
|
||||||
const P = Point.fromHex(p);
|
const tweaked = Point.fromHex(p).add(Point.fromPrivateKey(tweak));
|
||||||
const t = normal(tweak);
|
if (tweaked.equals(Point.ZERO)) throw new Error('Tweaked point at infinity');
|
||||||
const Q = Point.BASE.multiplyAndAddUnsafe(P, t, 1n);
|
return tweaked.toRawBytes(isCompressed);
|
||||||
if (!Q) throw new Error('Tweaked point at infinity');
|
|
||||||
return Q.toRawBytes(isCompressed);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
pointMultiply: (p, tweak, isCompressed) => {
|
pointMultiply: (p, tweak, isCompressed) => {
|
||||||
const P = Point.fromHex(p);
|
if (typeof tweak === 'string') tweak = hexToBytes(tweak);
|
||||||
const h = typeof tweak === 'string' ? tweak : bytesToHex(tweak);
|
const t = bytesToNumberBE(tweak);
|
||||||
const t = BigInt(`0x${h}`);
|
return Point.fromHex(p).multiply(t).toRawBytes(isCompressed);
|
||||||
return P.multiply(t).toRawBytes(isCompressed);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
should('privateAdd()', () => {
|
should('privateAdd()', () => {
|
||||||
for (const vector of privates.valid.add) {
|
for (const vector of privates.valid.add) {
|
||||||
const { a, b, expected } = vector;
|
const { a, b, expected } = vector;
|
||||||
deepStrictEqual(bytesToHex(tweakUtils.privateAdd(a, b)), expected);
|
deepStrictEqual(hex(tweakUtils.privateAdd(a, b)), expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
should('privateNegate()', () => {
|
should('privateNegate()', () => {
|
||||||
for (const vector of privates.valid.negate) {
|
for (const vector of privates.valid.negate) {
|
||||||
const { a, expected } = vector;
|
const { a, expected } = vector;
|
||||||
deepStrictEqual(bytesToHex(tweakUtils.privateNegate(a)), expected);
|
deepStrictEqual(hex(tweakUtils.privateNegate(a)), expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
should('pointAddScalar()', () => {
|
should('pointAddScalar()', () => {
|
||||||
for (const vector of points.valid.pointAddScalar) {
|
for (const vector of points.valid.pointAddScalar) {
|
||||||
const { description, P, d, expected } = vector;
|
const { description, P, d, expected } = vector;
|
||||||
const compressed = !!expected && expected.length === 66; // compressed === 33 bytes
|
const compressed = !!expected && expected.length === 66; // compressed === 33 bytes
|
||||||
deepStrictEqual(bytesToHex(tweakUtils.pointAddScalar(P, d, compressed)), expected);
|
deepStrictEqual(hex(tweakUtils.pointAddScalar(P, d, compressed)), expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
should('pointAddScalar() invalid', () => {
|
should('pointAddScalar() invalid', () => {
|
||||||
@@ -516,7 +486,7 @@ describe('secp256k1', () => {
|
|||||||
should('pointMultiply()', () => {
|
should('pointMultiply()', () => {
|
||||||
for (const vector of points.valid.pointMultiply) {
|
for (const vector of points.valid.pointMultiply) {
|
||||||
const { P, d, expected } = vector;
|
const { P, d, expected } = vector;
|
||||||
deepStrictEqual(bytesToHex(tweakUtils.pointMultiply(P, d, true)), expected);
|
deepStrictEqual(hex(tweakUtils.pointMultiply(P, d, true)), expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
should('pointMultiply() invalid', () => {
|
should('pointMultiply() invalid', () => {
|
||||||
@@ -532,10 +502,12 @@ describe('secp256k1', () => {
|
|||||||
// const pubKey = Point.fromHex().toRawBytes();
|
// const pubKey = Point.fromHex().toRawBytes();
|
||||||
const pubKey = group.key.uncompressed;
|
const pubKey = group.key.uncompressed;
|
||||||
for (let test of group.tests) {
|
for (let test of group.tests) {
|
||||||
const m = secp.CURVE.hash(hexToBytes(test.msg));
|
const h = selectHash(secp);
|
||||||
|
|
||||||
|
const m = h(hexToBytes(test.msg));
|
||||||
if (test.result === 'valid' || test.result === 'acceptable') {
|
if (test.result === 'valid' || test.result === 'acceptable') {
|
||||||
const verified = secp.verify(test.sig, m, pubKey);
|
const verified = secp.verify(normVerifySig(test.sig), m, pubKey);
|
||||||
if (secp.Signature.fromDER(test.sig).hasHighS()) {
|
if (sigFromDER(test.sig).hasHighS()) {
|
||||||
deepStrictEqual(verified, false);
|
deepStrictEqual(verified, false);
|
||||||
} else {
|
} else {
|
||||||
deepStrictEqual(verified, true);
|
deepStrictEqual(verified, true);
|
||||||
|
|||||||
Reference in New Issue
Block a user