import { deepStrictEqual, throws } from 'assert'; import { describe, should } from 'micro-should'; import { secp192r1, secp224r1, p192, p224 } from './_more-curves.helpers.js'; import { DER } from '../esm/abstract/weierstrass.js'; import { secp256r1, p256 } from '../esm/p256.js'; import { secp384r1, p384 } from '../esm/p384.js'; import { secp521r1, p521 } from '../esm/p521.js'; import { secp256k1 } from '../esm/secp256k1.js'; import { hexToBytes, bytesToHex } from '../esm/abstract/utils.js'; 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 { default as rfc6979 } from './vectors/rfc6979.json' assert { type: 'json' }; 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' }; // 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 secp224r1_shake128_test } from './wycheproof/ecdsa_secp224r1_shake128_test.json' assert { type: 'json' }; import { default as secp256k1_sha256_bitcoin_test } from './wycheproof/ecdsa_secp256k1_sha256_bitcoin_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 secp256k1_shake128_test } from './wycheproof/ecdsa_secp256k1_shake128_test.json' assert { type: 'json' }; import { default as secp256k1_shake256_test } from './wycheproof/ecdsa_secp256k1_shake256_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 secp256r1_shake128_test } from './wycheproof/ecdsa_secp256r1_shake128_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 secp384r1_shake256_test } from './wycheproof/ecdsa_secp384r1_shake256_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 { default as secp521r1_shake256_test } from './wycheproof/ecdsa_secp521r1_shake256_test.json' assert { type: 'json' }; import { sha3_224, sha3_256, sha3_384, sha3_512, shake128, shake256 } from '@noble/hashes/sha3'; import { sha512, sha384 } from '@noble/hashes/sha512'; import { sha224, sha256 } from '@noble/hashes/sha256'; // TODO: maybe add to noble-hashes? const wrapShake = (shake, dkLen) => { const hashC = (msg) => shake(msg, { dkLen }); hashC.outputLen = dkLen; hashC.blockLen = shake.blockLen; hashC.create = () => shake.create({ dkLen }); return hashC; }; const shake128_224 = wrapShake(shake128, 224 / 8); const shake128_256 = wrapShake(shake128, 256 / 8); const shake256_256 = wrapShake(shake256, 256 / 8); const shake256_384 = wrapShake(shake256, 384 / 8); const shake256_512 = wrapShake(shake256, 512 / 8); const hex = bytesToHex; // prettier-ignore const NIST = { secp192r1, P192: p192, secp224r1, P224: p224, secp256r1, P256: p256, secp384r1, P384: p384, secp521r1, P521: p521, secp256k1, }; describe('NIST curves', () => {}); should('fields', () => { const vectors = { secp192r1: 0xfffffffffffffffffffffffffffffffeffffffffffffffffn, secp224r1: 0xffffffffffffffffffffffffffffffff000000000000000000000001n, secp256r1: 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn, secp256k1: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn, secp384r1: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffffn, secp521r1: 0x01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn, }; for (const n in vectors) deepStrictEqual(NIST[n].CURVE.Fp.ORDER, vectors[n]); }); describe('wycheproof ECDH', () => { for (const group of ecdh.testGroups) { const CURVE = NIST[group.curve]; if (!CURVE) continue; should(group.curve, () => { for (const test of group.tests) { if (test.result === 'valid' || test.result === 'acceptable') { try { const pub = CURVE.ProjectivePoint.fromHex(test.public); } catch (e) { // Our strict validation filter doesn't let weird-length DER vectors if (e.message.startsWith('Point of length')) continue; throw e; } const shared = CURVE.getSharedSecret(test.private, test.public); deepStrictEqual(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'); } }); } // More per curve tests const WYCHEPROOF_ECDH = { p224: { curve: p224, tests: [ecdh_secp224r1_test], }, p256: { curve: p256, tests: [ecdh_secp256r1_test], }, secp256k1: { curve: secp256k1, tests: [ecdh_secp256k1_test], }, p384: { curve: p384, tests: [ecdh_secp384r1_test], }, p521: { curve: 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(`additional ${name} (${i}/${j})`, () => { for (const test of group.tests) { if (test.result === 'valid' || test.result === 'acceptable') { try { const pub = curve.ProjectivePoint.fromHex(test.public); } catch (e) { // Our strict validation filter doesn't let weird-length DER vectors if (e.message.includes('Point of length')) 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'); } }); } } } }); const WYCHEPROOF_ECDSA = { p224: { curve: p224, hashes: { 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], }, shake128: { hash: shake128_224, tests: [secp224r1_shake128_test], }, }, }, secp256k1: { curve: secp256k1, hashes: { sha256: { hash: sha256, tests: [secp256k1_sha256_test, secp256k1_sha256_bitcoin_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], }, shake128: { hash: shake128_256, tests: [secp256k1_shake128_test], }, shake256: { hash: shake256_256, tests: [secp256k1_shake256_test], }, }, }, p256: { curve: 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], }, shake128: { hash: shake128_256, tests: [secp256r1_shake128_test], }, }, }, p384: { curve: 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], }, shake256: { hash: shake256_384, tests: [secp384r1_shake256_test], }, }, }, p521: { curve: p521, hashes: { sha3_512: { hash: sha3_512, tests: [secp521r1_sha3_512_test], }, sha512: { hash: sha512, tests: [secp521r1_sha512_test], }, shake256: { hash: shake256_512, tests: [secp521r1_shake256_test], }, }, }, }; function runWycheproof(name, CURVE, group, index) { const key = group.publicKey; const pubKey = CURVE.ProjectivePoint.fromHex(key.uncompressed); deepStrictEqual(pubKey.x, BigInt(`0x${key.wx}`)); deepStrictEqual(pubKey.y, BigInt(`0x${key.wy}`)); const pubR = pubKey.toRawBytes(); for (const test of group.tests) { const m = CURVE.CURVE.hash(hexToBytes(test.msg)); const { sig } = test; if (test.result === 'valid' || test.result === 'acceptable') { try { CURVE.Signature.fromDER(sig); } catch (e) { // Some tests has invalid signature which we don't accept if (e.message.includes('Invalid signature: incorrect length')) continue; throw e; } const verified = CURVE.verify(sig, m, pubR); if (name === 'secp256k1') { // lowS: true for secp256k1 deepStrictEqual(verified, !CURVE.Signature.fromDER(sig).hasHighS(), `${index}: valid`); } else { deepStrictEqual(verified, true, `${index}: valid`); } } else if (test.result === 'invalid') { let failed = false; try { failed = !CURVE.verify(sig, m, pubR); } catch (error) { failed = true; } deepStrictEqual(failed, true, `${index}: invalid`); } else throw new Error('unknown test result'); } } describe('wycheproof ECDSA', () => { should('generic', () => { for (const group of ecdsa.testGroups) { // Tested in secp256k1.test.js let CURVE = NIST[group.key.curve]; if (!CURVE) continue; if (group.key.curve === 'secp224r1' && group.sha !== 'SHA-224') { if (group.sha === 'SHA-256') CURVE = CURVE.create(sha256); } const pubKey = CURVE.ProjectivePoint.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; } // These old Wycheproof vectors which still accept missing zero, new one is not. if (test.flags.includes('MissingZero') && test.result === 'acceptable') test.result = 'invalid'; 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.toHex()); if (group.key.curve === 'secp256k1') { // lowS: true for secp256k1 deepStrictEqual(verified, !CURVE.Signature.fromDER(test.sig).hasHighS(), `valid`); } else { deepStrictEqual(verified, true, `valid`); } } else if (test.result === 'invalid') { let failed = false; try { failed = !CURVE.verify(test.sig, m, pubKey.toHex()); } catch (error) { failed = true; } deepStrictEqual(failed, true, 'invalid'); } else throw new Error('unknown test result'); } } }); for (const name in WYCHEPROOF_ECDSA) { const { curve, hashes } = WYCHEPROOF_ECDSA[name]; describe(name, () => { for (const hName in hashes) { const { hash, tests } = hashes[hName]; const CURVE = curve.create(hash); should(`${name}/${hName}`, () => { for (let i = 0; i < tests.length; i++) { const groups = tests[i].testGroups; for (let j = 0; j < groups.length; j++) { const group = groups[j]; runWycheproof(name, CURVE, group, `${i}/${j}`); } } }); } }); } }); const hexToBigint = (hex) => BigInt(`0x${hex}`); describe('RFC6979', () => { for (const v of rfc6979) { should(v.curve, () => { const curve = NIST[v.curve]; deepStrictEqual(curve.CURVE.n, hexToBigint(v.q)); const pubKey = curve.getPublicKey(v.private); const pubPoint = curve.ProjectivePoint.fromHex(pubKey); deepStrictEqual(pubPoint.x, hexToBigint(v.Ux)); deepStrictEqual(pubPoint.y, hexToBigint(v.Uy)); for (const c of v.cases) { const h = curve.CURVE.hash(c.message); const sigObj = curve.sign(h, v.private); deepStrictEqual(sigObj.r, hexToBigint(c.r), 'R'); deepStrictEqual(sigObj.s, hexToBigint(c.s), 'S'); deepStrictEqual(curve.verify(sigObj.toDERRawBytes(), h, pubKey), true, 'verify(1)'); deepStrictEqual(curve.verify(sigObj, h, pubKey), true, 'verify(2)'); } }); } }); should('DER Leading zero', () => { // Valid DER deepStrictEqual( DER.toSig( '303c021c70049af31f8348673d56cece2b27e587a402f2a48f0b21a7911a480a021c2840bf24f6f66be287066b7cbf38788e1b7770b18fd1aa6a26d7c6dc' ), { r: 11796871166002955884468185727465595477481802908758874298363724580874n, s: 4239126896857047637966364941684493209162496401998708914961872570076n, } ); // Invalid DER (missing trailing zero) throws(() => DER.toSig( '303c021c70049af31f8348673d56cece2b27e587a402f2a48f0b21a7911a480a021cd7bf40db0909941d78f9948340c69e14c5417f8c840b7edb35846361' ) ); // Correctly adds trailing zero deepStrictEqual( DER.hexFromSig({ r: 11796871166002955884468185727465595477481802908758874298363724580874n, s: 22720819770293592156700650145335132731295311312425682806720849797985n, }), '303d021c70049af31f8348673d56cece2b27e587a402f2a48f0b21a7911a480a021d00d7bf40db0909941d78f9948340c69e14c5417f8c840b7edb35846361' ); }); // ESM is broken. import url from 'url'; if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { should.run(); }