Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
131f88b504 | ||
|
|
4333e9a686 | ||
|
|
a60d15ff05 | ||
|
|
ceffbc69da | ||
|
|
c75129e629 | ||
|
|
f39fb80c52 | ||
|
|
fcd422d246 | ||
|
|
ed9bf89038 | ||
|
|
7262b4219f | ||
|
|
02b0b25147 |
128
README.md
128
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/)
|
||||
for encoding or hashing an arbitrary string to a point on an elliptic curve
|
||||
- [Poseidon](https://www.poseidon-hash.info) ZK-friendly hash
|
||||
- Auditable
|
||||
- 🏎 [Ultra-fast](#speed), hand-optimized for caveats of JS engines
|
||||
- 🔍 Unique tests ensure correctness. Wycheproof vectors included
|
||||
- 🔻 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
|
||||
2. root directory utilizes one dependency `@noble/hashes` and provides ready-to-use:
|
||||
@@ -26,6 +25,7 @@ Curves incorporate work from previous noble packages
|
||||
[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.
|
||||
Check out [Upgrading](#upgrading) section if you've used them before.
|
||||
|
||||
### This library belongs to _noble_ crypto
|
||||
|
||||
@@ -445,63 +445,93 @@ We consider infrastructure attacks like rogue NPM modules very important; that's
|
||||
Benchmark results on Apple M2 with node v18.10:
|
||||
|
||||
```
|
||||
getPublicKey
|
||||
secp256k1 x 5,241 ops/sec @ 190μs/op
|
||||
P256 x 7,993 ops/sec @ 125μs/op
|
||||
P384 x 3,819 ops/sec @ 261μs/op
|
||||
P521 x 2,074 ops/sec @ 481μs/op
|
||||
ed25519 x 8,390 ops/sec @ 119μs/op
|
||||
ed448 x 3,224 ops/sec @ 310μs/op
|
||||
sign
|
||||
secp256k1 x 3,934 ops/sec @ 254μs/op
|
||||
P256 x 5,327 ops/sec @ 187μs/op
|
||||
P384 x 2,728 ops/sec @ 366μs/op
|
||||
P521 x 1,594 ops/sec @ 626μs/op
|
||||
ed25519 x 4,233 ops/sec @ 236μs/op
|
||||
ed448 x 1,561 ops/sec @ 640μs/op
|
||||
verify
|
||||
secp256k1 x 731 ops/sec @ 1ms/op
|
||||
P256 x 806 ops/sec @ 1ms/op
|
||||
P384 x 353 ops/sec @ 2ms/op
|
||||
P521 x 171 ops/sec @ 5ms/op
|
||||
ed25519 x 860 ops/sec @ 1ms/op
|
||||
ed448 x 313 ops/sec @ 3ms/op
|
||||
getSharedSecret
|
||||
secp256k1 x 445 ops/sec @ 2ms/op
|
||||
recoverPublicKey
|
||||
secp256k1 x 732 ops/sec @ 1ms/op
|
||||
==== bls12-381 ====
|
||||
getPublicKey x 817 ops/sec @ 1ms/op
|
||||
sign x 50 ops/sec @ 19ms/op
|
||||
verify x 34 ops/sec @ 28ms/op
|
||||
pairing x 89 ops/sec @ 11ms/op
|
||||
==== stark ====
|
||||
secp256k1
|
||||
init x 57 ops/sec @ 17ms/op
|
||||
getPublicKey x 4,946 ops/sec @ 202μs/op
|
||||
sign x 3,914 ops/sec @ 255μs/op
|
||||
verify x 682 ops/sec @ 1ms/op
|
||||
getSharedSecret x 427 ops/sec @ 2ms/op
|
||||
recoverPublicKey x 683 ops/sec @ 1ms/op
|
||||
schnorr.sign x 539 ops/sec @ 1ms/op
|
||||
schnorr.verify x 716 ops/sec @ 1ms/op
|
||||
|
||||
P256
|
||||
init x 30 ops/sec @ 32ms/op
|
||||
getPublicKey x 5,008 ops/sec @ 199μs/op
|
||||
sign x 3,970 ops/sec @ 251μs/op
|
||||
verify x 515 ops/sec @ 1ms/op
|
||||
|
||||
P384
|
||||
init x 14 ops/sec @ 66ms/op
|
||||
getPublicKey x 2,434 ops/sec @ 410μs/op
|
||||
sign x 1,942 ops/sec @ 514μs/op
|
||||
verify x 206 ops/sec @ 4ms/op
|
||||
|
||||
P521
|
||||
init x 7 ops/sec @ 126ms/op
|
||||
getPublicKey x 1,282 ops/sec @ 779μs/op
|
||||
sign x 1,077 ops/sec @ 928μs/op
|
||||
verify x 110 ops/sec @ 9ms/op
|
||||
|
||||
ed25519
|
||||
init x 37 ops/sec @ 26ms/op
|
||||
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
|
||||
old x 85 ops/sec @ 11ms/op
|
||||
noble x 1,216 ops/sec @ 822μs/op
|
||||
├─old x 84 ops/sec @ 11ms/op
|
||||
└─noble x 802 ops/sec @ 1ms/op
|
||||
poseidon x 7,466 ops/sec @ 133μs/op
|
||||
verify
|
||||
old x 302 ops/sec @ 3ms/op
|
||||
noble x 698 ops/sec @ 1ms/op
|
||||
├─old x 300 ops/sec @ 3ms/op
|
||||
└─noble x 474 ops/sec @ 2ms/op
|
||||
```
|
||||
|
||||
## 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)
|
||||
2. Different sqrt() function
|
||||
3. DRBG supports outputLen bigger than outputLen of hmac
|
||||
4. Support for different hash functions
|
||||
- 2d affine (x, y) points have been removed to reduce complexity and improve speed
|
||||
- Removed `number` support as a type for private keys. `bigint` is still supported
|
||||
- `mod`, `invert` are no longer present in `utils`. Use `@noble/curves/abstract/modular.js` now.
|
||||
|
||||
Differences from @noble/ed25519 1.7:
|
||||
Upgrading from @noble/secp256k1 1.7:
|
||||
|
||||
1. Variable field element lengths between EDDSA/ECDH:
|
||||
EDDSA (RFC8032) is 456 bits / 57 bytes, ECDH (RFC7748) is 448 bits / 56 bytes
|
||||
2. Different addition formula (doubling is same)
|
||||
3. uvRatio differs between curves (half-expected, not only pow fn changes)
|
||||
4. Point decompression code is different (unexpected), now using generalized formula
|
||||
5. Domain function was no-op for ed25519, but adds some data even with empty context for ed448
|
||||
- Compressed (33-byte) public keys are now returned by default, instead of uncompressed
|
||||
- Methods are now synchronous. Setting `secp.utils.hmacSha256` is no longer required
|
||||
- `sign()`
|
||||
- `der`, `recovered` options were removed
|
||||
- `canonical` was renamed to `lowS`
|
||||
- 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
|
||||
|
||||
## 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();
|
||||
}
|
||||
@@ -1,26 +1,22 @@
|
||||
{
|
||||
"name": "benchmark",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "benchmarks",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"bench": "node index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"micro-bmark": "0.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/bls12-381": "^1.4.0",
|
||||
"@noble/ed25519": "^1.7.1",
|
||||
"@noble/hashes": "^1.1.5",
|
||||
"@noble/secp256k1": "^1.7.0",
|
||||
"@starkware-industries/starkware-crypto-utils": "^0.0.2",
|
||||
"calculate-correlation": "^1.2.3",
|
||||
"elliptic": "^6.5.4"
|
||||
}
|
||||
"name": "benchmark",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "benchmarks",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"bench": "node index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"micro-bmark": "0.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.1.5",
|
||||
"@starkware-industries/starkware-crypto-utils": "^0.0.2",
|
||||
"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();
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@noble/curves",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"description": "Minimal, auditable JS implementation of elliptic curve cryptography",
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"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:release": "rollup -c rollup.config.js",
|
||||
"lint": "prettier --check 'src/**/*.{js,ts}' 'test/*.js'",
|
||||
@@ -30,8 +30,8 @@
|
||||
"@scure/bip39": "~1.1.0",
|
||||
"@types/node": "18.11.3",
|
||||
"fast-check": "3.0.0",
|
||||
"micro-bmark": "0.2.0",
|
||||
"micro-should": "0.3.0",
|
||||
"micro-bmark": "0.3.0",
|
||||
"micro-should": "0.4.0",
|
||||
"prettier": "2.8.3",
|
||||
"rollup": "2.75.5",
|
||||
"typescript": "4.7.3"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||
// Abelian group utilities
|
||||
import { Field, validateField, nLength } from './modular.js';
|
||||
import { validateObject } from './utils.js';
|
||||
const _0n = BigInt(0);
|
||||
const _1n = BigInt(1);
|
||||
|
||||
@@ -153,7 +154,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.
|
||||
// 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)
|
||||
n: bigint; // Curve order, total count of valid points in the field
|
||||
nBitLength?: number; // bit length of curve order
|
||||
@@ -162,24 +163,24 @@ export type AbstractCurve<T> = {
|
||||
hEff?: bigint; // Number to multiply to clear cofactor
|
||||
Gx: T; // base point X 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
|
||||
};
|
||||
|
||||
export function validateAbsOpts<FP, T>(curve: AbstractCurve<FP> & T) {
|
||||
export function validateBasic<FP, T>(curve: BasicCurve<FP> & T) {
|
||||
validateField(curve.Fp);
|
||||
for (const i of ['n', 'h'] as const) {
|
||||
const val = curve[i];
|
||||
if (typeof val !== 'bigint') throw new Error(`Invalid curve param ${i}=${val} (${typeof val})`);
|
||||
}
|
||||
if (!curve.Fp.isValid(curve.Gx)) throw new Error('Invalid generator X coordinate Fp element');
|
||||
if (!curve.Fp.isValid(curve.Gy)) throw new Error('Invalid generator Y coordinate Fp element');
|
||||
|
||||
for (const i of ['nBitLength', 'nByteLength'] as const) {
|
||||
const val = curve[i];
|
||||
if (val === undefined) continue; // Optional
|
||||
if (!Number.isSafeInteger(val)) throw new Error(`Invalid param ${i}=${val} (${typeof val})`);
|
||||
}
|
||||
validateObject(
|
||||
curve,
|
||||
{
|
||||
n: 'bigint',
|
||||
h: 'bigint',
|
||||
Gx: 'field',
|
||||
Gy: 'field',
|
||||
},
|
||||
{
|
||||
nBitLength: 'isSafeInteger',
|
||||
nByteLength: 'isSafeInteger',
|
||||
}
|
||||
);
|
||||
// Set defaults
|
||||
return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||
// Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y²
|
||||
import { mod } from './modular.js';
|
||||
import {
|
||||
bytesToHex,
|
||||
bytesToNumberLE,
|
||||
concatBytes,
|
||||
ensureBytes,
|
||||
FHash,
|
||||
Hex,
|
||||
numberToBytesLE,
|
||||
} from './utils.js';
|
||||
import {
|
||||
Group,
|
||||
GroupConstructor,
|
||||
wNAF,
|
||||
AbstractCurve,
|
||||
validateAbsOpts,
|
||||
AffinePoint,
|
||||
} from './curve.js';
|
||||
import * as ut from './utils.js';
|
||||
import { ensureBytes, FHash, Hex } from './utils.js';
|
||||
import { Group, GroupConstructor, wNAF, BasicCurve, validateBasic, AffinePoint } from './curve.js';
|
||||
|
||||
// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n
|
||||
const _0n = BigInt(0);
|
||||
@@ -26,7 +12,7 @@ const _2n = BigInt(2);
|
||||
const _8n = BigInt(8);
|
||||
|
||||
// Edwards curves must declare params a & d.
|
||||
export type CurveType = AbstractCurve<bigint> & {
|
||||
export type CurveType = BasicCurve<bigint> & {
|
||||
a: bigint; // curve param a
|
||||
d: bigint; // curve param d
|
||||
hash: FHash; // Hashing
|
||||
@@ -39,19 +25,22 @@ export type CurveType = AbstractCurve<bigint> & {
|
||||
};
|
||||
|
||||
function validateOpts(curve: CurveType) {
|
||||
const opts = validateAbsOpts(curve);
|
||||
if (typeof opts.hash !== 'function') throw new Error('Invalid hash function');
|
||||
for (const i of ['a', 'd'] as const) {
|
||||
const val = opts[i];
|
||||
if (typeof val !== 'bigint') throw new Error(`Invalid curve param ${i}=${val} (${typeof val})`);
|
||||
}
|
||||
for (const fn of ['randomBytes'] as const) {
|
||||
if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
||||
}
|
||||
for (const fn of ['adjustScalarBytes', 'domain', 'uvRatio', 'mapToCurve'] as const) {
|
||||
if (opts[fn] === undefined) continue; // Optional
|
||||
if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
||||
}
|
||||
const opts = validateBasic(curve);
|
||||
ut.validateObject(
|
||||
curve,
|
||||
{
|
||||
hash: 'function',
|
||||
a: 'bigint',
|
||||
d: 'bigint',
|
||||
randomBytes: 'function',
|
||||
},
|
||||
{
|
||||
adjustScalarBytes: 'function',
|
||||
domain: 'function',
|
||||
uvRatio: 'function',
|
||||
mapToCurve: 'function',
|
||||
}
|
||||
);
|
||||
// Set defaults
|
||||
return Object.freeze({ ...opts } as const);
|
||||
}
|
||||
@@ -75,7 +64,7 @@ export interface ExtPointConstructor extends GroupConstructor<ExtPointType> {
|
||||
new (x: bigint, y: bigint, z: bigint, t: bigint): ExtPointType;
|
||||
fromAffine(p: AffinePoint<bigint>): ExtPointType;
|
||||
fromHex(hex: Hex): ExtPointType;
|
||||
fromPrivateKey(privateKey: Hex): ExtPointType; // TODO: remove
|
||||
fromPrivateKey(privateKey: Hex): ExtPointType;
|
||||
}
|
||||
|
||||
export type CurveFn = {
|
||||
@@ -340,7 +329,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
||||
const normed = hex.slice(); // copy again, we'll manipulate it
|
||||
const lastByte = hex[len - 1]; // select last byte
|
||||
normed[len - 1] = lastByte & ~0x80; // clear last bit
|
||||
const y = bytesToNumberLE(normed);
|
||||
const y = ut.bytesToNumberLE(normed);
|
||||
if (y === _0n) {
|
||||
// y=0 is allowed
|
||||
} else {
|
||||
@@ -366,12 +355,12 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
||||
}
|
||||
toRawBytes(): Uint8Array {
|
||||
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
|
||||
return bytes; // and use the last byte to encode sign of x
|
||||
}
|
||||
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;
|
||||
@@ -382,7 +371,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
||||
}
|
||||
// Little-endian SHA512 with modulo n
|
||||
function modN_LE(hash: Uint8Array): bigint {
|
||||
return modN(bytesToNumberLE(hash));
|
||||
return modN(ut.bytesToNumberLE(hash));
|
||||
}
|
||||
function isHex(item: Hex, err: string) {
|
||||
if (typeof item !== 'string' && !(item instanceof Uint8Array))
|
||||
@@ -411,7 +400,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
||||
|
||||
// int('LE', SHA512(dom2(F, C) || msgs)) mod N
|
||||
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)));
|
||||
}
|
||||
|
||||
@@ -426,7 +415,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
||||
const k = hashDomainToScalar(context, R, pointBytes, msg); // R || A || PH(M)
|
||||
const s = modN(r + k * scalar); // S = (r + k * s) mod L
|
||||
assertGE0(s); // 0 <= s < l
|
||||
const res = concatBytes(R, numberToBytesLE(s, Fp.BYTES));
|
||||
const res = ut.concatBytes(R, ut.numberToBytesLE(s, Fp.BYTES));
|
||||
return ensureBytes(res, nByteLength * 2); // 64-byte signature
|
||||
}
|
||||
|
||||
@@ -439,7 +428,7 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
||||
if (preHash) msg = preHash(msg); // for ed25519ph, etc
|
||||
const A = Point.fromHex(publicKey, false); // Check for s bounds, hex validity
|
||||
const R = Point.fromHex(sig.slice(0, len), false); // 0 <= R < 2^256: ZIP215 R can be >= P
|
||||
const s = 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 k = hashDomainToScalar(context, R.toRawBytes(), A.toRawBytes(), msg);
|
||||
const RkA = R.add(A.multiplyUnsafe(k));
|
||||
|
||||
@@ -45,7 +45,7 @@ declare const TextDecoder: any;
|
||||
|
||||
export function stringToBytes(str: string): Uint8Array {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
bytesToNumberBE,
|
||||
bytesToNumberLE,
|
||||
ensureBytes,
|
||||
validateObject,
|
||||
} from './utils.js';
|
||||
// prettier-ignore
|
||||
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)
|
||||
// TODO: Fp version?
|
||||
export function pow2(x: bigint, power: bigint, modulo: bigint): bigint {
|
||||
let res = x;
|
||||
while (power-- > _0n) {
|
||||
@@ -249,18 +249,17 @@ const FIELD_FIELDS = [
|
||||
'addN', 'subN', 'mulN', 'sqrN'
|
||||
] as const;
|
||||
export function validateField<T>(field: Field<T>) {
|
||||
for (const i of ['ORDER', 'MASK'] as const) {
|
||||
if (typeof field[i] !== 'bigint')
|
||||
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`);
|
||||
}
|
||||
for (const i of ['BYTES', 'BITS'] as const) {
|
||||
if (typeof field[i] !== 'number')
|
||||
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`);
|
||||
}
|
||||
for (const i of FIELD_FIELDS) {
|
||||
if (typeof field[i] !== 'function')
|
||||
throw new Error(`Invalid field param ${i}=${field[i]} (${typeof field[i]})`);
|
||||
}
|
||||
const initial = {
|
||||
ORDER: 'bigint',
|
||||
MASK: 'bigint',
|
||||
BYTES: 'isSafeInteger',
|
||||
BITS: 'isSafeInteger',
|
||||
} as Record<string, string>;
|
||||
const opts = FIELD_FIELDS.reduce((map, val: string) => {
|
||||
map[val] = 'function';
|
||||
return map;
|
||||
}, initial);
|
||||
return validateObject(field, opts);
|
||||
}
|
||||
|
||||
// Generic field functions
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||
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 _1n = BigInt(1);
|
||||
type Hex = string | Uint8Array;
|
||||
|
||||
export type CurveType = {
|
||||
// Field over which we'll do calculations. Verify with:
|
||||
P: bigint;
|
||||
P: bigint; // finite field prime
|
||||
nByteLength: number;
|
||||
adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array;
|
||||
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
|
||||
@@ -27,24 +26,20 @@ export type CurveFn = {
|
||||
};
|
||||
|
||||
function validateOpts(curve: CurveType) {
|
||||
for (const i of ['a24'] as const) {
|
||||
if (typeof curve[i] !== 'bigint')
|
||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
|
||||
}
|
||||
for (const i of ['montgomeryBits', 'nByteLength'] as const) {
|
||||
if (curve[i] === undefined) continue; // Optional
|
||||
if (!Number.isSafeInteger(curve[i]))
|
||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
|
||||
}
|
||||
for (const fn of ['adjustScalarBytes', 'domain', 'powPminus2'] as const) {
|
||||
if (curve[fn] === undefined) continue; // Optional
|
||||
if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
||||
}
|
||||
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]})`);
|
||||
}
|
||||
validateObject(
|
||||
curve,
|
||||
{
|
||||
a24: 'bigint',
|
||||
},
|
||||
{
|
||||
montgomeryBits: 'isSafeInteger',
|
||||
nByteLength: 'isSafeInteger',
|
||||
adjustScalarBytes: 'function',
|
||||
domain: 'function',
|
||||
powPminus2: 'function',
|
||||
Gu: 'string',
|
||||
}
|
||||
);
|
||||
// Set defaults
|
||||
return Object.freeze({ ...curve } as const);
|
||||
}
|
||||
@@ -61,27 +56,7 @@ export function montgomery(curveDef: CurveType): CurveFn {
|
||||
const adjustScalarBytes = CURVE.adjustScalarBytes || ((bytes: Uint8Array) => bytes);
|
||||
const powPminus2 = CURVE.powPminus2 || ((x: bigint) => pow(x, P - BigInt(2), P));
|
||||
|
||||
/**
|
||||
* 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 from RFC7748. But it is not from RFC7748!
|
||||
/*
|
||||
cswap(swap, x_2, 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];
|
||||
}
|
||||
|
||||
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
|
||||
/**
|
||||
*
|
||||
@@ -106,11 +86,10 @@ export function montgomery(curveDef: CurveType): CurveFn {
|
||||
* @returns new Point on Montgomery curve
|
||||
*/
|
||||
function montgomeryLadder(pointU: bigint, scalar: bigint): bigint {
|
||||
const { P } = CURVE;
|
||||
const u = normalizeScalar(pointU, P);
|
||||
const u = assertFieldElement(pointU);
|
||||
// Section 5: Implementations MUST accept non-canonical values and process them as
|
||||
// if they had been reduced modulo the field prime.
|
||||
const k = normalizeScalar(scalar, P);
|
||||
const k = assertFieldElement(scalar);
|
||||
// The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519
|
||||
const a24 = CURVE.a24;
|
||||
const x_1 = u;
|
||||
@@ -166,28 +145,20 @@ export function montgomery(curveDef: CurveType): CurveFn {
|
||||
}
|
||||
|
||||
function decodeUCoordinate(uEnc: Hex): bigint {
|
||||
const u = ensureBytes(uEnc, montgomeryBytes);
|
||||
// Section 5: When receiving such an array, implementations of X25519
|
||||
// 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
|
||||
// fieldLen - scalaryBytes = 1 for X448 and = 0 for X25519
|
||||
const u = ensureBytes(uEnc, montgomeryBytes);
|
||||
u[fieldLen - 1] &= 127; // 0b0111_1111
|
||||
return bytesToNumberLE(u);
|
||||
}
|
||||
|
||||
function decodeScalar(n: Hex): bigint {
|
||||
const bytes = ensureBytes(n);
|
||||
if (bytes.length !== montgomeryBytes && bytes.length !== fieldLen)
|
||||
throw new Error(`Expected ${montgomeryBytes} or ${fieldLen} bytes, got ${bytes.length}`);
|
||||
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 {
|
||||
const pointU = decodeUCoordinate(u);
|
||||
const _scalar = decodeScalar(scalar);
|
||||
@@ -197,12 +168,7 @@ export function montgomery(curveDef: CurveType): CurveFn {
|
||||
if (pu === _0n) throw new Error('Invalid private or public key received');
|
||||
return encodeUCoordinate(pu);
|
||||
}
|
||||
/**
|
||||
* Computes public key from private.
|
||||
* Executes scalar multiplication of curve's base point by scalar.
|
||||
* @param scalar private key
|
||||
* @returns new public key
|
||||
*/
|
||||
// Computes public key from private. By doing scalar multiplication of base point.
|
||||
function scalarMultBase(scalar: Hex): Uint8Array {
|
||||
return scalarMult(scalar, CURVE.Gu);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||
// Poseidon Hash: https://eprint.iacr.org/2019/458.pdf, https://www.poseidon-hash.info
|
||||
import { 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.
|
||||
// For reference constants see './test/poseidon.test.js'.
|
||||
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'));
|
||||
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
|
||||
let hex = '';
|
||||
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 {
|
||||
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
|
||||
return BigInt(`0x${hex}`);
|
||||
}
|
||||
|
||||
// Caching slows it down 2-3x
|
||||
export function hexToBytes(hex: string): Uint8Array {
|
||||
if (typeof hex !== 'string') throw new Error('hexToBytes: expected string, got ' + typeof hex);
|
||||
if (hex.length % 2) throw new Error('hexToBytes: received invalid unpadded hex ' + hex.length);
|
||||
if (typeof hex !== 'string') throw new Error('string expected, got ' + typeof hex);
|
||||
if (hex.length % 2) throw new Error('hex string is invalid: unpadded ' + hex.length);
|
||||
const array = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const j = i * 2;
|
||||
const hexByte = hex.slice(j, j + 2);
|
||||
const byte = Number.parseInt(hexByte, 16);
|
||||
if (Number.isNaN(byte) || byte < 0) throw new Error('Invalid byte sequence');
|
||||
if (Number.isNaN(byte) || byte < 0) throw new Error('invalid byte sequence');
|
||||
array[i] = byte;
|
||||
}
|
||||
return array;
|
||||
@@ -58,7 +58,7 @@ export function bytesToNumberBE(bytes: Uint8Array): bigint {
|
||||
return hexToNumber(bytesToHex(bytes));
|
||||
}
|
||||
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()));
|
||||
}
|
||||
|
||||
@@ -66,11 +66,7 @@ export const numberToBytesBE = (n: bigint, len: number) =>
|
||||
hexToBytes(n.toString(16).padStart(len * 2, '0'));
|
||||
export const numberToBytesLE = (n: bigint, len: number) => numberToBytesBE(n, len).reverse();
|
||||
// Returns variable number bytes (minimal bigint encoding?)
|
||||
export const numberToVarBytesBE = (n: bigint) => {
|
||||
let hex = n.toString(16);
|
||||
if (hex.length & 1) hex = '0' + hex;
|
||||
return hexToBytes(hex);
|
||||
};
|
||||
export const numberToVarBytesBE = (n: bigint) => hexToBytes(numberToHexUnpadded(n));
|
||||
|
||||
export function ensureBytes(hex: Hex, expectedLength?: number): Uint8Array {
|
||||
// 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.
|
||||
export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
|
||||
if (!arrays.every((b) => u8a(b))) throw new Error('Uint8Array list expected');
|
||||
if (arrays.length === 1) return arrays[0];
|
||||
const length = arrays.reduce((a, arr) => a + arr.length, 0);
|
||||
const result = new Uint8Array(length);
|
||||
for (let i = 0, pad = 0; i < arrays.length; i++) {
|
||||
const arr = arrays[i];
|
||||
result.set(arr, pad);
|
||||
pad += arr.length;
|
||||
}
|
||||
return result;
|
||||
export function concatBytes(...arrs: Uint8Array[]): Uint8Array {
|
||||
const r = new Uint8Array(arrs.reduce((sum, a) => sum + a.length, 0));
|
||||
let pad = 0; // walk through each item, ensure they have proper type
|
||||
arrs.forEach((a) => {
|
||||
if (!u8a(a)) throw new Error('Uint8Array expected');
|
||||
r.set(a, pad);
|
||||
pad += a.length;
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
export function equalBytes(b1: Uint8Array, b2: Uint8Array) {
|
||||
@@ -119,3 +113,32 @@ export const bitSet = (n: bigint, pos: number, value: boolean) =>
|
||||
// Return mask for N bits (Same as BigInt(`0b${Array(i).fill('1').join('')}`))
|
||||
// Not using ** operator with bigints for old engines.
|
||||
export const bitMask = (n: number) => (_2n << BigInt(n - 1)) - _1n;
|
||||
|
||||
type ValMap = Record<string, string>;
|
||||
export function validateObject(object: object, validators: ValMap, optValidators: ValMap = {}) {
|
||||
const validatorFns: Record<string, (val: any) => boolean> = {
|
||||
bigint: (val) => typeof val === 'bigint',
|
||||
function: (val) => typeof val === 'function',
|
||||
boolean: (val) => typeof val === 'boolean',
|
||||
string: (val) => typeof val === 'string',
|
||||
isSafeInteger: (val) => Number.isSafeInteger(val),
|
||||
array: (val) => Array.isArray(val),
|
||||
field: (val) => (object as any).Fp.isValid(val),
|
||||
hash: (val) => typeof val === 'function' && Number.isSafeInteger(val.outputLen),
|
||||
};
|
||||
// type Key = keyof typeof validators;
|
||||
const checkField = (fieldName: string, type: string, 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)) {
|
||||
throw new Error(`Invalid param ${fieldName}=${val} (${typeof val}), expected ${type}`);
|
||||
}
|
||||
};
|
||||
for (let [fieldName, type] of Object.entries(validators)) checkField(fieldName, type, false);
|
||||
for (let [fieldName, type] of Object.entries(optValidators)) checkField(fieldName, type, true);
|
||||
return object;
|
||||
}
|
||||
|
||||
@@ -2,15 +2,8 @@
|
||||
// Short Weierstrass curve. The formula is: y² = x³ + ax + b
|
||||
import * as mod from './modular.js';
|
||||
import * as ut from './utils.js';
|
||||
import { Hex, PrivKey, ensureBytes, CHash } from './utils.js';
|
||||
import {
|
||||
Group,
|
||||
GroupConstructor,
|
||||
wNAF,
|
||||
AbstractCurve,
|
||||
validateAbsOpts,
|
||||
AffinePoint,
|
||||
} from './curve.js';
|
||||
import { CHash, Hex, PrivKey, ensureBytes } from './utils.js';
|
||||
import { Group, GroupConstructor, wNAF, BasicCurve, validateBasic, AffinePoint } from './curve.js';
|
||||
|
||||
export type { AffinePoint };
|
||||
type HmacFnSync = (key: Uint8Array, ...messages: Uint8Array[]) => Uint8Array;
|
||||
@@ -18,18 +11,15 @@ type EndomorphismOpts = {
|
||||
beta: 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
|
||||
a: T;
|
||||
b: T;
|
||||
|
||||
// Optional params
|
||||
// Executed before privkey validation. Useful for P521 with var-length priv key
|
||||
normalizePrivateKey?: (key: PrivKey) => PrivKey;
|
||||
// Whether to execute modular division on a private key, useful for bls curves with cofactor > 1
|
||||
wrapPrivateKey?: boolean;
|
||||
// Endomorphism options for Koblitz curves
|
||||
endo?: EndomorphismOpts;
|
||||
allowedPrivateKeyLengths?: readonly number[]; // for P521
|
||||
wrapPrivateKey?: boolean; // bls12-381 requires mod(n) instead of rejecting keys >= n
|
||||
endo?: EndomorphismOpts; // Endomorphism options for Koblitz curves
|
||||
// When a cofactor != 1, there can be an effective methods to:
|
||||
// 1. Determine whether a point is torsion-free
|
||||
isTorsionFree?: (c: ProjConstructor<T>, point: ProjPointType<T>) => boolean;
|
||||
@@ -89,26 +79,32 @@ export interface ProjConstructor<T> extends GroupConstructor<ProjPointType<T>> {
|
||||
normalizeZ(points: ProjPointType<T>[]): ProjPointType<T>[];
|
||||
}
|
||||
|
||||
export type CurvePointsType<T> = BasicCurve<T> & {
|
||||
export type CurvePointsType<T> = BasicWCurve<T> & {
|
||||
// Bytes
|
||||
fromBytes: (bytes: Uint8Array) => AffinePoint<T>;
|
||||
toBytes: (c: ProjConstructor<T>, point: ProjPointType<T>, compressed: boolean) => Uint8Array;
|
||||
};
|
||||
|
||||
function validatePointOpts<T>(curve: CurvePointsType<T>) {
|
||||
const opts = validateAbsOpts(curve);
|
||||
const Fp = opts.Fp;
|
||||
for (const i of ['a', 'b'] as const) {
|
||||
if (!Fp.isValid(curve[i]))
|
||||
throw new Error(`Invalid curve param ${i}=${opts[i]} (${typeof opts[i]})`);
|
||||
}
|
||||
for (const i of ['isTorsionFree', 'clearCofactor'] as const) {
|
||||
if (curve[i] === undefined) continue; // Optional
|
||||
if (typeof curve[i] !== 'function') throw new Error(`Invalid ${i} function`);
|
||||
}
|
||||
const endo = opts.endo;
|
||||
const opts = validateBasic(curve);
|
||||
ut.validateObject(
|
||||
opts,
|
||||
{
|
||||
a: 'field',
|
||||
b: 'field',
|
||||
fromBytes: 'function',
|
||||
toBytes: 'function',
|
||||
},
|
||||
{
|
||||
allowedPrivateKeyLengths: 'array',
|
||||
wrapPrivateKey: 'boolean',
|
||||
isTorsionFree: 'function',
|
||||
clearCofactor: 'function',
|
||||
}
|
||||
);
|
||||
const { endo, Fp, a } = opts;
|
||||
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');
|
||||
}
|
||||
if (
|
||||
@@ -119,9 +115,6 @@ function validatePointOpts<T>(curve: CurvePointsType<T>) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -207,32 +200,24 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
|
||||
function assertGE(num: bigint) {
|
||||
if (!isWithinCurveOrder(num)) throw new Error('Expected valid bigint: 0 < bigint < curve.n');
|
||||
}
|
||||
/**
|
||||
* Validates if a private key is valid and converts it to bigint form.
|
||||
* 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`
|
||||
*/
|
||||
// Validates if priv key is valid and converts it to bigint.
|
||||
// Supports options CURVE.normalizePrivateKey and CURVE.wrapPrivateKey.
|
||||
function normalizePrivateKey(key: PrivKey): bigint {
|
||||
const { normalizePrivateKey: custom, nByteLength: groupLen, wrapPrivateKey, n } = CURVE;
|
||||
if (typeof custom === 'function') key = custom(key);
|
||||
let num: bigint;
|
||||
if (typeof key === 'bigint') {
|
||||
// Curve order check is done below
|
||||
num = key;
|
||||
} 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);
|
||||
const { allowedPrivateKeyLengths: lengths, nByteLength, wrapPrivateKey, n } = CURVE;
|
||||
if (lengths && typeof key !== 'bigint') {
|
||||
if (key instanceof Uint8Array) key = ut.bytesToHex(key);
|
||||
// Normalize to hex string, pad. E.g. P521 would norm 130-132 char hex to 132-char bytes
|
||||
if (typeof key !== 'string' || !lengths.includes(key.length)) throw new Error('Invalid key');
|
||||
key = key.padStart(nByteLength * 2, '0');
|
||||
}
|
||||
// Useful for curves with cofactor != 1
|
||||
if (wrapPrivateKey) num = mod.mod(num, n);
|
||||
assertGE(num);
|
||||
let num: bigint;
|
||||
try {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -612,25 +597,30 @@ type SignatureLike = { r: bigint; s: bigint };
|
||||
|
||||
export type PubKey = Hex | ProjPointType<bigint>;
|
||||
|
||||
export type CurveType = BasicCurve<bigint> & {
|
||||
// Default options
|
||||
lowS?: boolean;
|
||||
// Hashes
|
||||
hash: CHash; // Because we need outputLen for DRBG
|
||||
export type CurveType = BasicWCurve<bigint> & {
|
||||
hash: CHash; // CHash not FHash because we need outputLen for DRBG
|
||||
hmac: HmacFnSync;
|
||||
randomBytes: (bytesLength?: number) => Uint8Array;
|
||||
// truncateHash?: (hash: Uint8Array, truncateOnly?: boolean) => Uint8Array;
|
||||
lowS?: boolean;
|
||||
bits2int?: (bytes: Uint8Array) => bigint;
|
||||
bits2int_modN?: (bytes: Uint8Array) => bigint;
|
||||
};
|
||||
|
||||
function validateOpts(curve: CurveType) {
|
||||
const opts = validateAbsOpts(curve);
|
||||
if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen))
|
||||
throw new Error('Invalid hash function');
|
||||
if (typeof opts.hmac !== 'function') throw new Error('Invalid hmac function');
|
||||
if (typeof opts.randomBytes !== 'function') throw new Error('Invalid randomBytes function');
|
||||
// Set defaults
|
||||
const opts = validateBasic(curve);
|
||||
ut.validateObject(
|
||||
opts,
|
||||
{
|
||||
hash: 'hash',
|
||||
hmac: 'function',
|
||||
randomBytes: 'function',
|
||||
},
|
||||
{
|
||||
bits2int: 'function',
|
||||
bits2int_modN: 'function',
|
||||
lowS: 'boolean',
|
||||
}
|
||||
);
|
||||
return Object.freeze({ lowS: true, ...opts } as const);
|
||||
}
|
||||
|
||||
@@ -759,7 +749,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
||||
return { x, y };
|
||||
} else {
|
||||
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`
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -824,7 +814,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
||||
const ir = invN(radj); // r^-1
|
||||
const u1 = modN(-h * ir); // -hr^-1
|
||||
const u2 = modN(s * ir); // sr^-1
|
||||
const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2); // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1)
|
||||
const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2); // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1)
|
||||
if (!Q) throw new Error('point at infinify'); // unsafe is fine: no priv data leaked
|
||||
Q.assertValidity();
|
||||
return Q;
|
||||
@@ -954,9 +944,10 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
||||
// NOTE: pads output with zero as per spec
|
||||
const ORDER_MASK = ut.bitMask(CURVE.nBitLength);
|
||||
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))
|
||||
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!
|
||||
return ut.numberToBytesBE(num, CURVE.nByteLength);
|
||||
}
|
||||
@@ -967,32 +958,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.
|
||||
// Also it can be bigger for P224 + SHA256
|
||||
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 (['recovered', 'canonical'].some((k) => k in opts))
|
||||
// Ban legacy options
|
||||
throw new Error('sign() legacy options not supported');
|
||||
let { lowS, prehash, extraEntropy: ent } = opts; // generates low-s sigs by default
|
||||
if (prehash) msgHash = CURVE.hash(ensureBytes(msgHash));
|
||||
if (lowS == null) lowS = true; // RFC6979 3.2: we skip step A, because
|
||||
// Step A is ignored, since we already provide hash instead of msg
|
||||
if (prehash) msgHash = hash(ensureBytes(msgHash));
|
||||
if (lowS == null) lowS = true; // RFC6979 3.2: we skip step A, because we already provide hash
|
||||
|
||||
// NOTE: instead of bits2int, we calling here truncateHash, since we need
|
||||
// custom truncation for stark. For other curves it is essentially same as calling bits2int + mod
|
||||
// However, we cannot later call bits2octets (which is truncateHash + int2octets), since nested bits2int is broken
|
||||
// for curves where nBitLength % 8 !== 0, so we unwrap it here as int2octets call.
|
||||
// const bits2octets = (bits)=>int2octets(bytesToNumberBE(truncateHash(bits)))
|
||||
// We can't later call bits2octets, since nested bits2int is broken for curves
|
||||
// with nBitLength % 8 !== 0. Because of that, we unwrap it here as int2octets call.
|
||||
// const bits2octets = (bits) => int2octets(bits2int_modN(bits))
|
||||
const h1int = bits2int_modN(ensureBytes(msgHash));
|
||||
const h1octets = int2octets(h1int);
|
||||
|
||||
const d = normalizePrivateKey(privateKey);
|
||||
// K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k')
|
||||
const seedArgs = [int2octets(d), h1octets];
|
||||
const d = normalizePrivateKey(privateKey); // validate private key, convert to bigint
|
||||
const seedArgs = [int2octets(d), int2octets(h1int)];
|
||||
// extraEntropy. RFC6979 3.6: additional k' (optional).
|
||||
if (ent != null) {
|
||||
// RFC6979 3.6: additional k' (optional)
|
||||
if (ent === true) ent = CURVE.randomBytes(Fp.BYTES);
|
||||
const e = ensureBytes(ent);
|
||||
if (e.length !== Fp.BYTES) throw new Error(`sign: Expected ${Fp.BYTES} bytes of extra data`);
|
||||
seedArgs.push(e);
|
||||
// K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k')
|
||||
// Either pass as-is, or generate random bytes. Then validate for being ui8a of size BYTES
|
||||
seedArgs.push(ensureBytes(ent === true ? randomBytes(Fp.BYTES) : ent, Fp.BYTES));
|
||||
}
|
||||
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!
|
||||
@@ -1054,7 +1039,7 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
||||
* ```
|
||||
*/
|
||||
function verify(
|
||||
signature: Hex | { r: bigint; s: bigint },
|
||||
signature: Hex | SignatureLike,
|
||||
msgHash: Hex,
|
||||
publicKey: Hex,
|
||||
opts = defaultVerOpts
|
||||
@@ -1099,7 +1084,6 @@ export function weierstrass(curveDef: CurveType): CurveFn {
|
||||
getSharedSecret,
|
||||
sign,
|
||||
verify,
|
||||
// Point,
|
||||
ProjectivePoint: Point,
|
||||
Signature,
|
||||
utils,
|
||||
|
||||
@@ -10,7 +10,7 @@ export const P224 = createCurve(
|
||||
// Params: a, b
|
||||
a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffffffffffe'),
|
||||
b: BigInt('0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4'),
|
||||
// Field over which we'll do calculations; 2n**224n - 2n**96n + 1n
|
||||
// Field over which we'll do calculations;
|
||||
Fp: Fp(BigInt('0xffffffffffffffffffffffffffffffff000000000000000000000001')),
|
||||
// Curve order, total count of valid points in the field
|
||||
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) */
|
||||
import { createCurve } from './_shortw_utils.js';
|
||||
import { sha512 } from '@noble/hashes/sha512';
|
||||
import { bytesToHex, PrivKey } from './abstract/utils.js';
|
||||
import { Fp as Field } from './abstract/modular.js';
|
||||
import { mapToCurveSimpleSWU } from './abstract/weierstrass.js';
|
||||
import * as htf from './abstract/hash-to-curve.js';
|
||||
@@ -38,16 +37,7 @@ export const P521 = createCurve({
|
||||
Gy: BigInt('0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650'),
|
||||
h: BigInt(1),
|
||||
lowS: false,
|
||||
// P521 keys could be 130, 131, 132 bytes. We normalize to 132 bytes.
|
||||
// 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
|
||||
},
|
||||
allowedPrivateKeyLengths: [130, 131, 132] // P521 keys are variable-length. Normalize to 132b
|
||||
} as const, sha512);
|
||||
export const secp521r1 = P521;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ensureBytes,
|
||||
concatBytes,
|
||||
Hex,
|
||||
bytesToNumberBE as bytesToNum,
|
||||
bytesToNumberBE as bytesToInt,
|
||||
PrivKey,
|
||||
numberToBytesBE,
|
||||
} from './abstract/utils.js';
|
||||
@@ -130,57 +130,53 @@ function taggedHash(tag: string, ...messages: Uint8Array[]): Uint8Array {
|
||||
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 modN = (x: bigint) => mod(x, secp256k1N);
|
||||
const _Point = secp256k1.ProjectivePoint;
|
||||
const Gmul = (priv: PrivKey) => _Point.fromPrivateKey(priv);
|
||||
const Point = secp256k1.ProjectivePoint;
|
||||
const GmulAdd = (Q: PointType<bigint>, a: bigint, b: bigint) =>
|
||||
_Point.BASE.multiplyAndAddUnsafe(Q, a, b);
|
||||
function schnorrGetScalar(priv: bigint) {
|
||||
// Let d' = int(sk)
|
||||
// Fail if d' = 0 or d' ≥ n
|
||||
// Let P = d'⋅G
|
||||
// Let d = d' if has_even_y(P), otherwise let d = n - d' .
|
||||
const point = Gmul(priv);
|
||||
const scalar = point.hasEvenY() ? priv : modN(-priv);
|
||||
return { point, scalar, x: toRawX(point) };
|
||||
Point.BASE.multiplyAndAddUnsafe(Q, a, b);
|
||||
const hex32ToInt = (key: Hex) => bytesToInt(ensureBytes(key, 32));
|
||||
function schnorrGetExtPubKey(priv: PrivKey) {
|
||||
let d = typeof priv === 'bigint' ? priv : hex32ToInt(priv);
|
||||
const point = Point.fromPrivateKey(d); // P = d'⋅G; 0 < d' < n check is done inside
|
||||
const scalar = point.hasEvenY() ? d : modN(-d); // d = d' if has_even_y(P), otherwise d = n-d'
|
||||
return { point, scalar, bytes: pointToBytes(point) };
|
||||
}
|
||||
function lift_x(x: bigint): PointType<bigint> {
|
||||
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.
|
||||
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
|
||||
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();
|
||||
return p;
|
||||
}
|
||||
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
|
||||
// TODO: replace with proper xor?
|
||||
const t = numTo32b(d ^ bytesToNum(taggedHash(TAGS.aux, a))); // Let t be the byte-wise xor of bytes(d) and hash/aux(a)
|
||||
const t = numTo32b(d ^ bytesToInt(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 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.
|
||||
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 sig = new Uint8Array(64); // Let sig = bytes(R) || bytes((k + ed) mod n).
|
||||
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 {
|
||||
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 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;
|
||||
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;
|
||||
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
|
||||
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.
|
||||
@@ -212,11 +208,18 @@ function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): boolean {
|
||||
}
|
||||
|
||||
export const schnorr = {
|
||||
// Schnorr's pubkey is just `x` of Point (BIP340)
|
||||
getPublicKey: schnorrGetPublicKey,
|
||||
sign: schnorrSign,
|
||||
verify: schnorrVerify,
|
||||
utils: { lift_x, int: bytesToNum, taggedHash },
|
||||
utils: {
|
||||
getExtendedPublicKey: schnorrGetExtPubKey,
|
||||
lift_x,
|
||||
pointToBytes,
|
||||
numberToBytesBE,
|
||||
bytesToNumberBE: bytesToInt,
|
||||
taggedHash,
|
||||
mod,
|
||||
},
|
||||
};
|
||||
|
||||
const isoMap = htf.isogenyMap(
|
||||
|
||||
@@ -97,7 +97,7 @@ function ensureBytes0x(hex: Hex): Uint8Array {
|
||||
function normalizePrivateKey(privKey: Hex) {
|
||||
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);
|
||||
}
|
||||
function getSharedSecret0x(privKeyA: Hex, pubKeyB: Hex) {
|
||||
|
||||
@@ -436,9 +436,16 @@ for (const name in CURVES) {
|
||||
throws(() => G[1][op](0n), '0n');
|
||||
G[1][op](G[2]);
|
||||
throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER');
|
||||
throws(() => G[1][op](-123n), '-123n');
|
||||
throws(() => G[1][op](123), '123');
|
||||
throws(() => G[1][op](123.456), '123.456');
|
||||
throws(() => G[1][op](true), 'true');
|
||||
throws(() => G[1][op](false), 'false');
|
||||
throws(() => G[1][op](null), 'null');
|
||||
throws(() => G[1][op](undefined), 'undefined');
|
||||
throws(() => G[1][op]('1'), "'1'");
|
||||
throws(() => G[1][op]({ x: 1n, y: 1n }), '{ x: 1n, y: 1n }');
|
||||
throws(() => G[1][op]({ x: 1n, y: 1n, z: 1n }), '{ x: 1n, y: 1n, z: 1n }');
|
||||
throws(
|
||||
() => G[1][op]({ x: 1n, y: 1n, z: 1n, t: 1n }),
|
||||
'{ x: 1n, y: 1n, z: 1n, t: 1n }'
|
||||
@@ -527,10 +534,13 @@ for (const name in CURVES) {
|
||||
should('.getPublicKey() type check', () => {
|
||||
throws(() => C.getPublicKey(0), '0');
|
||||
throws(() => C.getPublicKey(0n), '0n');
|
||||
throws(() => C.getPublicKey(false), 'false');
|
||||
throws(() => C.getPublicKey(-123n), '-123n');
|
||||
throws(() => C.getPublicKey(123), '123');
|
||||
throws(() => C.getPublicKey(123.456), '123.456');
|
||||
throws(() => C.getPublicKey(true), 'true');
|
||||
throws(() => C.getPublicKey(false), 'false');
|
||||
throws(() => C.getPublicKey(null), 'null');
|
||||
throws(() => C.getPublicKey(undefined), 'undefined');
|
||||
throws(() => C.getPublicKey(''), "''");
|
||||
// NOTE: passes because of disabled hex padding checks for starknet, maybe enable?
|
||||
// throws(() => C.getPublicKey('1'), "'1'");
|
||||
@@ -560,25 +570,25 @@ for (const name in CURVES) {
|
||||
should('.sign() edge cases', () => {
|
||||
throws(() => C.sign());
|
||||
throws(() => C.sign(''));
|
||||
throws(() => C.sign('', ''));
|
||||
throws(() => C.sign(new Uint8Array(), new Uint8Array()));
|
||||
});
|
||||
|
||||
describe('verify()', () => {
|
||||
const msg = '01'.repeat(32);
|
||||
should('true for proper signatures', () => {
|
||||
const msg = '01'.repeat(32);
|
||||
const priv = C.utils.randomPrivateKey();
|
||||
const sig = C.sign(msg, priv);
|
||||
const pub = C.getPublicKey(priv);
|
||||
deepStrictEqual(C.verify(sig, msg, pub), true);
|
||||
});
|
||||
should('false for wrong messages', () => {
|
||||
const msg = '01'.repeat(32);
|
||||
const priv = C.utils.randomPrivateKey();
|
||||
const sig = C.sign(msg, priv);
|
||||
const pub = C.getPublicKey(priv);
|
||||
deepStrictEqual(C.verify(sig, '11'.repeat(32), pub), false);
|
||||
});
|
||||
should('false for wrong keys', () => {
|
||||
const msg = '01'.repeat(32);
|
||||
const priv = C.utils.randomPrivateKey();
|
||||
const sig = C.sign(msg, priv);
|
||||
deepStrictEqual(C.verify(sig, msg, C.getPublicKey(C.utils.randomPrivateKey())), false);
|
||||
|
||||
@@ -86,7 +86,8 @@ describe('wycheproof ECDH', () => {
|
||||
try {
|
||||
const pub = CURVE.ProjectivePoint.fromHex(test.public);
|
||||
} 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;
|
||||
}
|
||||
const shared = CURVE.getSharedSecret(test.private, test.public);
|
||||
@@ -140,7 +141,8 @@ describe('wycheproof ECDH', () => {
|
||||
try {
|
||||
const pub = curve.ProjectivePoint.fromHex(test.public);
|
||||
} 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;
|
||||
}
|
||||
const shared = curve.getSharedSecret(test.private, test.public);
|
||||
@@ -194,7 +196,6 @@ const WYCHEPROOF_ECDSA = {
|
||||
secp256k1: {
|
||||
curve: secp256k1,
|
||||
hashes: {
|
||||
// TODO: debug why fails, can be bug
|
||||
sha256: {
|
||||
hash: sha256,
|
||||
tests: [secp256k1_sha256_test],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { bytesToNumberBE, ensureBytes, numberToBytesBE } from '../lib/esm/abstract/utils.js';
|
||||
import { readFileSync } from 'fs';
|
||||
import { default as ecdsa } from './vectors/ecdsa.json' assert { type: 'json' };
|
||||
import { default as ecdh } from './vectors/ecdh.json' assert { type: 'json' };
|
||||
@@ -463,9 +463,7 @@ describe('secp256k1', () => {
|
||||
const normal = secp.utils._normalizePrivateKey;
|
||||
const tweakUtils = {
|
||||
privateAdd: (privateKey, tweak) => {
|
||||
const p = normal(privateKey);
|
||||
const t = normal(tweak);
|
||||
return numberToBytesBE(Fn.create(p + t), 32);
|
||||
return numberToBytesBE(Fn.add(normal(privateKey), normal(tweak)), 32);
|
||||
},
|
||||
|
||||
privateNegate: (privateKey) => {
|
||||
@@ -473,18 +471,13 @@ describe('secp256k1', () => {
|
||||
},
|
||||
|
||||
pointAddScalar: (p, tweak, isCompressed) => {
|
||||
const P = Point.fromHex(p);
|
||||
const t = normal(tweak);
|
||||
const Q = Point.BASE.multiplyAndAddUnsafe(P, t, 1n);
|
||||
if (!Q) throw new Error('Tweaked point at infinity');
|
||||
return Q.toRawBytes(isCompressed);
|
||||
// Will throw if tweaked point is at infinity
|
||||
return Point.fromHex(p).add(Point.fromPrivateKey(tweak)).toRawBytes(isCompressed);
|
||||
},
|
||||
|
||||
pointMultiply: (p, tweak, isCompressed) => {
|
||||
const P = Point.fromHex(p);
|
||||
const h = typeof tweak === 'string' ? tweak : bytesToHex(tweak);
|
||||
const t = BigInt(`0x${h}`);
|
||||
return P.multiply(t).toRawBytes(isCompressed);
|
||||
const t = bytesToNumberBE(ensureBytes(tweak));
|
||||
return Point.fromHex(p).multiply(t).toRawBytes(isCompressed);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user