commit
af8c1eebee
48
README.md
48
README.md
@ -149,15 +149,25 @@ edwardsToMontgomeryPub(ed25519.getPublicKey(ed25519.utils.randomPrivateKey()));
|
|||||||
edwardsToMontgomeryPriv(ed25519.utils.randomPrivateKey());
|
edwardsToMontgomeryPriv(ed25519.utils.randomPrivateKey());
|
||||||
|
|
||||||
// hash-to-curve, ristretto255
|
// hash-to-curve, ristretto255
|
||||||
import { hashToCurve, encodeToCurve, RistrettoPoint } from '@noble/curves/ed25519';
|
import { utf8ToBytes } from '@noble/hashes/utils';
|
||||||
|
import { sha512 } from '@noble/hashes/sha512';
|
||||||
|
import { hashToCurve, encodeToCurve, RistrettoPoint, hash_to_ristretto255 } from '@noble/curves/ed25519';
|
||||||
|
|
||||||
|
const msg = utf8ToBytes('Ristretto is traditionally a short shot of espresso coffee');
|
||||||
|
hashToCurve(msg);
|
||||||
|
|
||||||
const rp = RistrettoPoint.fromHex(
|
const rp = RistrettoPoint.fromHex(
|
||||||
'6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919'
|
'6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919'
|
||||||
);
|
);
|
||||||
RistrettoPoint.hashToCurve('Ristretto is traditionally a short shot of espresso coffee');
|
RistrettoPoint.BASE.multiply(2n).add(rp).subtract(RistrettoPoint.BASE).toRawBytes();
|
||||||
// also has add(), equals(), multiply(), toRawBytes() methods
|
RistrettoPoint.ZERO.equals(dp) === false;
|
||||||
|
// pre-hashed hash-to-curve
|
||||||
|
RistrettoPoint.hashToCurve(sha512(msg));
|
||||||
|
// full hash-to-curve including domain separation tag
|
||||||
|
hash_to_ristretto255(msg, { DST: 'ristretto255_XMD:SHA-512_R255MAP_RO_' });
|
||||||
```
|
```
|
||||||
|
|
||||||
#### ed448, X448
|
#### ed448, X448, decaf448
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { ed448 } from '@noble/curves/ed448';
|
import { ed448 } from '@noble/curves/ed448';
|
||||||
@ -167,12 +177,38 @@ const msg = new TextEncoder().encode('whatsup');
|
|||||||
const sig = ed448.sign(msg, priv);
|
const sig = ed448.sign(msg, priv);
|
||||||
ed448.verify(sig, msg, pub);
|
ed448.verify(sig, msg, pub);
|
||||||
|
|
||||||
import { ed448ph, ed448ctx, x448, hashToCurve, encodeToCurve } from '@noble/curves/ed448';
|
// Variants from RFC8032: prehashed
|
||||||
|
import { ed448ph } from '@noble/curves/ed448';
|
||||||
|
|
||||||
|
// ECDH using curve448 aka x448
|
||||||
|
import { x448 } from '@noble/curves/ed448';
|
||||||
x448.getSharedSecret(priv, pub) === x448.scalarMult(priv, pub); // aliases
|
x448.getSharedSecret(priv, pub) === x448.scalarMult(priv, pub); // aliases
|
||||||
x448.getPublicKey(priv) === x448.scalarMultBase(priv);
|
x448.getPublicKey(priv) === x448.scalarMultBase(priv);
|
||||||
|
|
||||||
|
// ed448 => x448 conversion
|
||||||
|
import { edwardsToMontgomeryPub } from '@noble/curves/ed448';
|
||||||
|
edwardsToMontgomeryPub(ed448.getPublicKey(ed448.utils.randomPrivateKey()));
|
||||||
|
|
||||||
|
// hash-to-curve, decaf448
|
||||||
|
import { utf8ToBytes } from '@noble/hashes/utils';
|
||||||
|
import { shake256 } from '@noble/hashes/sha3';
|
||||||
|
import { hashToCurve, encodeToCurve, DecafPoint, hash_to_decaf448 } from '@noble/curves/ed448';
|
||||||
|
|
||||||
|
const msg = utf8ToBytes('Ristretto is traditionally a short shot of espresso coffee');
|
||||||
|
hashToCurve(msg);
|
||||||
|
|
||||||
|
const dp = DecafPoint.fromHex(
|
||||||
|
'c898eb4f87f97c564c6fd61fc7e49689314a1f818ec85eeb3bd5514ac816d38778f69ef347a89fca817e66defdedce178c7cc709b2116e75'
|
||||||
|
);
|
||||||
|
DecafPoint.BASE.multiply(2n).add(dp).subtract(DecafPoint.BASE).toRawBytes();
|
||||||
|
DecafPoint.ZERO.equals(dp) === false;
|
||||||
|
// pre-hashed hash-to-curve
|
||||||
|
DecafPoint.hashToCurve(shake256(msg, { dkLen: 112 }));
|
||||||
|
// full hash-to-curve including domain separation tag
|
||||||
|
hash_to_decaf448(msg, { DST: 'decaf448_XOF:SHAKE256_D448MAP_RO_' });
|
||||||
```
|
```
|
||||||
|
|
||||||
Same RFC7748 / RFC8032 are followed.
|
Same RFC7748 / RFC8032 / IRTF draft are followed.
|
||||||
|
|
||||||
#### bls12-381
|
#### bls12-381
|
||||||
|
|
||||||
|
18
benchmark/decaf448.js
Normal file
18
benchmark/decaf448.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { run, mark, utils } from 'micro-bmark';
|
||||||
|
import { shake256 } from '@noble/hashes/sha3';
|
||||||
|
import * as mod from '../abstract/modular.js';
|
||||||
|
import { ed448, DecafPoint } from '../ed448.js';
|
||||||
|
|
||||||
|
run(async () => {
|
||||||
|
const RAM = false;
|
||||||
|
if (RAM) utils.logMem();
|
||||||
|
console.log(`\x1b[36mdecaf448\x1b[0m`);
|
||||||
|
const priv = mod.hashToPrivateScalar(shake256(ed448.utils.randomPrivateKey(), { dkLen: 112 }), ed448.CURVE.n);
|
||||||
|
const pub = DecafPoint.BASE.multiply(priv);
|
||||||
|
const encoded = pub.toRawBytes();
|
||||||
|
await mark('add', 1000000, () => pub.add(DecafPoint.BASE));
|
||||||
|
await mark('multiply', 1000, () => DecafPoint.BASE.multiply(priv));
|
||||||
|
await mark('encode', 10000, () => DecafPoint.BASE.toRawBytes());
|
||||||
|
await mark('decode', 10000, () => DecafPoint.fromHex(encoded));
|
||||||
|
if (RAM) utils.logMem();
|
||||||
|
});
|
@ -8,8 +8,8 @@ import { hashToCurve as secp256k1 } from '../secp256k1.js';
|
|||||||
import { hashToCurve as p256 } from '../p256.js';
|
import { hashToCurve as p256 } from '../p256.js';
|
||||||
import { hashToCurve as p384 } from '../p384.js';
|
import { hashToCurve as p384 } from '../p384.js';
|
||||||
import { hashToCurve as p521 } from '../p521.js';
|
import { hashToCurve as p521 } from '../p521.js';
|
||||||
import { hashToCurve as ed25519 } from '../ed25519.js';
|
import { hashToCurve as ed25519, hash_to_ristretto255 } from '../ed25519.js';
|
||||||
import { hashToCurve as ed448 } from '../ed448.js';
|
import { hashToCurve as ed448, hash_to_decaf448 } from '../ed448.js';
|
||||||
import { utf8ToBytes } from '../abstract/utils.js';
|
import { utf8ToBytes } from '../abstract/utils.js';
|
||||||
|
|
||||||
const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
||||||
@ -26,4 +26,7 @@ run(async () => {
|
|||||||
for (let [title, fn] of Object.entries({ secp256k1, p256, p384, p521, ed25519, ed448 })) {
|
for (let [title, fn] of Object.entries({ secp256k1, p256, p384, p521, ed25519, ed448 })) {
|
||||||
await mark(`hashToCurve ${title}`, 1000, () => fn(msg));
|
await mark(`hashToCurve ${title}`, 1000, () => fn(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await mark('hash_to_ristretto255', 1000, () => hash_to_ristretto255(msg, { DST: 'ristretto255_XMD:SHA-512_R255MAP_RO_' }));
|
||||||
|
await mark('hash_to_decaf448', 1000, () => hash_to_decaf448(msg, { DST: 'decaf448_XOF:SHAKE256_D448MAP_RO_' }));
|
||||||
});
|
});
|
||||||
|
18
benchmark/ristretto255.js
Normal file
18
benchmark/ristretto255.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { run, mark, utils } from 'micro-bmark';
|
||||||
|
import { sha512 } from '@noble/hashes/sha512';
|
||||||
|
import * as mod from '../abstract/modular.js';
|
||||||
|
import { ed25519, RistrettoPoint } from '../ed25519.js';
|
||||||
|
|
||||||
|
run(async () => {
|
||||||
|
const RAM = false;
|
||||||
|
if (RAM) utils.logMem();
|
||||||
|
console.log(`\x1b[36mristretto255\x1b[0m`);
|
||||||
|
const priv = mod.hashToPrivateScalar(sha512(ed25519.utils.randomPrivateKey()), ed25519.CURVE.n);
|
||||||
|
const pub = RistrettoPoint.BASE.multiply(priv);
|
||||||
|
const encoded = pub.toRawBytes();
|
||||||
|
await mark('add', 1000000, () => pub.add(RistrettoPoint.BASE));
|
||||||
|
await mark('multiply', 10000, () => RistrettoPoint.BASE.multiply(priv));
|
||||||
|
await mark('encode', 10000, () => RistrettoPoint.BASE.toRawBytes());
|
||||||
|
await mark('decode', 10000, () => RistrettoPoint.fromHex(encoded));
|
||||||
|
if (RAM) utils.logMem();
|
||||||
|
});
|
@ -12,7 +12,7 @@
|
|||||||
"*.d.ts.map"
|
"*.d.ts.map"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bench": "cd benchmark; node secp256k1.js; node curves.js; node ecdh.js; node hash-to-curve.js; node modular.js; node bls.js",
|
"bench": "cd benchmark; node secp256k1.js; node curves.js; node ecdh.js; node hash-to-curve.js; node modular.js; node bls.js; node ristretto255.js; node decaf448.js",
|
||||||
"build": "tsc && tsc -p tsconfig.esm.json",
|
"build": "tsc && tsc -p tsconfig.esm.json",
|
||||||
"build:release": "rollup -c rollup.config.js",
|
"build:release": "rollup -c rollup.config.js",
|
||||||
"build:clean": "rm *.{js,d.ts,d.ts.map,js.map} esm/*.{js,d.ts,d.ts.map,js.map} 2> /dev/null",
|
"build:clean": "rm *.{js,d.ts,d.ts.map,js.map} esm/*.{js,d.ts,d.ts.map,js.map} 2> /dev/null",
|
||||||
|
280
src/ed448.ts
280
src/ed448.ts
@ -1,14 +1,25 @@
|
|||||||
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
||||||
import { shake256 } from '@noble/hashes/sha3';
|
import { shake256 } from '@noble/hashes/sha3';
|
||||||
import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/hashes/utils';
|
import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/hashes/utils';
|
||||||
import { twistedEdwards } from './abstract/edwards.js';
|
import { ExtPointType, twistedEdwards } from './abstract/edwards.js';
|
||||||
import { mod, pow2, Field } from './abstract/modular.js';
|
import { mod, pow2, Field, isNegativeLE } from './abstract/modular.js';
|
||||||
import { montgomery } from './abstract/montgomery.js';
|
import { montgomery } from './abstract/montgomery.js';
|
||||||
import { createHasher } from './abstract/hash-to-curve.js';
|
import { createHasher, htfBasicOpts, expand_message_xof } from './abstract/hash-to-curve.js';
|
||||||
|
import {
|
||||||
|
bytesToHex,
|
||||||
|
bytesToNumberLE,
|
||||||
|
ensureBytes,
|
||||||
|
equalBytes,
|
||||||
|
Hex,
|
||||||
|
numberToBytesLE,
|
||||||
|
} from './abstract/utils.js';
|
||||||
|
import { AffinePoint } from './abstract/curve.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edwards448 (not Ed448-Goldilocks) curve with following addons:
|
* Edwards448 (not Ed448-Goldilocks) curve with following addons:
|
||||||
* * X448 ECDH
|
* - X448 ECDH
|
||||||
|
* - Decaf cofactor elimination
|
||||||
|
* - Elligator hash-to-group / point indistinguishability
|
||||||
* Conforms to RFC 8032 https://www.rfc-editor.org/rfc/rfc8032.html#section-5.2
|
* Conforms to RFC 8032 https://www.rfc-editor.org/rfc/rfc8032.html#section-5.2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -18,15 +29,16 @@ const ed448P = BigInt(
|
|||||||
'726838724295606890549323807888004534353641360687318060281490199180612328166730772686396383698676545930088884461843637361053498018365439'
|
'726838724295606890549323807888004534353641360687318060281490199180612328166730772686396383698676545930088884461843637361053498018365439'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3), _4n = BigInt(4), _11n = BigInt(11);
|
||||||
|
// prettier-ignore
|
||||||
|
const _22n = BigInt(22), _44n = BigInt(44), _88n = BigInt(88), _223n = BigInt(223);
|
||||||
|
|
||||||
// powPminus3div4 calculates z = x^k mod p, where k = (p-3)/4.
|
// powPminus3div4 calculates z = x^k mod p, where k = (p-3)/4.
|
||||||
// Used for efficient square root calculation.
|
// Used for efficient square root calculation.
|
||||||
// ((P-3)/4).toString(2) would produce bits [223x 1, 0, 222x 1]
|
// ((P-3)/4).toString(2) would produce bits [223x 1, 0, 222x 1]
|
||||||
function ed448_pow_Pminus3div4(x: bigint): bigint {
|
function ed448_pow_Pminus3div4(x: bigint): bigint {
|
||||||
const P = ed448P;
|
const P = ed448P;
|
||||||
// prettier-ignore
|
|
||||||
const _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3), _11n = BigInt(11);
|
|
||||||
// prettier-ignore
|
|
||||||
const _22n = BigInt(22), _44n = BigInt(44), _88n = BigInt(88), _223n = BigInt(223);
|
|
||||||
const b2 = (x * x * x) % P;
|
const b2 = (x * x * x) % P;
|
||||||
const b3 = (b2 * b2 * x) % P;
|
const b3 = (b2 * b2 * x) % P;
|
||||||
const b6 = (pow2(b3, _3n, P) * b3) % P;
|
const b6 = (pow2(b3, _3n, P) * b3) % P;
|
||||||
@ -53,8 +65,29 @@ function adjustScalarBytes(bytes: Uint8Array): Uint8Array {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Constant-time ratio of u to v. Allows to combine inversion and square root u/√v.
|
||||||
|
// Uses algo from RFC8032 5.1.3.
|
||||||
|
function uvRatio(u: bigint, v: bigint): { isValid: boolean; value: bigint } {
|
||||||
|
const P = ed448P;
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8032#section-5.2.3
|
||||||
|
// To compute the square root of (u/v), the first step is to compute the
|
||||||
|
// candidate root x = (u/v)^((p+1)/4). This can be done using the
|
||||||
|
// following trick, to use a single modular powering for both the
|
||||||
|
// inversion of v and the square root:
|
||||||
|
// x = (u/v)^((p+1)/4) = u³v(u⁵v³)^((p-3)/4) (mod p)
|
||||||
|
const u2v = mod(u * u * v, P); // u²v
|
||||||
|
const u3v = mod(u2v * u, P); // u³v
|
||||||
|
const u5v3 = mod(u3v * u2v * v, P); // u⁵v³
|
||||||
|
const root = ed448_pow_Pminus3div4(u5v3);
|
||||||
|
const x = mod(u3v * root, P);
|
||||||
|
// Verify that root is exists
|
||||||
|
const x2 = mod(x * x, P); // x²
|
||||||
|
// If vx² = u, the recovered x-coordinate is x. Otherwise, no
|
||||||
|
// square root exists, and the decoding fails.
|
||||||
|
return { isValid: mod(x2 * v, P) === u, value: x };
|
||||||
|
}
|
||||||
|
|
||||||
const Fp = Field(ed448P, 456, true);
|
const Fp = Field(ed448P, 456, true);
|
||||||
const _4n = BigInt(4);
|
|
||||||
|
|
||||||
const ED448_DEF = {
|
const ED448_DEF = {
|
||||||
// Param: a
|
// Param: a
|
||||||
@ -94,28 +127,7 @@ const ED448_DEF = {
|
|||||||
data
|
data
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
uvRatio,
|
||||||
// Constant-time ratio of u to v. Allows to combine inversion and square root u/√v.
|
|
||||||
// Uses algo from RFC8032 5.1.3.
|
|
||||||
uvRatio: (u: bigint, v: bigint): { isValid: boolean; value: bigint } => {
|
|
||||||
const P = ed448P;
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc8032#section-5.2.3
|
|
||||||
// To compute the square root of (u/v), the first step is to compute the
|
|
||||||
// candidate root x = (u/v)^((p+1)/4). This can be done using the
|
|
||||||
// following trick, to use a single modular powering for both the
|
|
||||||
// inversion of v and the square root:
|
|
||||||
// x = (u/v)^((p+1)/4) = u³v(u⁵v³)^((p-3)/4) (mod p)
|
|
||||||
const u2v = mod(u * u * v, P); // u²v
|
|
||||||
const u3v = mod(u2v * u, P); // u³v
|
|
||||||
const u5v3 = mod(u3v * u2v * v, P); // u⁵v³
|
|
||||||
const root = ed448_pow_Pminus3div4(u5v3);
|
|
||||||
const x = mod(u3v * root, P);
|
|
||||||
// Verify that root is exists
|
|
||||||
const x2 = mod(x * x, P); // x²
|
|
||||||
// If vx² = u, the recovered x-coordinate is x. Otherwise, no
|
|
||||||
// square root exists, and the decoding fails.
|
|
||||||
return { isValid: mod(x2 * v, P) === u, value: x };
|
|
||||||
},
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ed448 = twistedEdwards(ED448_DEF);
|
export const ed448 = twistedEdwards(ED448_DEF);
|
||||||
@ -245,3 +257,209 @@ const htf = /* @__PURE__ */ (() =>
|
|||||||
))();
|
))();
|
||||||
export const hashToCurve = /* @__PURE__ */ (() => htf.hashToCurve)();
|
export const hashToCurve = /* @__PURE__ */ (() => htf.hashToCurve)();
|
||||||
export const encodeToCurve = /* @__PURE__ */ (() => htf.encodeToCurve)();
|
export const encodeToCurve = /* @__PURE__ */ (() => htf.encodeToCurve)();
|
||||||
|
|
||||||
|
function assertDcfPoint(other: unknown) {
|
||||||
|
if (!(other instanceof DcfPoint)) throw new Error('DecafPoint expected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1-d
|
||||||
|
const ONE_MINUS_D = BigInt('39082');
|
||||||
|
// 1-2d
|
||||||
|
const ONE_MINUS_TWO_D = BigInt('78163');
|
||||||
|
// √(-d)
|
||||||
|
const SQRT_MINUS_D = BigInt(
|
||||||
|
'98944233647732219769177004876929019128417576295529901074099889598043702116001257856802131563896515373927712232092845883226922417596214'
|
||||||
|
);
|
||||||
|
// 1 / √(-d)
|
||||||
|
const INVSQRT_MINUS_D = BigInt(
|
||||||
|
'315019913931389607337177038330951043522456072897266928557328499619017160722351061360252776265186336876723201881398623946864393857820716'
|
||||||
|
);
|
||||||
|
// Calculates 1/√(number)
|
||||||
|
const invertSqrt = (number: bigint) => uvRatio(_1n, number);
|
||||||
|
|
||||||
|
const MAX_448B = BigInt(
|
||||||
|
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
|
||||||
|
);
|
||||||
|
const bytes448ToNumberLE = (bytes: Uint8Array) =>
|
||||||
|
ed448.CURVE.Fp.create(bytesToNumberLE(bytes) & MAX_448B);
|
||||||
|
|
||||||
|
type ExtendedPoint = ExtPointType;
|
||||||
|
|
||||||
|
// Computes Elligator map for Decaf
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448-07#name-element-derivation-2
|
||||||
|
function calcElligatorDecafMap(r0: bigint): ExtendedPoint {
|
||||||
|
const { d } = ed448.CURVE;
|
||||||
|
const P = ed448.CURVE.Fp.ORDER;
|
||||||
|
const mod = ed448.CURVE.Fp.create;
|
||||||
|
|
||||||
|
const r = mod(-(r0 * r0)); // 1
|
||||||
|
const u0 = mod(d * (r - _1n)); // 2
|
||||||
|
const u1 = mod((u0 + _1n) * (u0 - r)); // 3
|
||||||
|
|
||||||
|
const { isValid: was_square, value: v } = uvRatio(ONE_MINUS_TWO_D, mod((r + _1n) * u1)); // 4
|
||||||
|
|
||||||
|
let v_prime = v; // 5
|
||||||
|
if (!was_square) v_prime = mod(r0 * v);
|
||||||
|
|
||||||
|
let sgn = _1n; // 6
|
||||||
|
if (!was_square) sgn = mod(-_1n);
|
||||||
|
|
||||||
|
const s = mod(v_prime * (r + _1n)); // 7
|
||||||
|
let s_abs = s;
|
||||||
|
if (isNegativeLE(s, P)) s_abs = mod(-s);
|
||||||
|
|
||||||
|
const s2 = s * s;
|
||||||
|
const W0 = mod(s_abs * _2n); // 8
|
||||||
|
const W1 = mod(s2 + _1n); // 9
|
||||||
|
const W2 = mod(s2 - _1n); // 10
|
||||||
|
const W3 = mod(v_prime * s * (r - _1n) * ONE_MINUS_TWO_D + sgn); // 11
|
||||||
|
return new ed448.ExtendedPoint(mod(W0 * W3), mod(W2 * W1), mod(W1 * W3), mod(W0 * W2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each ed448/ExtendedPoint has 4 different equivalent points. This can be
|
||||||
|
* a source of bugs for protocols like ring signatures. Decaf was created to solve this.
|
||||||
|
* Decaf point operates in X:Y:Z:T extended coordinates like ExtendedPoint,
|
||||||
|
* but it should work in its own namespace: do not combine those two.
|
||||||
|
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448
|
||||||
|
*/
|
||||||
|
class DcfPoint {
|
||||||
|
static BASE: DcfPoint;
|
||||||
|
static ZERO: DcfPoint;
|
||||||
|
// Private property to discourage combining ExtendedPoint + DecafPoint
|
||||||
|
// Always use Decaf encoding/decoding instead.
|
||||||
|
constructor(private readonly ep: ExtendedPoint) {}
|
||||||
|
|
||||||
|
static fromAffine(ap: AffinePoint<bigint>) {
|
||||||
|
return new DcfPoint(ed448.ExtendedPoint.fromAffine(ap));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes uniform output of 112-byte hash function like shake256 and converts it to `DecafPoint`.
|
||||||
|
* The hash-to-group operation applies Elligator twice and adds the results.
|
||||||
|
* **Note:** this is one-way map, there is no conversion from point to hash.
|
||||||
|
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448-07#name-element-derivation-2
|
||||||
|
* @param hex 112-byte output of a hash function
|
||||||
|
*/
|
||||||
|
static hashToCurve(hex: Hex): DcfPoint {
|
||||||
|
hex = ensureBytes('decafHash', hex, 112);
|
||||||
|
const r1 = bytes448ToNumberLE(hex.slice(0, 56));
|
||||||
|
const R1 = calcElligatorDecafMap(r1);
|
||||||
|
const r2 = bytes448ToNumberLE(hex.slice(56, 112));
|
||||||
|
const R2 = calcElligatorDecafMap(r2);
|
||||||
|
return new DcfPoint(R1.add(R2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts decaf-encoded string to decaf point.
|
||||||
|
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448-07#name-decode-2
|
||||||
|
* @param hex Decaf-encoded 56 bytes. Not every 56-byte string is valid decaf encoding
|
||||||
|
*/
|
||||||
|
static fromHex(hex: Hex): DcfPoint {
|
||||||
|
hex = ensureBytes('decafHex', hex, 56);
|
||||||
|
const { d } = ed448.CURVE;
|
||||||
|
const P = ed448.CURVE.Fp.ORDER;
|
||||||
|
const mod = ed448.CURVE.Fp.create;
|
||||||
|
const emsg = 'DecafPoint.fromHex: the hex is not valid encoding of DecafPoint';
|
||||||
|
const s = bytes448ToNumberLE(hex);
|
||||||
|
|
||||||
|
// 1. Check that s_bytes is the canonical encoding of a field element, or else abort.
|
||||||
|
// 2. Check that s is non-negative, or else abort
|
||||||
|
if (!equalBytes(numberToBytesLE(s, 56), hex) || isNegativeLE(s, P)) throw new Error(emsg);
|
||||||
|
|
||||||
|
const s2 = mod(s * s); // 1
|
||||||
|
const u1 = mod(_1n + s2); // 2
|
||||||
|
const u1sq = mod(u1 * u1);
|
||||||
|
const u2 = mod(u1sq - _4n * d * s2); // 3
|
||||||
|
|
||||||
|
const { isValid, value: invsqrt } = invertSqrt(mod(u2 * u1sq)); // 4
|
||||||
|
|
||||||
|
let u3 = mod((s + s) * invsqrt * u1 * SQRT_MINUS_D); // 5
|
||||||
|
if (isNegativeLE(u3, P)) u3 = mod(-u3);
|
||||||
|
|
||||||
|
const x = mod(u3 * invsqrt * u2 * INVSQRT_MINUS_D); // 6
|
||||||
|
const y = mod((_1n - s2) * invsqrt * u1); // 7
|
||||||
|
const t = mod(x * y); // 8
|
||||||
|
|
||||||
|
if (!isValid) throw new Error(emsg);
|
||||||
|
return new DcfPoint(new ed448.ExtendedPoint(x, y, _1n, t));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes decaf point to Uint8Array.
|
||||||
|
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448-07#name-encode-2
|
||||||
|
*/
|
||||||
|
toRawBytes(): Uint8Array {
|
||||||
|
let { ex: x, ey: _y, ez: z, et: t } = this.ep;
|
||||||
|
const P = ed448.CURVE.Fp.ORDER;
|
||||||
|
const mod = ed448.CURVE.Fp.create;
|
||||||
|
|
||||||
|
const u1 = mod(mod(x + t) * mod(x - t)); // 1
|
||||||
|
const x2 = mod(x * x);
|
||||||
|
const { value: invsqrt } = invertSqrt(mod(u1 * ONE_MINUS_D * x2)); // 2
|
||||||
|
|
||||||
|
let ratio = mod(invsqrt * u1 * SQRT_MINUS_D); // 3
|
||||||
|
if (isNegativeLE(ratio, P)) ratio = mod(-ratio);
|
||||||
|
|
||||||
|
const u2 = mod(INVSQRT_MINUS_D * ratio * z - t); // 4
|
||||||
|
|
||||||
|
let s = mod(ONE_MINUS_D * invsqrt * x * u2); // 5
|
||||||
|
if (isNegativeLE(s, P)) s = mod(-s);
|
||||||
|
|
||||||
|
return numberToBytesLE(s, 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
toHex(): string {
|
||||||
|
return bytesToHex(this.toRawBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare one point to another.
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448-07#name-equals-2
|
||||||
|
equals(other: DcfPoint): boolean {
|
||||||
|
assertDcfPoint(other);
|
||||||
|
const { ex: X1, ey: Y1 } = this.ep;
|
||||||
|
const { ex: X2, ey: Y2 } = other.ep;
|
||||||
|
const mod = ed448.CURVE.Fp.create;
|
||||||
|
// (x1 * y2 == y1 * x2)
|
||||||
|
return mod(X1 * Y2) === mod(Y1 * X2);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(other: DcfPoint): DcfPoint {
|
||||||
|
assertDcfPoint(other);
|
||||||
|
return new DcfPoint(this.ep.add(other.ep));
|
||||||
|
}
|
||||||
|
|
||||||
|
subtract(other: DcfPoint): DcfPoint {
|
||||||
|
assertDcfPoint(other);
|
||||||
|
return new DcfPoint(this.ep.subtract(other.ep));
|
||||||
|
}
|
||||||
|
|
||||||
|
multiply(scalar: bigint): DcfPoint {
|
||||||
|
return new DcfPoint(this.ep.multiply(scalar));
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplyUnsafe(scalar: bigint): DcfPoint {
|
||||||
|
return new DcfPoint(this.ep.multiplyUnsafe(scalar));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const DecafPoint = /* @__PURE__ */ (() => {
|
||||||
|
// decaf448 base point is ed448 base x 2
|
||||||
|
// https://github.com/dalek-cryptography/curve25519-dalek/blob/59837c6ecff02b77b9d5ff84dbc239d0cf33ef90/vendor/ristretto.sage#L699
|
||||||
|
if (!DcfPoint.BASE) DcfPoint.BASE = new DcfPoint(ed448.ExtendedPoint.BASE).multiply(_2n);
|
||||||
|
if (!DcfPoint.ZERO) DcfPoint.ZERO = new DcfPoint(ed448.ExtendedPoint.ZERO);
|
||||||
|
return DcfPoint;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/16/
|
||||||
|
// Appendix C. Hashing to decaf448
|
||||||
|
export const hash_to_decaf448 = (msg: Uint8Array, options: htfBasicOpts) => {
|
||||||
|
const d = options.DST;
|
||||||
|
const DST = typeof d === 'string' ? utf8ToBytes(d) : d;
|
||||||
|
const uniform_bytes = expand_message_xof(msg, DST, 112, 224, shake256);
|
||||||
|
const P = DcfPoint.hashToCurve(uniform_bytes);
|
||||||
|
return P;
|
||||||
|
};
|
||||||
|
117
test/ed448-addons.test.js
Normal file
117
test/ed448-addons.test.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { bytesToHex as hex, hexToBytes } from '@noble/hashes/utils';
|
||||||
|
import { deepStrictEqual, throws } from 'assert';
|
||||||
|
import { describe, should } from 'micro-should';
|
||||||
|
import { bytesToNumberLE } from '../esm/abstract/utils.js';
|
||||||
|
import { ed448, DecafPoint } from '../esm/ed448.js';
|
||||||
|
|
||||||
|
describe('decaf448', () => {
|
||||||
|
should('follow the byte encodings of small multiples', () => {
|
||||||
|
const encodingsOfSmallMultiples = [
|
||||||
|
// This is the identity point
|
||||||
|
'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
|
||||||
|
// This is the basepoint
|
||||||
|
'6666666666666666666666666666666666666666666666666666666633333333333333333333333333333333333333333333333333333333',
|
||||||
|
// These are small multiples of the basepoint
|
||||||
|
'c898eb4f87f97c564c6fd61fc7e49689314a1f818ec85eeb3bd5514ac816d38778f69ef347a89fca817e66defdedce178c7cc709b2116e75',
|
||||||
|
'a0c09bf2ba7208fda0f4bfe3d0f5b29a543012306d43831b5adc6fe7f8596fa308763db15468323b11cf6e4aeb8c18fe44678f44545a69bc',
|
||||||
|
'b46f1836aa287c0a5a5653f0ec5ef9e903f436e21c1570c29ad9e5f596da97eeaf17150ae30bcb3174d04bc2d712c8c7789d7cb4fda138f4',
|
||||||
|
'1c5bbecf4741dfaae79db72dface00eaaac502c2060934b6eaaeca6a20bd3da9e0be8777f7d02033d1b15884232281a41fc7f80eed04af5e',
|
||||||
|
'86ff0182d40f7f9edb7862515821bd67bfd6165a3c44de95d7df79b8779ccf6460e3c68b70c16aaa280f2d7b3f22d745b97a89906cfc476c',
|
||||||
|
'502bcb6842eb06f0e49032bae87c554c031d6d4d2d7694efbf9c468d48220c50f8ca28843364d70cee92d6fe246e61448f9db9808b3b2408',
|
||||||
|
'0c9810f1e2ebd389caa789374d78007974ef4d17227316f40e578b336827da3f6b482a4794eb6a3975b971b5e1388f52e91ea2f1bcb0f912',
|
||||||
|
'20d41d85a18d5657a29640321563bbd04c2ffbd0a37a7ba43a4f7d263ce26faf4e1f74f9f4b590c69229ae571fe37fa639b5b8eb48bd9a55',
|
||||||
|
'e6b4b8f408c7010d0601e7eda0c309a1a42720d6d06b5759fdc4e1efe22d076d6c44d42f508d67be462914d28b8edce32e7094305164af17',
|
||||||
|
'be88bbb86c59c13d8e9d09ab98105f69c2d1dd134dbcd3b0863658f53159db64c0e139d180f3c89b8296d0ae324419c06fa87fc7daaf34c1',
|
||||||
|
'a456f9369769e8f08902124a0314c7a06537a06e32411f4f93415950a17badfa7442b6217434a3a05ef45be5f10bd7b2ef8ea00c431edec5',
|
||||||
|
'186e452c4466aa4383b4c00210d52e7922dbf9771e8b47e229a9b7b73c8d10fd7ef0b6e41530f91f24a3ed9ab71fa38b98b2fe4746d51d68',
|
||||||
|
'4ae7fdcae9453f195a8ead5cbe1a7b9699673b52c40ab27927464887be53237f7f3a21b938d40d0ec9e15b1d5130b13ffed81373a53e2b43',
|
||||||
|
'841981c3bfeec3f60cfeca75d9d8dc17f46cf0106f2422b59aec580a58f342272e3a5e575a055ddb051390c54c24c6ecb1e0aceb075f6056',
|
||||||
|
];
|
||||||
|
let B = DecafPoint.BASE;
|
||||||
|
let P = DecafPoint.ZERO;
|
||||||
|
for (const encoded of encodingsOfSmallMultiples) {
|
||||||
|
deepStrictEqual(P.toHex(), encoded);
|
||||||
|
deepStrictEqual(DecafPoint.fromHex(encoded).toHex(), encoded);
|
||||||
|
P = P.add(B);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should('not convert bad bytes encoding', () => {
|
||||||
|
const badEncodings = [
|
||||||
|
// These are all bad because they're non-canonical field encodings.
|
||||||
|
'8e24f838059ee9fef1e209126defe53dcd74ef9b6304601c6966099effffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
'86fcc7212bd4a0b980928666dc28c444a605ef38e09fb569e28d4443ffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
'866d54bd4c4ff41a55d4eefdbeca73cbd653c7bd3135b383708ec0bdffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
'4a380ccdab9c86364a89e77a464d64f9157538cfdfa686adc0d5ece4ffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
'f22d9d4c945dd44d11e0b1d3d3d358d959b4844d83b08c44e659d79fffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
'8cdffc681aa99e9c818c8ef4c3808b58e86acdef1ab68c8477af185bffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
'0e1c12ac7b5920effbd044e897c57634e2d05b5c27f8fa3df8a086a1ffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||||
|
// These are all bad because they're negative field elements.
|
||||||
|
'15141bd2121837ef71a0016bd11be757507221c26542244f23806f3fd3496b7d4c36826276f3bf5deea2c60c4fa4cec69946876da497e795',
|
||||||
|
'455d380238434ab740a56267f4f46b7d2eb2dd8ee905e51d7b0ae8a6cb2bae501e67df34ab21fa45946068c9f233939b1d9521a998b7cb93',
|
||||||
|
'810b1d8e8bf3a9c023294bbfd3d905a97531709bdc0f42390feedd7010f77e98686d400c9c86ed250ceecd9de0a18888ffecda0f4ea1c60d',
|
||||||
|
'd3af9cc41be0e5de83c0c6273bedcb9351970110044a9a41c7b9b2267cdb9d7bf4dc9c2fdb8bed32878184604f1d9944305a8df4274ce301',
|
||||||
|
'9312bcab09009e4330ff89c4bc1e9e000d863efc3c863d3b6c507a40fd2cdefde1bf0892b4b5ed9780b91ed1398fb4a7344c605aa5efda74',
|
||||||
|
'53d11bce9e62a29d63ed82ae93761bdd76e38c21e2822d6ebee5eb1c5b8a03eaf9df749e2490eda9d8ac27d1f71150de93668074d18d1c3a',
|
||||||
|
'697c1aed3cd8858515d4be8ac158b229fe184d79cb2b06e49210a6f3a7cd537bcd9bd390d96c4ab6a4406da5d93640726285370cfa95df80',
|
||||||
|
// These are all bad because they give a nonsquare x².
|
||||||
|
'58ad48715c9a102569b68b88362a4b0645781f5a19eb7e59c6a4686fd0f0750ff42e3d7af1ab38c29d69b670f31258919c9fdbf6093d06c0',
|
||||||
|
'8ca37ee2b15693f06e910cf43c4e32f1d5551dda8b1e48cb6ddd55e440dbc7b296b601919a4e4069f59239ca247ff693f7daa42f086122b1',
|
||||||
|
'982c0ec7f43d9f97c0a74b36db0abd9ca6bfb98123a90782787242c8a523cdc76df14a910d54471127e7662a1059201f902940cd39d57af5',
|
||||||
|
'baa9ab82d07ca282b968a911a6c3728d74bf2fe258901925787f03ee4be7e3cb6684fd1bcfe5071a9a974ad249a4aaa8ca81264216c68574',
|
||||||
|
'2ed9ffe2ded67a372b181ac524996402c42970629db03f5e8636cbaf6074b523d154a7a8c4472c4c353ab88cd6fec7da7780834cc5bd5242',
|
||||||
|
'f063769e4241e76d815800e4933a3a144327a30ec40758ad3723a788388399f7b3f5d45b6351eb8eddefda7d5bff4ee920d338a8b89d8b63',
|
||||||
|
'5a0104f1f55d152ceb68bc138182499891d90ee8f09b40038ccc1e07cb621fd462f781d045732a4f0bda73f0b2acf94355424ff0388d4b9c',
|
||||||
|
];
|
||||||
|
for (const badBytes of badEncodings) {
|
||||||
|
const b = hexToBytes(badBytes);
|
||||||
|
throws(() => DecafPoint.fromHex(b), badBytes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should('create right points from uniform hash', () => {
|
||||||
|
const hashes = [
|
||||||
|
'cbb8c991fd2f0b7e1913462d6463e4fd2ce4ccdd28274dc2ca1f4165d5ee6cdccea57be3416e166fd06718a31af45a2f8e987e301be59ae6673e963001dbbda80df47014a21a26d6c7eb4ebe0312aa6fffb8d1b26bc62ca40ed51f8057a635a02c2b8c83f48fa6a2d70f58a1185902c0',
|
||||||
|
'b6d8da654b13c3101d6634a231569e6b85961c3f4b460a08ac4a5857069576b64428676584baa45b97701be6d0b0ba18ac28d443403b45699ea0fbd1164f5893d39ad8f29e48e399aec5902508ea95e33bc1e9e4620489d684eb5c26bc1ad1e09aba61fabc2cdfee0b6b6862ffc8e55a',
|
||||||
|
'36a69976c3e5d74e4904776993cbac27d10f25f5626dd45c51d15dcf7b3e6a5446a6649ec912a56895d6baa9dc395ce9e34b868d9fb2c1fc72eb6495702ea4f446c9b7a188a4e0826b1506b0747a6709f37988ff1aeb5e3788d5076ccbb01a4bc6623c92ff147a1e21b29cc3fdd0e0f4',
|
||||||
|
'd5938acbba432ecd5617c555a6a777734494f176259bff9dab844c81aadcf8f7abd1a9001d89c7008c1957272c1786a4293bb0ee7cb37cf3988e2513b14e1b75249a5343643d3c5e5545a0c1a2a4d3c685927c38bc5e5879d68745464e2589e000b31301f1dfb7471a4f1300d6fd0f99',
|
||||||
|
'4dec58199a35f531a5f0a9f71a53376d7b4bdd6bbd2904234a8ea65bbacbce2a542291378157a8f4be7b6a092672a34d85e473b26ccfbd4cdc6739783dc3f4f6ee3537b7aed81df898c7ea0ae89a15b5559596c2a5eeacf8b2b362f3db2940e3798b63203cae77c4683ebaed71533e51',
|
||||||
|
'df2aa1536abb4acab26efa538ce07fd7bca921b13e17bc5ebcba7d1b6b733deda1d04c220f6b5ab35c61b6bcb15808251cab909a01465b8ae3fc770850c66246d5a9eae9e2877e0826e2b8dc1bc08009590bc6778a84e919fbd28e02a0f9c49b48dc689eb5d5d922dc01469968ee81b5',
|
||||||
|
'e9fb440282e07145f1f7f5ecf3c273212cd3d26b836b41b02f108431488e5e84bd15f2418b3d92a3380dd66a374645c2a995976a015632d36a6c2189f202fc766e1c82f50ad9189be190a1f0e8f9b9e69c9c18cc98fdd885608f68bf0fdedd7b894081a63f70016a8abf04953affbefa',
|
||||||
|
];
|
||||||
|
const encodedHashToPoints = [
|
||||||
|
'0c709c9607dbb01c94513358745b7c23953d03b33e39c7234e268d1d6e24f34014ccbc2216b965dd231d5327e591dc3c0e8844ccfd568848',
|
||||||
|
'76ab794e28ff1224c727fa1016bf7f1d329260b7218a39aea2fdb17d8bd9119017b093d641cedf74328c327184dc6f2a64bd90eddccfcdab',
|
||||||
|
'c8d7ac384143500e50890a1c25d643343accce584caf2544f9249b2bf4a6921082be0e7f3669bb5ec24535e6c45621e1f6dec676edd8b664',
|
||||||
|
'62beffc6b8ee11ccd79dbaac8f0252c750eb052b192f41eeecb12f2979713b563caf7d22588eca5e80995241ef963e7ad7cb7962f343a973',
|
||||||
|
'f4ccb31d263731ab88bed634304956d2603174c66da38742053fa37dd902346c3862155d68db63be87439e3d68758ad7268e239d39c4fd3b',
|
||||||
|
'7e79b00e8e0a76a67c0040f62713b8b8c6d6f05e9c6d02592e8a22ea896f5deacc7c7df5ed42beae6fedb9000285b482aa504e279fd49c32',
|
||||||
|
'20b171cb16be977f15e013b9752cf86c54c631c4fc8cbf7c03c4d3ac9b8e8640e7b0e9300b987fe0ab5044669314f6ed1650ae037db853f1',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < hashes.length; i++) {
|
||||||
|
const hash = hexToBytes(hashes[i]);
|
||||||
|
const point = DecafPoint.hashToCurve(hash);
|
||||||
|
deepStrictEqual(point.toHex(), encodedHashToPoints[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
should('have proper equality testing', () => {
|
||||||
|
const MAX_448B = BigInt(
|
||||||
|
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
|
||||||
|
);
|
||||||
|
const bytes448ToNumberLE = (bytes) => ed448.CURVE.Fp.create(bytesToNumberLE(bytes) & MAX_448B);
|
||||||
|
|
||||||
|
const priv = new Uint8Array([
|
||||||
|
23, 211, 149, 179, 209, 108, 78, 37, 229, 45, 122, 220, 85, 38, 192, 182, 96, 40, 168, 63,
|
||||||
|
175, 194, 73, 202, 14, 175, 78, 15, 117, 175, 40, 32, 218, 221, 151, 58, 158, 91, 250, 141,
|
||||||
|
18, 175, 191, 119, 152, 124, 223, 101, 54, 218, 76, 158, 43, 112, 151, 32,
|
||||||
|
]);
|
||||||
|
const pub = DecafPoint.BASE.multiply(bytes448ToNumberLE(priv));
|
||||||
|
deepStrictEqual(pub.equals(DecafPoint.ZERO), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ESM is broken.
|
||||||
|
import url from 'url';
|
||||||
|
|
||||||
|
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
||||||
|
should.run();
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { should } from 'micro-should';
|
|||||||
import './basic.test.js';
|
import './basic.test.js';
|
||||||
import './nist.test.js';
|
import './nist.test.js';
|
||||||
import './ed448.test.js';
|
import './ed448.test.js';
|
||||||
|
import './ed448-addons.test.js';
|
||||||
import './ed25519.test.js';
|
import './ed25519.test.js';
|
||||||
import './ed25519-addons.test.js';
|
import './ed25519-addons.test.js';
|
||||||
import './secp256k1.test.js';
|
import './secp256k1.test.js';
|
||||||
|
Loading…
Reference in New Issue
Block a user