8 Commits
0.6.1 ... 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
8 changed files with 143 additions and 66 deletions

View File

@@ -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.. - `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 ```ts
function expand_message_xmd( function expand_message_xmd(
msg: Uint8Array, DST: Uint8Array, lenInBytes: number, H: CHash msg: Uint8Array,
): Uint8Array; DST: Uint8Array,
function expand_message_xof( lenInBytes: number,
msg: Uint8Array, DST: Uint8Array, lenInBytes: number, k: number, H: CHash H: CHash
): Uint8Array; ): 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) - `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[][];
type htfOpts = { type htfOpts = {
// DST: a domain separation tag // DST: a domain separation tag
// defined in section 2.2.5 // defined in section 2.2.5
DST: string; DST: string;
// p: the characteristic of F // p: the characteristic of F
// where F is a finite field of characteristic p and order q = p^m // where F is a finite field of characteristic p and order q = p^m
p: bigint; p: bigint;
// m: the extension degree of F, m >= 1 // m: the extension degree of F, m >= 1
// where F is a finite field of characteristic p and order q = p^m // where F is a finite field of characteristic p and order q = p^m
m: number; m: number;
// k: the target security level for the suite in bits // k: the target security level for the suite in bits
// defined in section 5.1 // defined in section 5.1
k: number; k: number;
// option to use a message that has already been processed by // option to use a message that has already been processed by
// expand_message_xmd // expand_message_xmd
expand?: 'xmd' | 'xof'; expand?: 'xmd' | 'xof';
// Hash functions for: expand_message_xmd is appropriate for use with a // Hash functions for: expand_message_xmd is appropriate for use with a
// wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others. // wide range of hash functions, including SHA-2, SHA-3, BLAKE2, and others.
// BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247 // BBS+ uses blake2: https://github.com/hyperledger/aries-framework-go/issues/2247
// TODO: verify that hash is shake if expand==='xof' via types // TODO: verify that hash is shake if expand==='xof' via types
hash: CHash; hash: CHash;
}; };
``` ```
### abstract/poseidon: Poseidon hash ### abstract/poseidon: Poseidon hash
@@ -516,11 +523,11 @@ Upgrading from @noble/secp256k1 1.7:
- Compressed (33-byte) public keys are now returned by default, instead of uncompressed - 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 - Methods are now synchronous. Setting `secp.utils.hmacSha256` is no longer required
- `sign()` - `sign()`
- `der`, `recovered` options were removed - `der`, `recovered` options were removed
- `canonical` was renamed to `lowS` - `canonical` was renamed to `lowS`
- Return type is now `{ r: bigint, s: bigint, recovery: number }` instance of `Signature` - Return type is now `{ r: bigint, s: bigint, recovery: number }` instance of `Signature`
- `verify()` - `verify()`
- `strict` was renamed to `lowS` - `strict` was renamed to `lowS`
- `recoverPublicKey()`: moved to sig instance `Signature#recoverPublicKey(msgHash)` - `recoverPublicKey()`: moved to sig instance `Signature#recoverPublicKey(msgHash)`
- `Point` was removed: use `ProjectivePoint` in xyz coordinates - `Point` was removed: use `ProjectivePoint` in xyz coordinates
- `utils`: Many methods were removed, others were moved to `schnorr` namespace - `utils`: Many methods were removed, others were moved to `schnorr` namespace
@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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. // 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) {