definitions: split ed25519, ed448. More wycheproof tests

This commit is contained in:
Paul Miller 2022-12-11 14:56:16 +00:00
parent c8fc24fd8f
commit b92866d9b8
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
29 changed files with 96492 additions and 418 deletions

@ -1,196 +0,0 @@
import { sha512 } from '@noble/hashes/sha512';
import { shake256 } from '@noble/hashes/sha3';
import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/hashes/utils';
import { twistedEdwards } from '@noble/curves/edwards';
import { mod, pow2, isNegativeLE } from '@noble/curves/modular';
const ed25519P = BigInt(
'57896044618658097711785492504343953926634992332820282019728792003956564819949'
);
// √(-1) aka √(a) aka 2^((p-1)/4)
const ED25519_SQRT_M1 = BigInt(
'19681161376707505956807079304988542015446066515923890162744021073123829784752'
);
function ed25519_pow_2_252_3(x: bigint) {
const P = ed25519P;
const _1n = BigInt(1);
const _2n = BigInt(2);
const _5n = BigInt(5);
const _10n = BigInt(10);
const _20n = BigInt(20);
const _40n = BigInt(40);
const _80n = BigInt(80);
const x2 = (x * x) % P;
const b2 = (x2 * x) % P; // x^3, 11
const b4 = (pow2(b2, _2n, P) * b2) % P; // x^15, 1111
const b5 = (pow2(b4, _1n, P) * x) % P; // x^31
const b10 = (pow2(b5, _5n, P) * b5) % P;
const b20 = (pow2(b10, _10n, P) * b10) % P;
const b40 = (pow2(b20, _20n, P) * b20) % P;
const b80 = (pow2(b40, _40n, P) * b40) % P;
const b160 = (pow2(b80, _80n, P) * b80) % P;
const b240 = (pow2(b160, _80n, P) * b80) % P;
const b250 = (pow2(b240, _10n, P) * b10) % P;
const pow_p_5_8 = (pow2(b250, _2n, P) * x) % P;
// ^ To pow to (p+3)/8, multiply it by x.
return { pow_p_5_8, b2 };
}
export const ed25519 = twistedEdwards({
// Param: a
a: BigInt(-1),
// Equal to -121665/121666 over finite field.
// Negative number is P - number, and division is invert(number, P)
d: BigInt('37095705934669439343138083508754565189542113879843219016388785533085940283555'),
// Finite field 𝔽p over which we'll do calculations; 2n ** 255n - 19n
P: ed25519P,
// Subgroup order: how many points ed25519 has
// 2n ** 252n + 27742317777372353535851937790883648493n;
n: BigInt('7237005577332262213973186563042994240857116359379907606001950938285454250989'),
// Cofactor
h: BigInt(8),
// Base point (x, y) aka generator point
Gx: BigInt('15112221349535400772501151409588531511454012693041857206046113283949847762202'),
Gy: BigInt('46316835694926478169428394003475163141307993866256225615783033603165251855960'),
// The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519
a24: BigInt('121665'),
scalarBits: 255,
hash: sha512,
randomBytes,
adjustScalarBytes: (bytes: Uint8Array): Uint8Array => {
// Section 5: For X25519, in order to decode 32 random bytes as an integer scalar,
// set the three least significant bits of the first byte
bytes[0] &= 248; // 0b1111_1000
// and the most significant bit of the last to zero,
bytes[31] &= 127; // 0b0111_1111
// set the second most significant bit of the last byte to 1
bytes[31] |= 64; // 0b0100_0000
return bytes;
},
// dom2
domain: (data: Uint8Array, ctx: Uint8Array, hflag: boolean) => {
if (ctx.length || hflag) throw new Error('Contexts/pre-hash are not supported');
// TODO: support for ph/ctx too?
return data;
},
// Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3.
// Constant-time, u/√v
uvRatio: (u: bigint, v: bigint): { isValid: boolean; value: bigint } => {
const P = ed25519P;
const v3 = mod(v * v * v, P); // v³
const v7 = mod(v3 * v3 * v, P); // v⁷
// (p+3)/8 and (p-5)/8
const pow = ed25519_pow_2_252_3(u * v7).pow_p_5_8;
let x = mod(u * v3 * pow, P); // (uv³)(uv⁷)^(p-5)/8
const vx2 = mod(v * x * x, P); // vx²
const root1 = x; // First root candidate
const root2 = mod(x * ED25519_SQRT_M1, P); // Second root candidate
const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root
const useRoot2 = vx2 === mod(-u, P); // If vx² = -u, set x <-- x * 2^((p-1)/4)
const noRoot = vx2 === mod(-u * ED25519_SQRT_M1, P); // There is no valid root, vx² = -u√(-1)
if (useRoot1) x = root1;
if (useRoot2 || noRoot) x = root2; // We return root2 anyway, for const-time
if (isNegativeLE(x, P)) x = mod(-x, P);
return { isValid: useRoot1 || useRoot2, value: x };
},
});
// https://www.rfc-editor.org/rfc/rfc8032.html says bitLength is 456
// https://www.rfc-editor.org/rfc/rfc7748 says bitLength is 448
// WTF?!
// So, if we looking at wycheproof:
// EdDSA: 456
// X448 (sharedkey stuff) is 448. Awesome!
const ed448P = BigInt(
'726838724295606890549323807888004534353641360687318060281490199180612328166730772686396383698676545930088884461843637361053498018365439'
);
// powPminus3div4 calculates z = x^k mod p, where k = (p-3)/4.
function ed448_pow_Pminus3div4(x: bigint): bigint {
const P = ed448P;
// x ** ((P - 3n)/4n) % P
// [223 of 1, 0, 222 of 1], almost same as secp!
const b2 = (x * x * x) % P;
const b3 = (b2 * b2 * x) % P;
const b6 = (pow2(b3, 3n, P) * b3) % P;
const b9 = (pow2(b6, 3n, P) * b3) % P;
const b11 = (pow2(b9, 2n, P) * b2) % P;
const b22 = (pow2(b11, 11n, P) * b11) % P;
const b44 = (pow2(b22, 22n, P) * b22) % P;
const b88 = (pow2(b44, 44n, P) * b44) % P;
const b176 = (pow2(b88, 88n, P) * b88) % P;
const b220 = (pow2(b176, 44n, P) * b44) % P;
const b222 = (pow2(b220, 2n, P) * b2) % P;
const b223 = (pow2(b222, 1n, P) * x) % P;
return (pow2(b223, 223n, P) * b222) % P;
}
export const ed448 = twistedEdwards({
// Param: a
a: BigInt(1),
// Equal to -39081 over finite field.
// Negative number is P - number
d: BigInt(
'726838724295606890549323807888004534353641360687318060281490199180612328166730772686396383698676545930088884461843637361053498018326358'
),
// Finite field 𝔽p over which we'll do calculations; 2n ** 448n - 2n ** 224n - 1n
P: ed448P,
// Subgroup order: how many points ed448 has; 2n**446n - 13818066809895115352007386748515426880336692474882178609894547503885n
n: BigInt(
'181709681073901722637330951972001133588410340171829515070372549795146003961539585716195755291692375963310293709091662304773755859649779'
),
nBitLength: 456,
// Cofactor
h: BigInt(4),
// Base point (x, y) aka generator point
Gx: BigInt(
'224580040295924300187604334099896036246789641632564134246125461686950415467406032909029192869357953282578032075146446173674602635247710'
),
Gy: BigInt(
'298819210078481492676017930443930673437544040154080242095928241372331506189835876003536878655418784733982303233503462500531545062832660'
),
// The constant a24 is (156326 - 2) / 4 = 39081 for curve448/X448.
a24: BigInt('39081'),
scalarBits: 448, // TODO: fix that
// SHAKE256(dom4(phflag,context)||x, 114)
hash: wrapConstructor(() => shake256.create({ dkLen: 114 })),
randomBytes,
adjustScalarBytes: (bytes: Uint8Array): Uint8Array => {
// Section 5: Likewise, for X448, set the two least significant bits of the first byte to 0, and the most
// significant bit of the last byte to 1.
bytes[0] &= 252; // 0b11111100
// and the most significant bit of the last byte to 1.
bytes[55] |= 128; // 0b10000000
bytes[56] = 0; // Byte outside of group (456 buts vs 448 bits)
return bytes;
},
// dom4
domain: (data: Uint8Array, ctx: Uint8Array, hflag: boolean) => {
if (ctx.length > 255) throw new Error(`Context is too big: ${ctx.length}`);
return concatBytes(utf8ToBytes('SigEd448'), new Uint8Array([hflag ? 1 : 0, ctx.length]), data);
},
// Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3.
// Constant-time, u/√v
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:
// (p+1)/4 3 (p-3)/4
// x = (u/v) = u v (u^5 v^3) (mod p)
const u2v = mod(u * u * v, P);
const u3v = mod(u2v * u, P); // u^2v
const u5v3 = mod(u3v * u2v * v, P); // u^5v^3
const root = ed448_pow_Pminus3div4(u5v3);
const x = mod(u3v * root, P);
// Verify that root is exists
const x2 = mod(x * x, P); // x^2
// If v * x^2 = 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 };
},
});

@ -0,0 +1,126 @@
import { sha512 } from '@noble/hashes/sha512';
import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
import { twistedEdwards } from '@noble/curves/edwards';
import { mod, pow2, isNegativeLE } from '@noble/curves/modular';
const ed25519P = BigInt(
'57896044618658097711785492504343953926634992332820282019728792003956564819949'
);
// √(-1) aka √(a) aka 2^((p-1)/4)
const ED25519_SQRT_M1 = BigInt(
'19681161376707505956807079304988542015446066515923890162744021073123829784752'
);
function ed25519_pow_2_252_3(x: bigint) {
const P = ed25519P;
const _1n = BigInt(1);
const _2n = BigInt(2);
const _5n = BigInt(5);
const _10n = BigInt(10);
const _20n = BigInt(20);
const _40n = BigInt(40);
const _80n = BigInt(80);
const x2 = (x * x) % P;
const b2 = (x2 * x) % P; // x^3, 11
const b4 = (pow2(b2, _2n, P) * b2) % P; // x^15, 1111
const b5 = (pow2(b4, _1n, P) * x) % P; // x^31
const b10 = (pow2(b5, _5n, P) * b5) % P;
const b20 = (pow2(b10, _10n, P) * b10) % P;
const b40 = (pow2(b20, _20n, P) * b20) % P;
const b80 = (pow2(b40, _40n, P) * b40) % P;
const b160 = (pow2(b80, _80n, P) * b80) % P;
const b240 = (pow2(b160, _80n, P) * b80) % P;
const b250 = (pow2(b240, _10n, P) * b10) % P;
const pow_p_5_8 = (pow2(b250, _2n, P) * x) % P;
// ^ To pow to (p+3)/8, multiply it by x.
return { pow_p_5_8, b2 };
}
// Just in case
export const ED25519_TORSION_SUBGROUP = [
'0100000000000000000000000000000000000000000000000000000000000000',
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a',
'0000000000000000000000000000000000000000000000000000000000000080',
'26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05',
'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
'26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85',
'0000000000000000000000000000000000000000000000000000000000000000',
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
];
const ED25519_DEF = {
// Param: a
a: BigInt(-1),
// Equal to -121665/121666 over finite field.
// Negative number is P - number, and division is invert(number, P)
d: BigInt('37095705934669439343138083508754565189542113879843219016388785533085940283555'),
// Finite field 𝔽p over which we'll do calculations; 2n ** 255n - 19n
P: ed25519P,
// Subgroup order: how many points ed25519 has
// 2n ** 252n + 27742317777372353535851937790883648493n;
n: BigInt('7237005577332262213973186563042994240857116359379907606001950938285454250989'),
// Cofactor
h: BigInt(8),
// Base point (x, y) aka generator point
Gx: BigInt('15112221349535400772501151409588531511454012693041857206046113283949847762202'),
Gy: BigInt('46316835694926478169428394003475163141307993866256225615783033603165251855960'),
hash: sha512,
randomBytes,
adjustScalarBytes: (bytes: Uint8Array): Uint8Array => {
// Section 5: For X25519, in order to decode 32 random bytes as an integer scalar,
// set the three least significant bits of the first byte
bytes[0] &= 248; // 0b1111_1000
// and the most significant bit of the last to zero,
bytes[31] &= 127; // 0b0111_1111
// set the second most significant bit of the last byte to 1
bytes[31] |= 64; // 0b0100_0000
return bytes;
},
// dom2
// Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3.
// Constant-time, u/√v
uvRatio: (u: bigint, v: bigint): { isValid: boolean; value: bigint } => {
const P = ed25519P;
const v3 = mod(v * v * v, P); // v³
const v7 = mod(v3 * v3 * v, P); // v⁷
// (p+3)/8 and (p-5)/8
const pow = ed25519_pow_2_252_3(u * v7).pow_p_5_8;
let x = mod(u * v3 * pow, P); // (uv³)(uv⁷)^(p-5)/8
const vx2 = mod(v * x * x, P); // vx²
const root1 = x; // First root candidate
const root2 = mod(x * ED25519_SQRT_M1, P); // Second root candidate
const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root
const useRoot2 = vx2 === mod(-u, P); // If vx² = -u, set x <-- x * 2^((p-1)/4)
const noRoot = vx2 === mod(-u * ED25519_SQRT_M1, P); // There is no valid root, vx² = -u√(-1)
if (useRoot1) x = root1;
if (useRoot2 || noRoot) x = root2; // We return root2 anyway, for const-time
if (isNegativeLE(x, P)) x = mod(-x, P);
return { isValid: useRoot1 || useRoot2, value: x };
},
// ECDH
// The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519
a24: BigInt('121665'),
montgomeryBits: 255, // n is 253 bits
powPminus2: (x: bigint): bigint => {
const P = ed25519P;
// x^(p-2) aka x^(2^255-21)
const { pow_p_5_8, b2 } = ed25519_pow_2_252_3(x);
return mod(pow2(pow_p_5_8, BigInt(3), P) * b2, P);
},
} as const;
export const ed25519 = twistedEdwards(ED25519_DEF);
function ed25519_domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) {
if (ctx.length > 255) throw new Error('Context is too big');
return concatBytes(
utf8ToBytes('SigEd25519 no Ed25519 collisions'),
new Uint8Array([phflag ? 1 : 0, ctx.length]),
ctx,
data
);
}
export const ed25519ctx = twistedEdwards({ ...ED25519_DEF, domain: ed25519_domain });
export const ed25519ph = twistedEdwards({
...ED25519_DEF,
domain: ed25519_domain,
preHash: sha512,
});

@ -0,0 +1,134 @@
import { shake256 } from '@noble/hashes/sha3';
import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/hashes/utils';
import { PointType, twistedEdwards } from '@noble/curves/edwards';
import { mod, pow2, invert } from '@noble/curves/modular';
import { numberToBytesLE } from '@noble/curves/utils';
const _0n = BigInt(0);
const shake256_114 = wrapConstructor(() => shake256.create({ dkLen: 114 }));
const shake256_64 = wrapConstructor(() => shake256.create({ dkLen: 64 }));
const ed448P = BigInt(
'726838724295606890549323807888004534353641360687318060281490199180612328166730772686396383698676545930088884461843637361053498018365439'
);
// powPminus3div4 calculates z = x^k mod p, where k = (p-3)/4.
function ed448_pow_Pminus3div4(x: bigint): bigint {
const P = ed448P;
// x ** ((P - 3n)/4n) % P
// [223 of 1, 0, 222 of 1], almost same as secp!
const b2 = (x * x * x) % P;
const b3 = (b2 * b2 * x) % P;
const b6 = (pow2(b3, 3n, P) * b3) % P;
const b9 = (pow2(b6, 3n, P) * b3) % P;
const b11 = (pow2(b9, 2n, P) * b2) % P;
const b22 = (pow2(b11, 11n, P) * b11) % P;
const b44 = (pow2(b22, 22n, P) * b22) % P;
const b88 = (pow2(b44, 44n, P) * b44) % P;
const b176 = (pow2(b88, 88n, P) * b88) % P;
const b220 = (pow2(b176, 44n, P) * b44) % P;
const b222 = (pow2(b220, 2n, P) * b2) % P;
const b223 = (pow2(b222, 1n, P) * x) % P;
return (pow2(b223, 223n, P) * b222) % P;
}
const ED448_DEF = {
// Param: a
a: BigInt(1),
// Equal to -39081 over finite field.
// Negative number is P - number
d: BigInt(
'726838724295606890549323807888004534353641360687318060281490199180612328166730772686396383698676545930088884461843637361053498018326358'
),
// Finite field 𝔽p over which we'll do calculations; 2n ** 448n - 2n ** 224n - 1n
P: ed448P,
// Subgroup order: how many points ed448 has; 2n**446n - 13818066809895115352007386748515426880336692474882178609894547503885n
n: BigInt(
'181709681073901722637330951972001133588410340171829515070372549795146003961539585716195755291692375963310293709091662304773755859649779'
),
nBitLength: 456,
// Cofactor
h: BigInt(4),
// Base point (x, y) aka generator point
Gx: BigInt(
'224580040295924300187604334099896036246789641632564134246125461686950415467406032909029192869357953282578032075146446173674602635247710'
),
Gy: BigInt(
'298819210078481492676017930443930673437544040154080242095928241372331506189835876003536878655418784733982303233503462500531545062832660'
),
// SHAKE256(dom4(phflag,context)||x, 114)
hash: shake256_114,
randomBytes,
adjustScalarBytes: (bytes: Uint8Array): Uint8Array => {
// Section 5: Likewise, for X448, set the two least significant bits of the first byte to 0, and the most
// significant bit of the last byte to 1.
bytes[0] &= 252; // 0b11111100
// and the most significant bit of the last byte to 1.
bytes[55] |= 128; // 0b10000000
// NOTE: is is NOOP for 56 bytes scalars (X25519/X448)
bytes[56] = 0; // Byte outside of group (456 buts vs 448 bits)
return bytes;
},
// dom4
domain: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => {
if (ctx.length > 255) throw new Error(`Context is too big: ${ctx.length}`);
return concatBytes(
utf8ToBytes('SigEd448'),
new Uint8Array([phflag ? 1 : 0, ctx.length]),
ctx,
data
);
},
// Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3.
// Constant-time, u/√v
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:
// (p+1)/4 3 (p-3)/4
// x = (u/v) = u v (u^5 v^3) (mod p)
const u2v = mod(u * u * v, P);
const u3v = mod(u2v * u, P); // u^2v
const u5v3 = mod(u3v * u2v * v, P); // u^5v^3
const root = ed448_pow_Pminus3div4(u5v3);
const x = mod(u3v * root, P);
// Verify that root is exists
const x2 = mod(x * x, P); // x^2
// If v * x^2 = 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 };
},
// ECDH
// basePointU:
// '0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
// The constant a24 is (156326 - 2) / 4 = 39081 for curve448/X448.
a24: BigInt('39081'),
montgomeryBits: 448,
powPminus2: (x: bigint): bigint => {
const P = ed448P;
const Pminus3div4 = ed448_pow_Pminus3div4(x);
const Pminus3 = pow2(Pminus3div4, BigInt(2), P);
return mod(Pminus3 * x, P); // Pminus3 * x = Pminus2
},
// The 4-isogeny maps between the Montgomery curve and this Edwards
// curve are:
// (u, v) = (y^2/x^2, (2 - x^2 - y^2)*y/x^3)
// (x, y) = (4*v*(u^2 - 1)/(u^4 - 2*u^2 + 4*v^2 + 1),
// -(u^5 - 2*u^3 - 4*u*v^2 + u)/
// (u^5 - 2*u^2*v^2 - 2*u^3 - 2*v^2 + u))
UfromPoint: (p: PointType) => {
const P = ed448P;
const { x, y } = p;
if (x === _0n) throw new Error(`Point with x=0 doesn't have mapping`);
const invX = invert(x * x, P); // x^2
const u = mod(y * y * invX, P); // (y^2/x^2)
return numberToBytesLE(u, 56);
},
} as const;
export const ed448 = twistedEdwards(ED448_DEF);
// NOTE: there is no ed448ctx, since ed448 supports ctx by default
export const ed448ph = twistedEdwards({ ...ED448_DEF, preHash: shake256_64 });

@ -6,9 +6,6 @@ import { concatBytes, randomBytes } from '@noble/hashes/utils';
import { weierstrass, CurveType, CHash } from '@noble/curves/weierstrass';
import { mod, pow2 } from '@noble/curves/modular';
// TODO: ability to provide API for different default hash.
// Wychenproof can help us here & test multiple hashes.
function getHash(hash: CHash) {
return {
hash,

@ -1,11 +1,11 @@
import { deepStrictEqual, throws } from 'assert';
import { should } from 'micro-should';
import * as nist from '../lib/nist.js';
import { hexToBytes } from '@noble/curves/utils';
import { hexToBytes, bytesToHex } from '@noble/curves/utils';
import { default as ecdsa } from './wycheproof/ecdsa_test.json' assert { type: 'json' };
import { default as ecdh } from './wycheproof/ecdh_test.json' assert { type: 'json' };
// import { hexToBytes } from '@noble/curves';
const hex = bytesToHex;
should('Curve Fields', () => {
const vectors = {
@ -90,6 +90,252 @@ should('wychenproof ECDH vectors', () => {
}
});
import { default as ecdh_secp224r1_test } from './wycheproof/ecdh_secp224r1_test.json' assert { type: 'json' };
import { default as ecdh_secp256r1_test } from './wycheproof/ecdh_secp256r1_test.json' assert { type: 'json' };
import { default as ecdh_secp256k1_test } from './wycheproof/ecdh_secp256k1_test.json' assert { type: 'json' };
import { default as ecdh_secp384r1_test } from './wycheproof/ecdh_secp384r1_test.json' assert { type: 'json' };
import { default as ecdh_secp521r1_test } from './wycheproof/ecdh_secp521r1_test.json' assert { type: 'json' };
// More per curve tests
const WYCHEPROOF_ECDH = {
P224: {
curve: nist.P224,
tests: [ecdh_secp224r1_test],
},
P256: {
curve: nist.P256,
tests: [ecdh_secp256r1_test],
},
secp256k1: {
curve: nist.secp256k1,
tests: [ecdh_secp256k1_test],
},
P384: {
curve: nist.P384,
tests: [ecdh_secp384r1_test],
},
P521: {
curve: nist.P521,
tests: [ecdh_secp521r1_test],
},
};
for (const name in WYCHEPROOF_ECDH) {
const { curve, tests } = WYCHEPROOF_ECDH[name];
for (let i = 0; i < tests.length; i++) {
const test = tests[i];
for (let j = 0; j < test.testGroups.length; j++) {
const group = test.testGroups[j];
should(`Wycheproof/ECDH ${name} (${i}/${j})`, () => {
for (const test of group.tests) {
if (test.result === 'valid' || test.result === 'acceptable') {
try {
const pub = curve.Point.fromHex(test.public);
} catch (e) {
if (e.message.includes('Point.fromHex: received invalid point.')) continue;
throw e;
}
const shared = curve.getSharedSecret(test.private, test.public);
deepStrictEqual(hex(shared), test.shared, 'valid');
} else if (test.result === 'invalid') {
let failed = false;
try {
curve.getSharedSecret(test.private, test.public);
} catch (error) {
failed = true;
}
deepStrictEqual(failed, true, 'invalid');
} else throw new Error('unknown test result');
}
});
}
}
}
// Tests with custom hashes
import { default as secp224r1_sha224_test } from './wycheproof/ecdsa_secp224r1_sha224_test.json' assert { type: 'json' };
import { default as secp224r1_sha256_test } from './wycheproof/ecdsa_secp224r1_sha256_test.json' assert { type: 'json' };
import { default as secp224r1_sha3_224_test } from './wycheproof/ecdsa_secp224r1_sha3_224_test.json' assert { type: 'json' };
import { default as secp224r1_sha3_256_test } from './wycheproof/ecdsa_secp224r1_sha3_256_test.json' assert { type: 'json' };
import { default as secp224r1_sha3_512_test } from './wycheproof/ecdsa_secp224r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp224r1_sha512_test } from './wycheproof/ecdsa_secp224r1_sha512_test.json' assert { type: 'json' };
import { default as secp256k1_sha256_test } from './wycheproof/ecdsa_secp256k1_sha256_test.json' assert { type: 'json' };
import { default as secp256k1_sha3_256_test } from './wycheproof/ecdsa_secp256k1_sha3_256_test.json' assert { type: 'json' };
import { default as secp256k1_sha3_512_test } from './wycheproof/ecdsa_secp256k1_sha3_512_test.json' assert { type: 'json' };
import { default as secp256k1_sha512_test } from './wycheproof/ecdsa_secp256k1_sha512_test.json' assert { type: 'json' };
import { default as secp256r1_sha256_test } from './wycheproof/ecdsa_secp256r1_sha256_test.json' assert { type: 'json' };
import { default as secp256r1_sha3_256_test } from './wycheproof/ecdsa_secp256r1_sha3_256_test.json' assert { type: 'json' };
import { default as secp256r1_sha3_512_test } from './wycheproof/ecdsa_secp256r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp256r1_sha512_test } from './wycheproof/ecdsa_secp256r1_sha512_test.json' assert { type: 'json' };
import { default as secp384r1_sha384_test } from './wycheproof/ecdsa_secp384r1_sha384_test.json' assert { type: 'json' };
import { default as secp384r1_sha3_384_test } from './wycheproof/ecdsa_secp384r1_sha3_384_test.json' assert { type: 'json' };
import { default as secp384r1_sha3_512_test } from './wycheproof/ecdsa_secp384r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp384r1_sha512_test } from './wycheproof/ecdsa_secp384r1_sha512_test.json' assert { type: 'json' };
import { default as secp521r1_sha3_512_test } from './wycheproof/ecdsa_secp521r1_sha3_512_test.json' assert { type: 'json' };
import { default as secp521r1_sha512_test } from './wycheproof/ecdsa_secp521r1_sha512_test.json' assert { type: 'json' };
import { sha3_224, sha3_256, sha3_384, sha3_512 } from '@noble/hashes/sha3';
import { sha512, sha384 } from '@noble/hashes/sha512';
import { sha256 } from '@noble/hashes/sha256';
const WYCHEPROOF_ECDSA = {
P224: {
curve: nist.P224,
hashes: {
// sha224 not released yet
// sha224: {
// hash: sha224,
// tests: [secp224r1_sha224_test],
// },
sha256: {
hash: sha256,
tests: [secp224r1_sha256_test],
},
sha3_224: {
hash: sha3_224,
tests: [secp224r1_sha3_224_test],
},
sha3_256: {
hash: sha3_256,
tests: [secp224r1_sha3_256_test],
},
sha3_512: {
hash: sha3_512,
tests: [secp224r1_sha3_512_test],
},
sha512: {
hash: sha512,
tests: [secp224r1_sha512_test],
},
},
},
secp256k1: {
curve: nist.secp256k1,
hashes: {
// TODO: debug why fails, can be bug
// sha256: {
// hash: sha256,
// tests: [secp256k1_sha256_test],
// },
// sha3_256: {
// hash: sha3_256,
// tests: [secp256k1_sha3_256_test],
// },
// sha3_512: {
// hash: sha3_512,
// tests: [secp256k1_sha3_512_test],
// },
// sha512: {
// hash: sha512,
// tests: [secp256k1_sha512_test],
// },
},
},
P256: {
curve: nist.P256,
hashes: {
sha256: {
hash: sha256,
tests: [secp256r1_sha256_test],
},
sha3_256: {
hash: sha3_256,
tests: [secp256r1_sha3_256_test],
},
sha3_512: {
hash: sha3_512,
tests: [secp256r1_sha3_512_test],
},
sha512: {
hash: sha512,
tests: [secp256r1_sha512_test],
},
},
},
P384: {
curve: nist.P384,
hashes: {
sha384: {
hash: sha384,
tests: [secp384r1_sha384_test],
},
sha3_384: {
hash: sha3_384,
tests: [secp384r1_sha3_384_test],
},
sha3_512: {
hash: sha3_512,
tests: [secp384r1_sha3_512_test],
},
sha512: {
hash: sha512,
tests: [secp384r1_sha512_test],
},
},
},
P521: {
curve: nist.P521,
hashes: {
sha3_512: {
hash: sha3_512,
tests: [secp521r1_sha3_512_test],
},
sha512: {
hash: sha512,
tests: [secp521r1_sha512_test],
},
},
},
};
for (const name in WYCHEPROOF_ECDSA) {
const { curve, hashes } = WYCHEPROOF_ECDSA[name];
for (const hName in hashes) {
const { hash, tests } = hashes[hName];
const CURVE = curve.create(hash);
for (let i = 0; i < tests.length; i++) {
const test = tests[i];
for (let j = 0; j < test.testGroups.length; j++) {
const group = test.testGroups[j];
should(`Wycheproof/WYCHEPROOF_ECDSA ${name}/${hName} (${i}/${j})`, () => {
const pubKey = CURVE.Point.fromHex(group.key.uncompressed);
deepStrictEqual(pubKey.x, BigInt(`0x${group.key.wx}`));
deepStrictEqual(pubKey.y, BigInt(`0x${group.key.wy}`));
for (const test of group.tests) {
// if (['Hash weaker than DL-group'].includes(test.comment)) {
// continue;
// }
const m = CURVE.CURVE.hash(hexToBytes(test.msg));
if (test.result === 'valid' || test.result === 'acceptable') {
try {
CURVE.Signature.fromDER(test.sig);
} catch (e) {
// Some test has invalid signature which we don't accept
if (e.message.includes('Invalid signature: incorrect length')) continue;
throw e;
}
const verified = CURVE.verify(test.sig, m, pubKey);
deepStrictEqual(verified, true, 'valid');
} else if (test.result === 'invalid') {
let failed = false;
try {
failed = !CURVE.verify(test.sig, m, pubKey);
} catch (error) {
failed = true;
}
deepStrictEqual(failed, true, 'invalid');
} else throw new Error('unknown test result');
}
});
}
}
}
}
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {

@ -1,13 +1,12 @@
import { deepStrictEqual, throws } from 'assert';
import { should } from 'micro-should';
import * as fc from 'fast-check';
import { ed25519 } from '../lib/ed.js';
import { ed25519, ed25519ctx, ed25519ph } from '../lib/ed25519.js';
import { readFileSync } from 'fs';
import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' };
import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' };
import { sha512 } from '@noble/hashes/sha512';
const ed = ed25519;
@ -27,7 +26,7 @@ function utf8ToBytes(str) {
ed.utils.precompute(8);
should('ed25519/should not accept >32byte private keys', async () => {
should('ed25519/should not accept >32byte private keys', () => {
const invalidPriv =
100000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800073278156000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000n;
throws(() => ed.getPublicKey(invalidPriv));
@ -93,12 +92,12 @@ should('ed25519/sync methods/should sign and verify', () => {
const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, msg, publicKey), true);
});
should('ed25519/sync methods/should not verify signature with wrong public key', async () => {
should('ed25519/sync methods/should not verify signature with wrong public key', () => {
const publicKey = ed.getPublicKey(12);
const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, msg, publicKey), false);
});
should('ed25519/sync methods/should not verify signature with wrong hash', async () => {
should('ed25519/sync methods/should not verify signature with wrong hash', () => {
const publicKey = ed.getPublicKey(privKey);
const signature = ed.sign(msg, privKey);
deepStrictEqual(ed.verify(signature, wrongMsg, publicKey), false);
@ -155,25 +154,6 @@ should('ed25519/BASE_POINT.multiply()/should throw Point#multiply on TEST 5', ()
}
});
// should('ed25519/getSharedSecret()/should convert base point to montgomery using toX25519()', () => {
// deepStrictEqual(hex(ed.Point.BASE.toX25519()), ed.curve25519.BASE_POINT_U);
// });
// should('ed25519/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;
// }
// }
// });
// https://ed25519.cr.yp.to/python/sign.py
// https://ed25519.cr.yp.to/python/sign.input
const data = readFileSync('./test/ed25519/vectors.txt', 'utf-8');
@ -419,55 +399,6 @@ should('rfc8032 vectors/should create right signature for 0xf5 and long msg', ()
// }
// });
// should('curve25519/scalarMult 1', () => {
// const X25519_VECTORS = [
// {
// k: 'a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4',
// u: 'e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c',
// ku: 'c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552',
// },
// {
// k: '4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d',
// u: 'e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493',
// ku: '95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957',
// },
// ];
// for (const { k, u, ku } of X25519_VECTORS) {
// const _k = Uint8Array.from(hexToBytes(k));
// const _u = Uint8Array.from(hexToBytes(u));
// deepStrictEqual(bytesToHex(ed.curve25519.scalarMult(_k, _u)), ku);
// }
// });
// should('curve25519/scalarMultBase recursive', () => {
// // https://datatracker.ietf.org/doc/html/rfc7748#section-5.2
// const VECTORS = [
// { iters: 1, res: '422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079' },
// { iters: 1000, res: '684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51' },
// // {iters: 1000000, res: '7c3911e0ab2586fd864497297e575e6f3bc601c0883c30df5f4dd2d24f665424'},
// ];
// for (const { iters, res } of VECTORS) {
// let k = hexToBytes('0900000000000000000000000000000000000000000000000000000000000000');
// let u = k;
// for (let i = 0; i < iters; i++) {
// if (i > 0 && i % 100000 === 0) console.log('10k');
// [k, u] = [ed.curve25519.scalarMult(k, u), k];
// }
// deepStrictEqual(bytesToHex(k), res);
// }
// });
// should('curve25519/scalarMult 2', () => {
// const a_priv = hexToBytes('77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a');
// const a_pub = hexToBytes('8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a');
// const b_priv = hexToBytes('5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb');
// const b_pub = hexToBytes('de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f');
// const k = '4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742';
// deepStrictEqual(bytesToHex(ed.curve25519.scalarMultBase(a_priv)), bytesToHex(a_pub));
// deepStrictEqual(bytesToHex(ed.curve25519.scalarMultBase(b_priv)), bytesToHex(b_pub));
// deepStrictEqual(bytesToHex(ed.curve25519.scalarMult(a_priv, b_pub)), k);
// deepStrictEqual(bytesToHex(ed.curve25519.scalarMult(b_priv, a_pub)), k);
// });
should('input immutability: sign/verify are immutable', () => {
const privateKey = ed.utils.randomPrivateKey();
const publicKey = ed.getPublicKey(privateKey);
@ -509,32 +440,100 @@ should('ZIP-215 compliance tests/disallows sig.s >= CURVE.n', () => {
throws(() => ed.verify(sig, 'deadbeef', ed.Point.BASE));
});
// {
// const group = x25519vectors.testGroups[0];
// for (let i = 0; i < group.tests.length; i++) {
// const v = group.tests[i];
// should(`Wycheproof/X25519(${i}, ${v.result}) ${v.comment}`, () => {
// if (v.result === 'valid' || v.result === 'acceptable') {
// try {
// ed.Point.fromHex(v.public);
// } catch (e) {
// if (e.message.includes('Point.fromHex: invalid y coordinate')) return;
// throw e;
// }
// const shared = hex(ed.getSharedSecret(v.private, v.public));
// deepStrictEqual(shared, v.shared, 'valid');
// } else if (v.result === 'invalid') {
// let failed = false;
// try {
// ed.getSharedSecret(v.private, v.public);
// } catch (error) {
// failed = true;
// }
// deepStrictEqual(failed, true, 'invalid');
// } else throw new Error('unknown test result');
// });
// }
// }
const rfc7748Mul = [
{
scalar: 'a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4',
u: 'e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c',
outputU: 'c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552',
},
{
scalar: '4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d',
u: 'e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493',
outputU: '95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957',
},
];
for (let i = 0; i < rfc7748Mul.length; i++) {
const v = rfc7748Mul[i];
should(`RFC7748: scalarMult (${i})`, () => {
deepStrictEqual(hex(ed.montgomeryCurve.scalarMult(v.u, v.scalar)), v.outputU);
});
}
const rfc7748Iter = [
{ scalar: '422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079', iters: 1 },
{ scalar: '684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51', iters: 1000 },
// { scalar: '7c3911e0ab2586fd864497297e575e6f3bc601c0883c30df5f4dd2d24f665424', iters: 1000000 },
];
for (let i = 0; i < rfc7748Iter.length; i++) {
const { scalar, iters } = rfc7748Iter[i];
should(`RFC7748: scalarMult iteration (${i})`, () => {
let k = ed.montgomeryCurve.BASE_POINT_U;
for (let i = 0, u = k; i < iters; i++) [k, u] = [ed.montgomeryCurve.scalarMult(u, k), k];
deepStrictEqual(hex(k), scalar);
});
}
should('RFC7748 getSharedKey', () => {
const alicePrivate = '77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a';
const alicePublic = '8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a';
const bobPrivate = '5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb';
const bobPublic = 'de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f';
const shared = '4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742';
deepStrictEqual(alicePublic, hex(ed.montgomeryCurve.getPublicKey(alicePrivate)));
deepStrictEqual(bobPublic, hex(ed.montgomeryCurve.getPublicKey(bobPrivate)));
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(alicePrivate, bobPublic)), shared);
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(bobPrivate, alicePublic)), shared);
});
should('X25519/getSharedSecret() should be commutative', () => {
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;
}
}
});
should('X25519: should convert base point to montgomery using fromPoint', () => {
deepStrictEqual(
hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)),
ed.montgomeryCurve.BASE_POINT_U
);
});
{
const group = x25519vectors.testGroups[0];
for (let i = 0; i < group.tests.length; i++) {
const v = group.tests[i];
should(`Wycheproof/X25519(${i}, ${v.result}) ${v.comment}`, () => {
if (v.result === 'valid' || v.result === 'acceptable') {
try {
const shared = hex(ed.montgomeryCurve.getSharedSecret(v.private, v.public));
deepStrictEqual(shared, v.shared, 'valid');
} catch (e) {
// We are more strict
if (e.message.includes('Expected valid scalar')) return;
if (e.message.includes('Invalid private or public key received')) return;
throw e;
}
} else if (v.result === 'invalid') {
let failed = false;
try {
ed.montgomeryCurve.getSharedSecret(v.private, v.public);
} catch (error) {
failed = true;
}
deepStrictEqual(failed, true, 'invalid');
} else throw new Error('unknown test result');
});
}
}
{
for (let g = 0; g < ed25519vectors.testGroups.length; g++) {
@ -570,6 +569,84 @@ should('Property test issue #1', () => {
deepStrictEqual(ed.verify(signature, message, publicKey), true);
});
const VECTORS_RFC8032_CTX = [
{
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
message: 'f726936d19c800494e3fdaff20b276a8',
context: '666f6f',
signature:
'55a4cc2f70a54e04288c5f4cd1e45a7b' +
'b520b36292911876cada7323198dd87a' +
'8b36950b95130022907a7fb7c4e9b2d5' +
'f6cca685a587b4b21f4b888e4e7edb0d',
},
{
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
message: 'f726936d19c800494e3fdaff20b276a8',
context: '626172',
signature:
'fc60d5872fc46b3aa69f8b5b4351d580' +
'8f92bcc044606db097abab6dbcb1aee3' +
'216c48e8b3b66431b5b186d1d28f8ee1' +
'5a5ca2df6668346291c2043d4eb3e90d',
},
{
secretKey: '0305334e381af78f141cb666f6199f57bc3495335a256a95bd2a55bf546663f6',
publicKey: 'dfc9425e4f968f7f0c29f0259cf5f9aed6851c2bb4ad8bfb860cfee0ab248292',
message: '508e9e6882b979fea900f62adceaca35',
context: '666f6f',
signature:
'8b70c1cc8310e1de20ac53ce28ae6e72' +
'07f33c3295e03bb5c0732a1d20dc6490' +
'8922a8b052cf99b7c4fe107a5abb5b2c' +
'4085ae75890d02df26269d8945f84b0b',
},
{
secretKey: 'ab9c2853ce297ddab85c993b3ae14bcad39b2c682beabc27d6d4eb20711d6560',
publicKey: '0f1d1274943b91415889152e893d80e93275a1fc0b65fd71b4b0dda10ad7d772',
message: 'f726936d19c800494e3fdaff20b276a8',
context: '666f6f',
signature:
'21655b5f1aa965996b3f97b3c849eafb' +
'a922a0a62992f73b3d1b73106a84ad85' +
'e9b86a7b6005ea868337ff2d20a7f5fb' +
'd4cd10b0be49a68da2b2e0dc0ad8960f',
},
];
for (let i = 0; i < VECTORS_RFC8032_CTX.length; i++) {
const v = VECTORS_RFC8032_CTX[i];
should(`RFC8032ctx/${i}`, () => {
deepStrictEqual(hex(ed25519ctx.getPublicKey(v.secretKey)), v.publicKey);
deepStrictEqual(hex(ed25519ctx.sign(v.message, v.secretKey, v.context)), v.signature);
deepStrictEqual(ed25519ctx.verify(v.signature, v.message, v.publicKey, v.context), true);
});
}
const VECTORS_RFC8032_PH = [
{
secretKey: '833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42',
publicKey: 'ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf',
message: '616263',
signature:
'98a70222f0b8121aa9d30f813d683f80' +
'9e462b469c7ff87639499bb94e6dae41' +
'31f85042463c2a355a2003d062adf5aa' +
'a10b8c61e636062aaad11c2a26083406',
},
];
for (let i = 0; i < VECTORS_RFC8032_PH.length; i++) {
const v = VECTORS_RFC8032_PH[i];
should(`RFC8032ph/${i}`, () => {
deepStrictEqual(hex(ed25519ph.getPublicKey(v.secretKey)), v.publicKey);
deepStrictEqual(hex(ed25519ph.sign(v.message, v.secretKey)), v.signature);
deepStrictEqual(ed25519ph.verify(v.signature, v.message, v.publicKey), true);
});
}
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {

@ -1,7 +1,7 @@
import { deepStrictEqual, throws } from 'assert';
import { should } from 'micro-should';
import * as fc from 'fast-check';
import { ed25519, ed448 } from '../lib/ed.js';
import { ed448, ed448ph } from '../lib/ed448.js';
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' };
import { default as x448vectors } from './wycheproof/x448_test.json' assert { type: 'json' };
@ -411,25 +411,6 @@ should('ed25519/BASE_POINT.multiply()/should throw Point#multiply on TEST 5', ()
}
});
// should('ed25519/getSharedSecret()/should convert base point to montgomery using toX25519()', () => {
// deepStrictEqual(hex(ed.Point.BASE.toX25519()), ed.curve25519.BASE_POINT_U);
// });
// should('ed25519/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;
// }
// }
// });
should('input immutability: sign/verify are immutable', () => {
const privateKey = ed.utils.randomPrivateKey();
const publicKey = ed.getPublicKey(privateKey);
@ -477,101 +458,196 @@ should('input immutability: sign/verify are immutable', () => {
}
}
// {
// const group = x25519vectors.testGroups[0];
// for (let i = 0; i < group.tests.length; i++) {
// const v = group.tests[i];
// should(`Wycheproof/X25519(${i}, ${v.result}) ${v.comment}`, () => {
// if (v.result === 'valid' || v.result === 'acceptable') {
// try {
// ed.Point.fromHex(v.public);
// } catch (e) {
// if (e.message.includes('Point.fromHex: invalid y coordinate')) return;
// throw e;
// }
// const shared = hex(ed.getSharedSecret(v.private, v.public));
// deepStrictEqual(shared, v.shared, 'valid');
// } else if (v.result === 'invalid') {
// let failed = false;
// try {
// ed.getSharedSecret(v.private, v.public);
// } catch (error) {
// failed = true;
// }
// deepStrictEqual(failed, true, 'invalid');
// } else throw new Error('unknown test result');
// });
// }
// }
// ECDH
const rfc7748Mul = [
{
scalar:
'3d262fddf9ec8e88495266fea19a34d28882acef045104d0d1aae121700a779c984c24f8cdd78fbff44943eba368f54b29259a4f1c600ad3',
u: '06fce640fa3487bfda5f6cf2d5263f8aad88334cbd07437f020f08f9814dc031ddbdc38c19c6da2583fa5429db94ada18aa7a7fb4ef8a086',
outputU:
'ce3e4ff95a60dc6697da1db1d85e6afbdf79b50a2412d7546d5f239fe14fbaadeb445fc66a01b0779d98223961111e21766282f73dd96b6f',
},
{
scalar:
'203d494428b8399352665ddca42f9de8fef600908e0d461cb021f8c538345dd77c3e4806e25f46d3315c44e0a5b4371282dd2c8d5be3095f',
u: '0fbcc2f993cd56d3305b0b7d9e55d4c1a8fb5dbb52f8e9a1e9b6201b165d015894e56c4d3570bee52fe205e28a78b91cdfbde71ce8d157db',
outputU:
'884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d',
},
];
for (let i = 0; i < rfc7748Mul.length; i++) {
const v = rfc7748Mul[i];
should(`RFC7748: scalarMult (${i})`, () => {
deepStrictEqual(hex(ed.montgomeryCurve.scalarMult(v.u, v.scalar)), v.outputU);
});
}
const rfc7748Iter = [
{
scalar:
'3f482c8a9f19b01e6c46ee9711d9dc14fd4bf67af30765c2ae2b846a4d23a8cd0db897086239492caf350b51f833868b9bc2b3bca9cf4113',
iters: 1,
},
{
scalar:
'aa3b4749d55b9daf1e5b00288826c467274ce3ebbdd5c17b975e09d4af6c67cf10d087202db88286e2b79fceea3ec353ef54faa26e219f38',
iters: 1000,
},
// { scalar: '077f453681caca3693198420bbe515cae0002472519b3e67661a7e89cab94695c8f4bcd66e61b9b9c946da8d524de3d69bd9d9d66b997e37', iters: 1000000 },
];
for (let i = 0; i < rfc7748Iter.length; i++) {
const { scalar, iters } = rfc7748Iter[i];
should(`RFC7748: scalarMult iteration (${i})`, () => {
let k = ed.montgomeryCurve.BASE_POINT_U;
for (let i = 0, u = k; i < iters; i++) [k, u] = [ed.montgomeryCurve.scalarMult(u, k), k];
deepStrictEqual(hex(k), scalar);
});
}
should('RFC7748 getSharedKey', () => {
const alicePrivate =
'9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b';
const alicePublic =
'9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0';
const bobPrivate =
'1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d';
const bobPublic =
'3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609';
const shared =
'07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d';
deepStrictEqual(alicePublic, hex(ed.montgomeryCurve.getPublicKey(alicePrivate)));
deepStrictEqual(bobPublic, hex(ed.montgomeryCurve.getPublicKey(bobPrivate)));
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(alicePrivate, bobPublic)), shared);
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(bobPrivate, alicePublic)), shared);
});
{
const group = x448vectors.testGroups[0];
for (let i = 0; i < group.tests.length; i++) {
const v = group.tests[i];
should(`Wycheproof/X448(${i}, ${v.result}) ${v.comment}`, () => {
if (v.result === 'valid' || v.result === 'acceptable') {
try {
const shared = hex(ed.montgomeryCurve.getSharedSecret(v.private, v.public));
deepStrictEqual(shared, v.shared, 'valid');
} catch (e) {
// We are more strict
if (e.message.includes('Expected valid scalar')) return;
if (e.message.includes('Invalid private or public key received')) return;
if (e.message.includes('Expected 56 bytes')) return;
throw e;
}
} else if (v.result === 'invalid') {
let failed = false;
try {
ed.montgomeryCurve.getSharedSecret(v.private, v.public);
} catch (error) {
failed = true;
}
deepStrictEqual(failed, true, 'invalid');
} else throw new Error('unknown test result');
});
}
}
should('X448: should convert base point to montgomery using fromPoint', () => {
deepStrictEqual(
hex(ed.montgomeryCurve.UfromPoint(ed.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);
});
}
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run();
}
/*
OUR:
verify(r) Point {
x: 117083889870452976893611547833716470707682026107584166971579726410379933200511856552571178072395970298806909801597194212619985635844270n,
y: 664613439225768038924451828443390535972832706605490606711138767575182918078146210986473699054775106829537240700777959779811881639147732n
}
verify(s) 171560600556246420336815304075147406138580530023825761769062776185328256162525428025207885397693292244020015666168990464688721996580108n
verify(G*s) Point {
x: 162580569181617012512915096099987276929520102470148179595683186785564923123644100210058934137413137508679834237807539232961980479559703n,
y: 300823828808029813226461528962321558722882991716498392051038897352648558972404548670077149616666377671445681138581862112882370877960540n
}
verify(k, hashed) 8903905151968610333524336167784696500100432517973872822290595420701503447289798240464976231189650851226450153394571270017367997886911n
verify(kA, pubKey*k) Point {
x: 689275764828592611152871878771714953006357504547408405065617338137477032523786110957620128317758694018203461690302684307888964572340785n,
y: 481328274497162994197716136544465653692120986048960981855841218770941500118441012672159998270864104759919787387966448820647078734831560n
}
verify(RkA, r+kA) Point {
x: 122982406147090575324915101352793466553671728663998628086189507014151752138194638629641411526703069442455515495095211707112301230852909n,
y: 628599576854758762451451863221817761200843280520482487953855243757271313346398229758661810534375546179313418158952236033181207878869809n
}
verify(RkA*h) Point {
x: 285367816437357046852385747901568148949273863571594761351869427482364138883891040584481510007402650226067916995110428282873262921611092n,
y: 156274886003874424437114343095234538185297810914769372891105611450134155931335556420419362638465757217877136785807507867845960943191541n
}
verify(SB*h) Point {
x: 49879895074926972607089290381794972663529905435154505099510082025305354413619454150885832024420892269163908738032246918490575686924369n,
y: 493395180352948178031760912270438078255871801590750728653962749871941650726588112030608282487999493622892374555049324278196950479173047n
}
verify(RkA-SB) Point {
x: 387994761565948378495106619286831545021939931213704916072043327425382665414799988004170105004375426191018762465670849206465968350321870n,
y: 6861694927033303775201514393095605504169468049999574963263295660536234490030921387020643041252124207285319521500442198840633575797105n
}
verify((RkA-SB)*h) Point {
x: 459153706735977722773001536505633023208447605158247558496590837749184216434578577646373840599757767671074933212247199030379143045005320n,
y: 610562540704855906228140299454456520343298226945170317172546981746408207471796755968182254530606222931325758957275600304516146767443812n
}
verify(R) x=117083889870452976893611547833716470707682026107584166971579726410379933200511856552571178072395970298806909801597194212619985635844270
y=664613439225768038924451828443390535972832706605490606711138767575182918078146210986473699054775106829537240700777959779811881639147732
verify(S) 171560600556246420336815304075147406138580530023825761769062776185328256162525428025207885397693292244020015666168990464688721996580108
Verify parsed
!!!!!!!! CTX True b'' b'53696745643434380000'
verify(h) 8903905151968610333524336167784696500100432517973872822290595420701503447289798240464976231189650851226450153394571270017367997886911
MUL 8903905151968610333524336167784696500100432517973872822290595420701503447289798240464976231189650851226450153394571270017367997886911
MUL 171560600556246420336815304075147406138580530023825761769062776185328256162525428025207885397693292244020015666168990464688721996580108
verify((R+(a*h)) ) x=122982406147090575324915101352793466553671728663998628086189507014151752138194638629641411526703069442455515495095211707112301230852909
y=628599576854758762451451863221817761200843280520482487953855243757271313346398229758661810534375546179313418158952236033181207878869809
ok ^
verify(B*S) x=162580569181617012512915096099987276929520102470148179595683186785564923123644100210058934137413137508679834237807539232961980479559703
y=300823828808029813226461528962321558722882991716498392051038897352648558972404548670077149616666377671445681138581862112882370877960540
OK!
verify((R+(a*h)) * c) x=285367816437357046852385747901568148949273863571594761351869427482364138883891040584481510007402650226067916995110428282873262921611092
y=156274886003874424437114343095234538185297810914769372891105611450134155931335556420419362638465757217877136785807507867845960943191541
verify(B*S*c) x=49879895074926972607089290381794972663529905435154505099510082025305354413619454150885832024420892269163908738032246918490575686924369
y=493395180352948178031760912270438078255871801590750728653962749871941650726588112030608282487999493622892374555049324278196950479173047
*/

@ -0,0 +1,58 @@
import { deepStrictEqual, throws } from 'assert';
import { should } from 'micro-should';
import * as fc from 'fast-check';
// Generic tests for all curves in package
import { secp192r1, secp224r1, secp256k1, secp256r1, secp384r1, secp521r1 } from '../lib/nist.js';
import { ed25519, ed25519ctx, ed25519ph, ed448, ed448ph } from '../lib/ed.js';
import { starkCurve } from '../lib/starknet.js';
import { pallas, vesta } from '../lib/pasta.js';
import { bn254 } from '../lib/bn.js';
// prettier-ignore
const CURVES = {
secp192r1, secp224r1, secp256k1, secp256r1, secp384r1, secp521r1,
ed25519, ed25519ctx, ed25519ph, ed448, ed448ph,
starkCurve,
pallas, vesta,
bn254,
};
for (const name in CURVES) {
const C = CURVES[name];
// Generic sanity tests:
// - group laws:
// G*(CURVE.n-1) + 1 = Point.ZERO
// G*(CURVE.n-2) + 2 = Point.ZERO
// G*(CURVE.n/2).double() = Point.ZERO or Point.BASE?
// rand*G + rand2*G = G*(rand+rand mod N)
// - double works
// ZERO.double() == zero
// - adding zero point works
// - add(samePoint) works
// - add(-samePoint) works
// - 2+2 = 2.double() = 7-5 (should have different Z coordinates, but it is same point)
// ToAffine: Point.BASE = Extended/Jacobian.toAffine()
// Property tests:
// signatures, getSharedKey/etc
//const FC_BIGINT = fc.bigInt(1n + 1n, C.n - 1n);
should(`${name}/Basic`, () => {});
const POINTS = { Point: C.Point, JacobianPoint: C.JacobianPoint, ExtendedPoint: C.ExtendedPoint };
for (const pointName in POINTS) {
const p = POINTS[pointName];
if (!p) continue;
const G = [p.ZERO, p.BASE];
for (let i = 2; i < 10; i++) G.push(G[1].multiply(i));
should(`${name}/${pointName}/Basic`, () => {
// ... And we dont have it
//deepStrictEqual(G[2].double().equals(G[4]), true);
//console.log('Z', G);
});
}
}
// ESM is broken.
import url from 'url';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run();
}

@ -195,7 +195,7 @@ should('secp256k1.Signature.fromDERHex() roundtrip', () => {
should('secp256k1.sign()/should create deterministic signatures with RFC 6979', async () => {
for (const vector of ecdsa.valid) {
let usig = await secp.sign(vector.m, vector.d);
let sig = (usig.toCompactHex());
let sig = usig.toCompactHex();
const vsig = vector.signature;
deepStrictEqual(sig.slice(0, 64), vsig.slice(0, 64));
deepStrictEqual(sig.slice(64, 128), vsig.slice(64, 128));
@ -238,7 +238,7 @@ should('secp256k1.sign()/should create correct DER encoding against libsecp256k1
const privKey = hexToBytes('0101010101010101010101010101010101010101010101010101010101010101');
for (let [msg, exp] of CASES) {
const res = await secp.sign(msg, privKey, { extraEntropy: undefined });
deepStrictEqual((res.toDERHex()), exp);
deepStrictEqual(res.toDERHex(), exp);
const rs = secp.Signature.fromDER(res.toDERHex()).toCompactHex();
deepStrictEqual(secp.Signature.fromCompact(rs).toDERHex(), exp);
}
@ -394,7 +394,7 @@ should('secp256k1.recoverPublicKey()/should recover public key from recovery bit
const recoveredPubkey = sig.recoverPublicKey(message);
// const recoveredPubkey = secp.recoverPublicKey(message, signature, recovery);
deepStrictEqual(recoveredPubkey !== null, true);
deepStrictEqual((recoveredPubkey).toHex(), publicKey);
deepStrictEqual(recoveredPubkey.toHex(), publicKey);
deepStrictEqual(secp.verify(sig, message, publicKey), true);
});
should('secp256k1.recoverPublicKey()/should not recover zero points', () => {
@ -416,10 +416,10 @@ should('secp256k1.recoverPublicKey()/should handle RFC 6979 vectors', async () =
for (const vector of ecdsa.valid) {
if (secp.utils.mod(hexToNumber(vector.m), secp.CURVE.n) === 0n) continue;
let usig = secp.sign(vector.m, vector.d);
let sig = (usig).toDERHex();
let sig = usig.toDERHex();
const vpub = secp.getPublicKey(vector.d);
const recovered = usig.recoverPublicKey(vector.m);
deepStrictEqual((recovered).toHex(), hex(vpub));
deepStrictEqual(recovered.toHex(), hex(vpub));
}
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff