Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2a7594eae | ||
|
|
823149ecd9 | ||
|
|
e57aec63d8 | ||
|
|
837aca98c9 | ||
|
|
dbb16b0e5e | ||
|
|
e14af67254 | ||
|
|
4780850748 | ||
|
|
3374a70f47 |
20
README.md
20
README.md
@@ -331,19 +331,26 @@ The module allows to hash arbitrary strings to elliptic curve points.
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
function expand_message_xmd(
|
function expand_message_xmd(
|
||||||
msg: Uint8Array, DST: Uint8Array, lenInBytes: number, H: CHash
|
msg: Uint8Array,
|
||||||
|
DST: Uint8Array,
|
||||||
|
lenInBytes: number,
|
||||||
|
H: CHash
|
||||||
): Uint8Array;
|
): Uint8Array;
|
||||||
function expand_message_xof(
|
function expand_message_xof(
|
||||||
msg: Uint8Array, DST: Uint8Array, lenInBytes: number, k: number, H: CHash
|
msg: Uint8Array,
|
||||||
|
DST: Uint8Array,
|
||||||
|
lenInBytes: number,
|
||||||
|
k: number,
|
||||||
|
H: CHash
|
||||||
): Uint8Array;
|
): Uint8Array;
|
||||||
```
|
```
|
||||||
|
|
||||||
- `hash_to_field(msg, count, options)` [(spec)](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3)
|
- `hash_to_field(msg, count, options)` [(spec)](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-5.3)
|
||||||
hashes arbitrary-length byte strings to a list of one or more elements of a finite field F.
|
hashes arbitrary-length byte strings to a list of one or more elements of a finite field F.
|
||||||
* `msg` a byte string containing the message to hash
|
_ `msg` a byte string containing the message to hash
|
||||||
* `count` the number of elements of F to output
|
_ `count` the number of elements of F to output
|
||||||
* `options` `{DST: string, p: bigint, m: number, k: number, expand: 'xmd' | 'xof', hash: H}`
|
_ `options` `{DST: string, p: bigint, m: number, k: number, expand: 'xmd' | 'xof', hash: H}`
|
||||||
* Returns `[u_0, ..., u_(count - 1)]`, a list of field elements.
|
_ Returns `[u_0, ..., u_(count - 1)]`, a list of field elements.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
function hash_to_field(msg: Uint8Array, count: number, options: htfOpts): bigint[][];
|
function hash_to_field(msg: Uint8Array, count: number, options: htfOpts): bigint[][];
|
||||||
@@ -532,6 +539,7 @@ Upgrading from @noble/ed25519 1.7:
|
|||||||
- `Point` was removed: use `ExtendedPoint` in xyzt coordinates
|
- `Point` was removed: use `ExtendedPoint` in xyzt coordinates
|
||||||
- `Signature` was removed
|
- `Signature` was removed
|
||||||
- `getSharedSecret` was removed: use separate x25519 sub-module
|
- `getSharedSecret` was removed: use separate x25519 sub-module
|
||||||
|
- `bigint` is no longer allowed in `getPublicKey`, `sign`, `verify`. Reason: ed25519 is LE, can lead to bugs
|
||||||
|
|
||||||
## Contributing & testing
|
## Contributing & testing
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@noble/curves",
|
"name": "@noble/curves",
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"description": "Minimal, auditable JS implementation of elliptic curve cryptography",
|
"description": "Minimal, auditable JS implementation of elliptic curve cryptography",
|
||||||
"files": [
|
"files": [
|
||||||
"lib"
|
"lib"
|
||||||
|
|||||||
@@ -171,8 +171,27 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|||||||
this._WINDOW_SIZE = windowSize;
|
this._WINDOW_SIZE = windowSize;
|
||||||
pointPrecomputes.delete(this);
|
pointPrecomputes.delete(this);
|
||||||
}
|
}
|
||||||
|
// Not required for fromHex(), which always creates valid points.
|
||||||
assertValidity(): void {}
|
// Could be useful for fromAffine().
|
||||||
|
assertValidity(): void {
|
||||||
|
const { a, d } = CURVE;
|
||||||
|
if (this.is0()) throw new Error('bad point: ZERO'); // TODO: optimize, with vars below?
|
||||||
|
// Equation in affine coordinates: ax² + y² = 1 + dx²y²
|
||||||
|
// Equation in projective coordinates (X/Z, Y/Z, Z): (aX² + Y²)Z² = Z⁴ + dX²Y²
|
||||||
|
const { ex: X, ey: Y, ez: Z, et: T } = this;
|
||||||
|
const X2 = modP(X * X); // X²
|
||||||
|
const Y2 = modP(Y * Y); // Y²
|
||||||
|
const Z2 = modP(Z * Z); // Z²
|
||||||
|
const Z4 = modP(Z2 * Z2); // Z⁴
|
||||||
|
const aX2 = modP(X2 * a); // aX²
|
||||||
|
const left = modP(Z2 * modP(aX2 + Y2)); // (aX² + Y²)Z²
|
||||||
|
const right = modP(Z4 + modP(d * modP(X2 * Y2))); // Z⁴ + dX²Y²
|
||||||
|
if (left !== right) throw new Error('bad point: equation left != right (1)');
|
||||||
|
// In Extended coordinates we also have T, which is x*y=T/Z: check X*Y == Z*T
|
||||||
|
const XY = modP(X * Y);
|
||||||
|
const ZT = modP(Z * T);
|
||||||
|
if (XY !== ZT) throw new Error('bad point: equation left != right (2)');
|
||||||
|
}
|
||||||
|
|
||||||
// Compare one point to another.
|
// Compare one point to another.
|
||||||
equals(other: Point): boolean {
|
equals(other: Point): boolean {
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ export function montgomery(curveDef: CurveType): CurveFn {
|
|||||||
// This is very ugly way, but it works because fieldLen-1 is outside of bounds for X448, so this becomes NOOP
|
// This is very ugly way, but it works because fieldLen-1 is outside of bounds for X448, so this becomes NOOP
|
||||||
// fieldLen - scalaryBytes = 1 for X448 and = 0 for X25519
|
// fieldLen - scalaryBytes = 1 for X448 and = 0 for X25519
|
||||||
const u = ensureBytes(uEnc, montgomeryBytes);
|
const u = ensureBytes(uEnc, montgomeryBytes);
|
||||||
u[fieldLen - 1] &= 127; // 0b0111_1111
|
// u[fieldLen-1] crashes QuickJS (TypeError: out-of-bound numeric index)
|
||||||
|
if (fieldLen === montgomeryBytes) u[fieldLen - 1] &= 127; // 0b0111_1111
|
||||||
return bytesToNumberLE(u);
|
return bytesToNumberLE(u);
|
||||||
}
|
}
|
||||||
function decodeScalar(n: Hex): bigint {
|
function decodeScalar(n: Hex): bigint {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function numberToHexUnpadded(num: number | bigint): string {
|
|||||||
export function hexToNumber(hex: string): bigint {
|
export function hexToNumber(hex: string): bigint {
|
||||||
if (typeof hex !== 'string') throw new Error('string expected, got ' + typeof hex);
|
if (typeof hex !== 'string') throw new Error('string expected, got ' + typeof hex);
|
||||||
// Big Endian
|
// Big Endian
|
||||||
return BigInt(`0x${hex}`);
|
return BigInt(hex === '' ? '0' : `0x${hex}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caching slows it down 2-3x
|
// Caching slows it down 2-3x
|
||||||
@@ -114,31 +114,47 @@ export const bitSet = (n: bigint, pos: number, value: boolean) =>
|
|||||||
// Not using ** operator with bigints for old engines.
|
// Not using ** operator with bigints for old engines.
|
||||||
export const bitMask = (n: number) => (_2n << BigInt(n - 1)) - _1n;
|
export const bitMask = (n: number) => (_2n << BigInt(n - 1)) - _1n;
|
||||||
|
|
||||||
type ValMap = Record<string, string>;
|
const validatorFns = {
|
||||||
export function validateObject(object: object, validators: ValMap, optValidators: ValMap = {}) {
|
bigint: (val: any) => typeof val === 'bigint',
|
||||||
const validatorFns: Record<string, (val: any) => boolean> = {
|
function: (val: any) => typeof val === 'function',
|
||||||
bigint: (val) => typeof val === 'bigint',
|
boolean: (val: any) => typeof val === 'boolean',
|
||||||
function: (val) => typeof val === 'function',
|
string: (val: any) => typeof val === 'string',
|
||||||
boolean: (val) => typeof val === 'boolean',
|
isSafeInteger: (val: any) => Number.isSafeInteger(val),
|
||||||
string: (val) => typeof val === 'string',
|
array: (val: any) => Array.isArray(val),
|
||||||
isSafeInteger: (val) => Number.isSafeInteger(val),
|
field: (val: any, object: any) => (object as any).Fp.isValid(val),
|
||||||
array: (val) => Array.isArray(val),
|
hash: (val: any) => typeof val === 'function' && Number.isSafeInteger(val.outputLen),
|
||||||
field: (val) => (object as any).Fp.isValid(val),
|
} as const;
|
||||||
hash: (val) => typeof val === 'function' && Number.isSafeInteger(val.outputLen),
|
type Validator = keyof typeof validatorFns;
|
||||||
};
|
type ValMap<T extends Record<string, any>> = { [K in keyof T]?: Validator };
|
||||||
// type Key = keyof typeof validators;
|
// type Record<K extends string | number | symbol, T> = { [P in K]: T; }
|
||||||
const checkField = (fieldName: string, type: string, isOptional: boolean) => {
|
|
||||||
|
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];
|
const checkVal = validatorFns[type];
|
||||||
if (typeof checkVal !== 'function')
|
if (typeof checkVal !== 'function')
|
||||||
throw new Error(`Invalid validator "${type}", expected function`);
|
throw new Error(`Invalid validator "${type}", expected function`);
|
||||||
|
|
||||||
const val = object[fieldName as keyof typeof object];
|
const val = object[fieldName as keyof typeof object];
|
||||||
if (isOptional && val === undefined) return;
|
if (isOptional && val === undefined) return;
|
||||||
if (!checkVal(val)) {
|
if (!checkVal(val, object)) {
|
||||||
throw new Error(`Invalid param ${fieldName}=${val} (${typeof val}), expected ${type}`);
|
throw new Error(
|
||||||
|
`Invalid param ${String(fieldName)}=${val} (${typeof val}), expected ${type}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for (let [fieldName, type] of Object.entries(validators)) checkField(fieldName, type, false);
|
for (const [fieldName, type] of Object.entries(validators)) checkField(fieldName, type!, false);
|
||||||
for (let [fieldName, type] of Object.entries(optValidators)) checkField(fieldName, type, true);
|
for (const [fieldName, type] of Object.entries(optValidators)) checkField(fieldName, type!, true);
|
||||||
return object;
|
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' });
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ function validatePointOpts<T>(curve: CurvePointsType<T>) {
|
|||||||
wrapPrivateKey: 'boolean',
|
wrapPrivateKey: 'boolean',
|
||||||
isTorsionFree: 'function',
|
isTorsionFree: 'function',
|
||||||
clearCofactor: 'function',
|
clearCofactor: 'function',
|
||||||
|
allowInfinityPoint: 'boolean',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const { endo, Fp, a } = opts;
|
const { endo, Fp, a } = opts;
|
||||||
@@ -240,6 +241,8 @@ export function weierstrassPoints<T>(opts: CurvePointsType<T>) {
|
|||||||
if (pz == null || !Fp.isValid(pz)) throw new Error('z required');
|
if (pz == null || !Fp.isValid(pz)) throw new Error('z required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Does not validate if the point is on-curve.
|
||||||
|
// Use fromHex instead, or call assertValidity() later.
|
||||||
static fromAffine(p: AffinePoint<T>): Point {
|
static fromAffine(p: AffinePoint<T>): Point {
|
||||||
const { x, y } = p || {};
|
const { x, y } = p || {};
|
||||||
if (!p || !Fp.isValid(x) || !Fp.isValid(y)) throw new Error('invalid affine point');
|
if (!p || !Fp.isValid(x) || !Fp.isValid(y)) throw new Error('invalid affine point');
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { secp256r1 } from '../lib/esm/p256.js';
|
|||||||
import { secp384r1 } from '../lib/esm/p384.js';
|
import { secp384r1 } from '../lib/esm/p384.js';
|
||||||
import { secp521r1 } from '../lib/esm/p521.js';
|
import { secp521r1 } from '../lib/esm/p521.js';
|
||||||
import { secp256k1 } from '../lib/esm/secp256k1.js';
|
import { secp256k1 } from '../lib/esm/secp256k1.js';
|
||||||
import { ed25519, ed25519ctx, ed25519ph } from '../lib/esm/ed25519.js';
|
import { ed25519, ed25519ctx, ed25519ph, x25519 } from '../lib/esm/ed25519.js';
|
||||||
import { ed448, ed448ph } from '../lib/esm/ed448.js';
|
import { ed448, ed448ph } from '../lib/esm/ed448.js';
|
||||||
import { starkCurve } from '../lib/esm/stark.js';
|
import { starkCurve } from '../lib/esm/stark.js';
|
||||||
import { pallas, vesta } from '../lib/esm/pasta.js';
|
import { pallas, vesta } from '../lib/esm/pasta.js';
|
||||||
@@ -567,6 +567,17 @@ for (const name in CURVES) {
|
|||||||
{ numRuns: NUM_RUNS }
|
{ numRuns: NUM_RUNS }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
should('.verify() should verify empty signatures', () => {
|
||||||
|
const msg = new Uint8Array([]);
|
||||||
|
const priv = C.utils.randomPrivateKey();
|
||||||
|
const pub = C.getPublicKey(priv);
|
||||||
|
const sig = C.sign(msg, priv);
|
||||||
|
deepStrictEqual(
|
||||||
|
C.verify(sig, msg, pub),
|
||||||
|
true,
|
||||||
|
'priv=${toHex(priv)},pub=${toHex(pub)},msg=${msg}'
|
||||||
|
);
|
||||||
|
});
|
||||||
should('.sign() edge cases', () => {
|
should('.sign() edge cases', () => {
|
||||||
throws(() => C.sign());
|
throws(() => C.sign());
|
||||||
throws(() => C.sign(''));
|
throws(() => C.sign(''));
|
||||||
@@ -651,6 +662,16 @@ should('secp224k1 sqrt bug', () => {
|
|||||||
deepStrictEqual(Fp.sqr(sqrtMinus1), Fp.create(-1n));
|
deepStrictEqual(Fp.sqr(sqrtMinus1), Fp.create(-1n));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
should('bigInt private keys', () => {
|
||||||
|
// Doesn't support bigints anymore
|
||||||
|
throws(() => ed25519.sign('', 123n));
|
||||||
|
throws(() => ed25519.getPublicKey(123n));
|
||||||
|
throws(() => x25519.getPublicKey(123n));
|
||||||
|
// Weierstrass still supports
|
||||||
|
secp256k1.getPublicKey(123n);
|
||||||
|
secp256k1.sign('', 123n);
|
||||||
|
});
|
||||||
|
|
||||||
// ESM is broken.
|
// ESM is broken.
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
||||||
|
|||||||
@@ -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.
|
// ESM is broken.
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
||||||
|
|||||||
Reference in New Issue
Block a user