tests of ed25519, ed448: improve
This commit is contained in:
parent
18eabfd3be
commit
37ebe6c40f
@ -1 +1,2 @@
|
|||||||
|
export { numberToBytesLE } from '../esm/abstract/utils.js';
|
||||||
export { ed25519, ED25519_TORSION_SUBGROUP } from '../esm/ed25519.js';
|
export { ed25519, ED25519_TORSION_SUBGROUP } from '../esm/ed25519.js';
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { deepStrictEqual, strictEqual, throws } from 'assert';
|
import { deepStrictEqual, strictEqual, throws } from 'assert';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
import { bytesToHex, concatBytes, hexToBytes, randomBytes } from '@noble/hashes/utils';
|
||||||
import * as fc from 'fast-check';
|
import * as fc from 'fast-check';
|
||||||
import { describe, should } from 'micro-should';
|
import { describe, should } from 'micro-should';
|
||||||
import { ed25519, ED25519_TORSION_SUBGROUP } from './ed25519.helpers.js';
|
import { ed25519, ED25519_TORSION_SUBGROUP, numberToBytesLE } from './ed25519.helpers.js';
|
||||||
import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' };
|
import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' };
|
||||||
import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
|
import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
|
||||||
|
|
||||||
@ -346,7 +346,7 @@ describe('ed25519', () => {
|
|||||||
// );
|
// );
|
||||||
// });
|
// });
|
||||||
|
|
||||||
should(`Wycheproof/ED25519`, () => {
|
should(`wycheproof/ED25519`, () => {
|
||||||
for (let g = 0; g < ed25519vectors.testGroups.length; g++) {
|
for (let g = 0; g < ed25519vectors.testGroups.length; g++) {
|
||||||
const group = ed25519vectors.testGroups[g];
|
const group = ed25519vectors.testGroups[g];
|
||||||
const key = group.key;
|
const key = group.key;
|
||||||
@ -370,7 +370,7 @@ describe('ed25519', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
should('Property test issue #1', () => {
|
should('not mutate inputs', () => {
|
||||||
const message = new Uint8Array([12, 12, 12]);
|
const message = new Uint8Array([12, 12, 12]);
|
||||||
const signature = ed.sign(message, to32Bytes(1n));
|
const signature = ed.sign(message, to32Bytes(1n));
|
||||||
const publicKey = ed.getPublicKey(to32Bytes(1n)); // <- was 1n
|
const publicKey = ed.getPublicKey(to32Bytes(1n)); // <- was 1n
|
||||||
@ -387,9 +387,28 @@ describe('ed25519', () => {
|
|||||||
strictEqual(cleared.isTorsionFree(), true, `cleared must be torsionFree: ${hex}`);
|
strictEqual(cleared.isTorsionFree(), true, `cleared must be torsionFree: ${hex}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
should('not verify when sig.s >= CURVE.n', () => {
|
||||||
|
const privateKey = ed25519.utils.randomPrivateKey();
|
||||||
|
const message = Uint8Array.from([0xab, 0xbc, 0xcd, 0xde]);
|
||||||
|
const publicKey = ed25519.getPublicKey(privateKey);
|
||||||
|
const signature = ed25519.sign(message, privateKey);
|
||||||
|
|
||||||
|
const R = signature.slice(0, 32);
|
||||||
|
let s = signature.slice(32, 64);
|
||||||
|
|
||||||
|
s = bytesToHex(s.slice().reverse());
|
||||||
|
s = BigInt('0x' + s);
|
||||||
|
s = s + ed25519.CURVE.n;
|
||||||
|
s = numberToBytesLE(s, 32);
|
||||||
|
|
||||||
|
const sig_invalid = concatBytes(R, s);
|
||||||
|
throws(() => {
|
||||||
|
ed25519.verify(sig_invalid, message, publicKey);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
should('ed25519 bug', () => {
|
should('not accept point without z, t', () => {
|
||||||
const t = 81718630521762619991978402609047527194981150691135404693881672112315521837062n;
|
const t = 81718630521762619991978402609047527194981150691135404693881672112315521837062n;
|
||||||
const point = ed25519.ExtendedPoint.fromAffine({ x: t, y: t });
|
const point = ed25519.ExtendedPoint.fromAffine({ x: t, y: t });
|
||||||
throws(() => point.assertValidity());
|
throws(() => point.assertValidity());
|
||||||
@ -397,6 +416,7 @@ should('ed25519 bug', () => {
|
|||||||
// const point2 = point.double();
|
// const point2 = point.double();
|
||||||
// point2.toAffine(); // crash!
|
// point2.toAffine(); // crash!
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ESM is broken.
|
// ESM is broken.
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
@ -2,7 +2,7 @@ import { deepStrictEqual, throws } from 'assert';
|
|||||||
import { describe, should } from 'micro-should';
|
import { describe, should } from 'micro-should';
|
||||||
import * as fc from 'fast-check';
|
import * as fc from 'fast-check';
|
||||||
import { ed448, ed448ph, x448 } from '../esm/ed448.js';
|
import { ed448, ed448ph, x448 } from '../esm/ed448.js';
|
||||||
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
import { bytesToHex, concatBytes, hexToBytes, randomBytes } from '@noble/hashes/utils';
|
||||||
import { numberToBytesLE } from '../esm/abstract/utils.js';
|
import { numberToBytesLE } from '../esm/abstract/utils.js';
|
||||||
import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' };
|
import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' };
|
||||||
import { default as x448vectors } from './wycheproof/x448_test.json' assert { type: 'json' };
|
import { default as x448vectors } from './wycheproof/x448_test.json' assert { type: 'json' };
|
||||||
@ -467,6 +467,125 @@ describe('ed448', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// should('X448: should convert base point to montgomery using fromPoint', () => {
|
||||||
|
// deepStrictEqual(
|
||||||
|
// hex(ed.montgomeryCurve.UfromPoint(Point.BASE)),
|
||||||
|
// ed.montgomeryCurve.BASE_POINT_U
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
|
||||||
|
// should('X448/getSharedSecret() should be commutative', async () => {
|
||||||
|
// for (let i = 0; i < 512; i++) {
|
||||||
|
// const asec = ed.utils.randomPrivateKey();
|
||||||
|
// const apub = ed.getPublicKey(asec);
|
||||||
|
// const bsec = ed.utils.randomPrivateKey();
|
||||||
|
// const bpub = ed.getPublicKey(bsec);
|
||||||
|
// try {
|
||||||
|
// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('not commutative', { asec, apub, bsec, bpub });
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
describe('ed448ctx', () => {
|
||||||
|
const VECTORS_RFC8032_CTX = [
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'c4eab05d357007c632f3dbb48489924d552b08fe0c353a0d4a1f00acda2c463afbea67c5e8d2877c5e3bc397a659949ef8021e954e0a12274e',
|
||||||
|
publicKey:
|
||||||
|
'43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a9480',
|
||||||
|
message: '03',
|
||||||
|
context: '666f6f',
|
||||||
|
signature:
|
||||||
|
'd4f8f6131770dd46f40867d6fd5d5055' +
|
||||||
|
'de43541f8c5e35abbcd001b32a89f7d2' +
|
||||||
|
'151f7647f11d8ca2ae279fb842d60721' +
|
||||||
|
'7fce6e042f6815ea000c85741de5c8da' +
|
||||||
|
'1144a6a1aba7f96de42505d7a7298524' +
|
||||||
|
'fda538fccbbb754f578c1cad10d54d0d' +
|
||||||
|
'5428407e85dcbc98a49155c13764e66c' +
|
||||||
|
'3c00',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (let i = 0; i < VECTORS_RFC8032_CTX.length; i++) {
|
||||||
|
const v = VECTORS_RFC8032_CTX[i];
|
||||||
|
should(`${i}`, () => {
|
||||||
|
deepStrictEqual(hex(ed.getPublicKey(v.secretKey)), v.publicKey);
|
||||||
|
deepStrictEqual(hex(ed.sign(v.message, v.secretKey, v.context)), v.signature);
|
||||||
|
deepStrictEqual(ed.verify(v.signature, v.message, v.publicKey, v.context), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ed448ph', () => {
|
||||||
|
const VECTORS_RFC8032_PH = [
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42ef7822e0d5104127dc05d6dbefde69e3ab2cec7c867c6e2c49',
|
||||||
|
publicKey:
|
||||||
|
'259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743c09428a131d6b1b57303d90d8132c276d5ed3d5d01c0f53880',
|
||||||
|
message: '616263',
|
||||||
|
signature:
|
||||||
|
'822f6901f7480f3d5f562c592994d969' +
|
||||||
|
'3602875614483256505600bbc281ae38' +
|
||||||
|
'1f54d6bce2ea911574932f52a4e6cadd' +
|
||||||
|
'78769375ec3ffd1b801a0d9b3f4030cd' +
|
||||||
|
'433964b6457ea39476511214f97469b5' +
|
||||||
|
'7dd32dbc560a9a94d00bff07620464a3' +
|
||||||
|
'ad203df7dc7ce360c3cd3696d9d9fab9' +
|
||||||
|
'0f00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42ef7822e0d5104127dc05d6dbefde69e3ab2cec7c867c6e2c49',
|
||||||
|
publicKey:
|
||||||
|
'259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743c09428a131d6b1b57303d90d8132c276d5ed3d5d01c0f53880',
|
||||||
|
message: '616263',
|
||||||
|
context: '666f6f',
|
||||||
|
signature:
|
||||||
|
'c32299d46ec8ff02b54540982814dce9' +
|
||||||
|
'a05812f81962b649d528095916a2aa48' +
|
||||||
|
'1065b1580423ef927ecf0af5888f90da' +
|
||||||
|
'0f6a9a85ad5dc3f280d91224ba9911a3' +
|
||||||
|
'653d00e484e2ce232521481c8658df30' +
|
||||||
|
'4bb7745a73514cdb9bf3e15784ab7128' +
|
||||||
|
'4f8d0704a608c54a6b62d97beb511d13' +
|
||||||
|
'2100',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (let i = 0; i < VECTORS_RFC8032_PH.length; i++) {
|
||||||
|
const v = VECTORS_RFC8032_PH[i];
|
||||||
|
should(`${i}`, () => {
|
||||||
|
deepStrictEqual(hex(ed448ph.getPublicKey(v.secretKey)), v.publicKey);
|
||||||
|
deepStrictEqual(hex(ed448ph.sign(v.message, v.secretKey, v.context)), v.signature);
|
||||||
|
deepStrictEqual(ed448ph.verify(v.signature, v.message, v.publicKey, v.context), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
should('not verify when sig.s >= CURVE.n', () => {
|
||||||
|
const privateKey = ed448.utils.randomPrivateKey();
|
||||||
|
const message = Uint8Array.from([0xab, 0xbc, 0xcd, 0xde]);
|
||||||
|
const publicKey = ed448.getPublicKey(privateKey);
|
||||||
|
const signature = ed448.sign(message, privateKey);
|
||||||
|
|
||||||
|
const R = signature.slice(0, 56);
|
||||||
|
let s = signature.slice(56, 112);
|
||||||
|
|
||||||
|
s = bytesToHex(s.slice().reverse());
|
||||||
|
s = BigInt('0x' + s);
|
||||||
|
s = s + ed448.CURVE.n;
|
||||||
|
s = numberToBytesLE(s, 56);
|
||||||
|
|
||||||
|
const sig_invalid = concatBytes(R, s);
|
||||||
|
throws(() => {
|
||||||
|
ed448.verify(sig_invalid, message, publicKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RFC7748 X448 ECDH', () => {
|
||||||
// ECDH
|
// ECDH
|
||||||
const rfc7748Mul = [
|
const rfc7748Mul = [
|
||||||
{
|
{
|
||||||
@ -484,14 +603,12 @@ describe('ed448', () => {
|
|||||||
'884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d',
|
'884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
describe('RFC7748', () => {
|
|
||||||
for (let i = 0; i < rfc7748Mul.length; i++) {
|
for (let i = 0; i < rfc7748Mul.length; i++) {
|
||||||
const v = rfc7748Mul[i];
|
const v = rfc7748Mul[i];
|
||||||
should(`scalarMult (${i})`, () => {
|
should(`scalarMult (${i})`, () => {
|
||||||
deepStrictEqual(hex(x448.scalarMult(v.scalar, v.u)), v.outputU);
|
deepStrictEqual(hex(x448.scalarMult(v.scalar, v.u)), v.outputU);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const rfc7748Iter = [
|
const rfc7748Iter = [
|
||||||
{
|
{
|
||||||
@ -508,14 +625,14 @@ describe('ed448', () => {
|
|||||||
];
|
];
|
||||||
for (let i = 0; i < rfc7748Iter.length; i++) {
|
for (let i = 0; i < rfc7748Iter.length; i++) {
|
||||||
const { scalar, iters } = rfc7748Iter[i];
|
const { scalar, iters } = rfc7748Iter[i];
|
||||||
should(`RFC7748: scalarMult iteration (${i})`, () => {
|
should(`scalarMult iterated ${iters}x`, () => {
|
||||||
let k = x448.GuBytes;
|
let k = x448.GuBytes;
|
||||||
for (let i = 0, u = k; i < iters; i++) [k, u] = [x448.scalarMult(k, u), k];
|
for (let i = 0, u = k; i < iters; i++) [k, u] = [x448.scalarMult(k, u), k];
|
||||||
deepStrictEqual(hex(k), scalar);
|
deepStrictEqual(hex(k), scalar);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
should('RFC7748 getSharedKey', () => {
|
should('getSharedKey', () => {
|
||||||
const alicePrivate =
|
const alicePrivate =
|
||||||
'9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b';
|
'9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b';
|
||||||
const alicePublic =
|
const alicePublic =
|
||||||
@ -562,103 +679,7 @@ describe('ed448', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// should('X448: should convert base point to montgomery using fromPoint', () => {
|
should('have proper base point', () => {
|
||||||
// deepStrictEqual(
|
|
||||||
// hex(ed.montgomeryCurve.UfromPoint(Point.BASE)),
|
|
||||||
// ed.montgomeryCurve.BASE_POINT_U
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
|
|
||||||
// should('X448/getSharedSecret() should be commutative', async () => {
|
|
||||||
// for (let i = 0; i < 512; i++) {
|
|
||||||
// const asec = ed.utils.randomPrivateKey();
|
|
||||||
// const apub = ed.getPublicKey(asec);
|
|
||||||
// const bsec = ed.utils.randomPrivateKey();
|
|
||||||
// const bpub = ed.getPublicKey(bsec);
|
|
||||||
// try {
|
|
||||||
// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('not commutative', { asec, apub, bsec, bpub });
|
|
||||||
// throw error;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
const VECTORS_RFC8032_CTX = [
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'c4eab05d357007c632f3dbb48489924d552b08fe0c353a0d4a1f00acda2c463afbea67c5e8d2877c5e3bc397a659949ef8021e954e0a12274e',
|
|
||||||
publicKey:
|
|
||||||
'43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a9480',
|
|
||||||
message: '03',
|
|
||||||
context: '666f6f',
|
|
||||||
signature:
|
|
||||||
'd4f8f6131770dd46f40867d6fd5d5055' +
|
|
||||||
'de43541f8c5e35abbcd001b32a89f7d2' +
|
|
||||||
'151f7647f11d8ca2ae279fb842d60721' +
|
|
||||||
'7fce6e042f6815ea000c85741de5c8da' +
|
|
||||||
'1144a6a1aba7f96de42505d7a7298524' +
|
|
||||||
'fda538fccbbb754f578c1cad10d54d0d' +
|
|
||||||
'5428407e85dcbc98a49155c13764e66c' +
|
|
||||||
'3c00',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < VECTORS_RFC8032_CTX.length; i++) {
|
|
||||||
const v = VECTORS_RFC8032_CTX[i];
|
|
||||||
should(`RFC8032ctx/${i}`, () => {
|
|
||||||
deepStrictEqual(hex(ed.getPublicKey(v.secretKey)), v.publicKey);
|
|
||||||
deepStrictEqual(hex(ed.sign(v.message, v.secretKey, v.context)), v.signature);
|
|
||||||
deepStrictEqual(ed.verify(v.signature, v.message, v.publicKey, v.context), true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const VECTORS_RFC8032_PH = [
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42ef7822e0d5104127dc05d6dbefde69e3ab2cec7c867c6e2c49',
|
|
||||||
publicKey:
|
|
||||||
'259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743c09428a131d6b1b57303d90d8132c276d5ed3d5d01c0f53880',
|
|
||||||
message: '616263',
|
|
||||||
signature:
|
|
||||||
'822f6901f7480f3d5f562c592994d969' +
|
|
||||||
'3602875614483256505600bbc281ae38' +
|
|
||||||
'1f54d6bce2ea911574932f52a4e6cadd' +
|
|
||||||
'78769375ec3ffd1b801a0d9b3f4030cd' +
|
|
||||||
'433964b6457ea39476511214f97469b5' +
|
|
||||||
'7dd32dbc560a9a94d00bff07620464a3' +
|
|
||||||
'ad203df7dc7ce360c3cd3696d9d9fab9' +
|
|
||||||
'0f00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42ef7822e0d5104127dc05d6dbefde69e3ab2cec7c867c6e2c49',
|
|
||||||
publicKey:
|
|
||||||
'259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743c09428a131d6b1b57303d90d8132c276d5ed3d5d01c0f53880',
|
|
||||||
message: '616263',
|
|
||||||
context: '666f6f',
|
|
||||||
signature:
|
|
||||||
'c32299d46ec8ff02b54540982814dce9' +
|
|
||||||
'a05812f81962b649d528095916a2aa48' +
|
|
||||||
'1065b1580423ef927ecf0af5888f90da' +
|
|
||||||
'0f6a9a85ad5dc3f280d91224ba9911a3' +
|
|
||||||
'653d00e484e2ce232521481c8658df30' +
|
|
||||||
'4bb7745a73514cdb9bf3e15784ab7128' +
|
|
||||||
'4f8d0704a608c54a6b62d97beb511d13' +
|
|
||||||
'2100',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < VECTORS_RFC8032_PH.length; i++) {
|
|
||||||
const v = VECTORS_RFC8032_PH[i];
|
|
||||||
should(`RFC8032ph/${i}`, () => {
|
|
||||||
deepStrictEqual(hex(ed448ph.getPublicKey(v.secretKey)), v.publicKey);
|
|
||||||
deepStrictEqual(hex(ed448ph.sign(v.message, v.secretKey, v.context)), v.signature);
|
|
||||||
deepStrictEqual(ed448ph.verify(v.signature, v.message, v.publicKey, v.context), true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
should('X448 base point', () => {
|
|
||||||
const { x, y } = Point.BASE;
|
const { x, y } = Point.BASE;
|
||||||
const { Fp } = ed448.CURVE;
|
const { Fp } = ed448.CURVE;
|
||||||
// const invX = Fp.invert(x * x); // x²
|
// const invX = Fp.invert(x * x); // x²
|
||||||
@ -667,6 +688,7 @@ describe('ed448', () => {
|
|||||||
deepStrictEqual(numberToBytesLE(u, 56), x448.GuBytes);
|
deepStrictEqual(numberToBytesLE(u, 56), x448.GuBytes);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ESM is broken.
|
// ESM is broken.
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
Loading…
Reference in New Issue
Block a user