18 Commits
0.6.0 ... 0.6.2

Author SHA1 Message Date
Paul Miller
e2a7594eae Release 0.6.2. 2023-01-30 08:18:07 +01:00
Paul Miller
823149ecd9 Clarify comment 2023-01-30 08:17:08 +01:00
Paul Miller
e57aec63d8 Fix edwards assertValidity 2023-01-30 08:04:36 +01:00
Paul Miller
837aca98c9 Fix bugs 2023-01-30 06:10:56 +01:00
Paul Miller
dbb16b0e5e edwards: add assertValidity 2023-01-30 06:10:08 +01:00
Paul Miller
e14af67254 utils: fix hexToNumber, improve validateObject 2023-01-30 06:07:53 +01:00
Paul Miller
4780850748 montgomery: fix fieldLen 2023-01-30 05:56:07 +01:00
Paul Miller
3374a70f47 README update 2023-01-30 05:55:36 +01:00
Paul Miller
131f88b504 Release 0.6.1. 2023-01-29 05:14:10 +01:00
Paul Miller
4333e9a686 README 2023-01-29 05:12:58 +01:00
Paul Miller
a60d15ff05 Upgrading guide from other noble libraries 2023-01-29 05:10:58 +01:00
Paul Miller
ceffbc69da More Schnorr utils 2023-01-29 04:46:38 +01:00
Paul Miller
c75129e629 Use declarative curve field validation 2023-01-28 03:19:46 +01:00
Paul Miller
f39fb80c52 weierstrass: rename normalizePrivateKey to allowedPrivateKeyLengths 2023-01-27 23:45:55 +01:00
Paul Miller
fcd422d246 README updates 2023-01-27 03:48:53 +01:00
Paul Miller
ed9bf89038 stark: isCompressed=false. Update benchmarks 2023-01-27 03:43:18 +01:00
Paul Miller
7262b4219f Bump micro-should 2023-01-26 08:26:07 +01:00
Paul Miller
02b0b25147 New schnorr exports. Simplify RFC6979 k gen, privkey checks 2023-01-26 08:16:00 +01:00
25 changed files with 658 additions and 860 deletions

212
README.md
View File

@@ -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
@@ -329,47 +329,54 @@ The module allows to hash arbitrary strings to elliptic curve points.
- `expand_message_xmd` [(spec)](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.4.1) produces a uniformly random byte string using a cryptographic hash function H that outputs b bits..
```ts
function expand_message_xmd(
msg: Uint8Array, DST: Uint8Array, lenInBytes: number, H: CHash
): Uint8Array;
function expand_message_xof(
msg: Uint8Array, DST: Uint8Array, lenInBytes: number, k: number, H: CHash
): Uint8Array;
```
```ts
function expand_message_xmd(
msg: Uint8Array,
DST: Uint8Array,
lenInBytes: number,
H: CHash
): Uint8Array;
function expand_message_xof(
msg: Uint8Array,
DST: Uint8Array,
lenInBytes: number,
k: number,
H: CHash
): Uint8Array;
```
- `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.
* `msg` a byte string containing the message to hash
* `count` the number of elements of F to output
* `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.
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
_ `count` the number of elements of F to output
_ `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.
```ts
function hash_to_field(msg: Uint8Array, count: number, options: htfOpts): bigint[][];
type htfOpts = {
// DST: a domain separation tag
// defined in section 2.2.5
DST: string;
// p: the characteristic of F
// where F is a finite field of characteristic p and order q = p^m
p: bigint;
// m: the extension degree of F, m >= 1
// where F is a finite field of characteristic p and order q = p^m
m: number;
// k: the target security level for the suite in bits
// defined in section 5.1
k: number;
// option to use a message that has already been processed by
// expand_message_xmd
expand?: 'xmd' | 'xof';
// Hash functions for: expand_message_xmd is appropriate for use with a
// wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others.
// BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247
// TODO: verify that hash is shake if expand==='xof' via types
hash: CHash;
};
```
```ts
function hash_to_field(msg: Uint8Array, count: number, options: htfOpts): bigint[][];
type htfOpts = {
// DST: a domain separation tag
// defined in section 2.2.5
DST: string;
// p: the characteristic of F
// where F is a finite field of characteristic p and order q = p^m
p: bigint;
// m: the extension degree of F, m >= 1
// where F is a finite field of characteristic p and order q = p^m
m: number;
// k: the target security level for the suite in bits
// defined in section 5.1
k: number;
// option to use a message that has already been processed by
// expand_message_xmd
expand?: 'xmd' | 'xof';
// Hash functions for: expand_message_xmd is appropriate for use with a
// wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others.
// BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247
// TODO: verify that hash is shake if expand==='xof' via types
hash: CHash;
};
```
### abstract/poseidon: Poseidon hash
@@ -445,63 +452,94 @@ 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
- `bigint` is no longer allowed in `getPublicKey`, `sign`, `verify`. Reason: ed25519 is LE, can lead to bugs
## Contributing & testing

7
benchmark/_shared.js Normal file
View File

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

52
benchmark/bls.js Normal file
View File

@@ -0,0 +1,52 @@
import { readFileSync } from 'fs';
import { mark, run } from 'micro-bmark';
import { bls12_381 as bls } from '../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
View 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();
}
});

View File

@@ -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();
}

View File

@@ -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
View 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
View 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();
});

View File

@@ -1,12 +1,12 @@
{
"name": "@noble/curves",
"version": "0.6.0",
"version": "0.6.2",
"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"

View File

@@ -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);
}

View File

@@ -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 = {
@@ -182,8 +171,27 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
this._WINDOW_SIZE = windowSize;
pointPrecomputes.delete(this);
}
assertValidity(): void {}
// Not required for fromHex(), which always creates valid points.
// Could be useful for fromAffine().
assertValidity(): void {
const { a, d } = CURVE;
if (this.is0()) throw new Error('bad point: ZERO'); // TODO: optimize, with vars below?
// Equation in affine coordinates: ax² + y² = 1 + dx²y²
// Equation in projective coordinates (X/Z, Y/Z, Z): (aX² + Y²)Z² = Z⁴ + dX²Y²
const { ex: X, ey: Y, ez: Z, et: T } = this;
const X2 = modP(X * X); // X²
const Y2 = modP(Y * Y); // Y²
const Z2 = modP(Z * Z); // Z²
const Z4 = modP(Z2 * Z2); // Z⁴
const aX2 = modP(X2 * a); // aX²
const left = modP(Z2 * modP(aX2 + Y2)); // (aX² + Y²)Z²
const right = modP(Z4 + modP(d * modP(X2 * Y2))); // Z⁴ + dX²Y²
if (left !== right) throw new Error('bad point: equation left != right (1)');
// In Extended coordinates we also have T, which is x*y=T/Z: check X*Y == Z*T
const XY = modP(X * Y);
const ZT = modP(Z * T);
if (XY !== ZT) throw new Error('bad point: equation left != right (2)');
}
// Compare one point to another.
equals(other: Point): boolean {
@@ -340,7 +348,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 +374,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 +390,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 +419,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 +434,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 +447,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));

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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,21 @@ 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
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);
}
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 +169,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);
}

View File

@@ -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 = {

View File

@@ -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}`);
return BigInt(hex === '' ? '0' : `0x${hex}`);
}
// Caching slows it down 2-3x
export function hexToBytes(hex: string): Uint8Array {
if (typeof hex !== 'string') throw new Error('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,48 @@ 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;
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' });

View File

@@ -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,33 @@ 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',
allowInfinityPoint: 'boolean',
}
);
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 +116,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 +201,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;
}
@@ -255,6 +241,8 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
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 {
const { x, y } = p || {};
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 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 +752,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 +817,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 +947,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 +961,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 +1042,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 +1087,6 @@ export function weierstrass(curveDef: CurveType): CurveFn {
getSharedSecret,
sign,
verify,
// Point,
ProjectivePoint: Point,
Signature,
utils,

View File

@@ -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'),

View File

@@ -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;

View File

@@ -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(

View File

@@ -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) {

View File

@@ -10,7 +10,7 @@ import { secp256r1 } from '../lib/esm/p256.js';
import { secp384r1 } from '../lib/esm/p384.js';
import { secp521r1 } from '../lib/esm/p521.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 { starkCurve } from '../lib/esm/stark.js';
import { pallas, vesta } from '../lib/esm/pasta.js';
@@ -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'");
@@ -557,28 +567,39 @@ for (const name in CURVES) {
{ numRuns: NUM_RUNS }
)
);
should('.verify() should verify empty signatures', () => {
const msg = new Uint8Array([]);
const priv = C.utils.randomPrivateKey();
const pub = C.getPublicKey(priv);
const sig = C.sign(msg, priv);
deepStrictEqual(
C.verify(sig, msg, pub),
true,
'priv=${toHex(priv)},pub=${toHex(pub)},msg=${msg}'
);
});
should('.sign() edge cases', () => {
throws(() => C.sign());
throws(() => C.sign(''));
throws(() => C.sign('', ''));
throws(() => C.sign(new Uint8Array(), new Uint8Array()));
});
describe('verify()', () => {
const msg = '01'.repeat(32);
should('true for proper signatures', () => {
const 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);
@@ -641,6 +662,16 @@ should('secp224k1 sqrt bug', () => {
deepStrictEqual(Fp.sqr(sqrtMinus1), Fp.create(-1n));
});
should('bigInt private keys', () => {
// Doesn't support bigints anymore
throws(() => ed25519.sign('', 123n));
throws(() => ed25519.getPublicKey(123n));
throws(() => x25519.getPublicKey(123n));
// Weierstrass still supports
secp256k1.getPublicKey(123n);
secp256k1.sign('', 123n);
});
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {

View File

@@ -656,6 +656,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.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {

View File

@@ -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],

View File

@@ -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);
},
};