diff --git a/test/ed25519.helpers.js b/test/ed25519.helpers.js index c426896..5a8cfe9 100644 --- a/test/ed25519.helpers.js +++ b/test/ed25519.helpers.js @@ -1 +1,2 @@ +export { numberToBytesLE } from '../esm/abstract/utils.js'; export { ed25519, ED25519_TORSION_SUBGROUP } from '../esm/ed25519.js'; diff --git a/test/ed25519.test.js b/test/ed25519.test.js index dbc4f4f..f086349 100644 --- a/test/ed25519.test.js +++ b/test/ed25519.test.js @@ -1,9 +1,9 @@ import { deepStrictEqual, strictEqual, throws } from 'assert'; 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 { 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 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++) { const group = ed25519vectors.testGroups[g]; 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 signature = ed.sign(message, to32Bytes(1n)); const publicKey = ed.getPublicKey(to32Bytes(1n)); // <- was 1n @@ -387,15 +387,35 @@ describe('ed25519', () => { strictEqual(cleared.isTorsionFree(), true, `cleared must be torsionFree: ${hex}`); } }); -}); -should('ed25519 bug', () => { - const t = 81718630521762619991978402609047527194981150691135404693881672112315521837062n; - const point = ed25519.ExtendedPoint.fromAffine({ x: t, y: t }); - throws(() => point.assertValidity()); - // Otherwise (without assertValidity): - // const point2 = point.double(); - // point2.toAffine(); // crash! + 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('not accept point without z, t', () => { + const t = 81718630521762619991978402609047527194981150691135404693881672112315521837062n; + const point = ed25519.ExtendedPoint.fromAffine({ x: t, y: t }); + throws(() => point.assertValidity()); + // Otherwise (without assertValidity): + // const point2 = point.double(); + // point2.toAffine(); // crash! + }); }); // ESM is broken. diff --git a/test/ed448.test.js b/test/ed448.test.js index f892cad..c96c07e 100644 --- a/test/ed448.test.js +++ b/test/ed448.test.js @@ -2,7 +2,7 @@ import { deepStrictEqual, throws } from 'assert'; import { describe, should } from 'micro-should'; import * as fc from 'fast-check'; 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 { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' }; import { default as x448vectors } from './wycheproof/x448_test.json' assert { type: 'json' }; @@ -467,101 +467,6 @@ describe('ed448', () => { } }); - // ECDH - const rfc7748Mul = [ - { - scalar: - '3d262fddf9ec8e88495266fea19a34d28882acef045104d0d1aae121700a779c984c24f8cdd78fbff44943eba368f54b29259a4f1c600ad3', - u: '06fce640fa3487bfda5f6cf2d5263f8aad88334cbd07437f020f08f9814dc031ddbdc38c19c6da2583fa5429db94ada18aa7a7fb4ef8a086', - outputU: - 'ce3e4ff95a60dc6697da1db1d85e6afbdf79b50a2412d7546d5f239fe14fbaadeb445fc66a01b0779d98223961111e21766282f73dd96b6f', - }, - { - scalar: - '203d494428b8399352665ddca42f9de8fef600908e0d461cb021f8c538345dd77c3e4806e25f46d3315c44e0a5b4371282dd2c8d5be3095f', - u: '0fbcc2f993cd56d3305b0b7d9e55d4c1a8fb5dbb52f8e9a1e9b6201b165d015894e56c4d3570bee52fe205e28a78b91cdfbde71ce8d157db', - outputU: - '884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d', - }, - ]; - describe('RFC7748', () => { - for (let i = 0; i < rfc7748Mul.length; i++) { - const v = rfc7748Mul[i]; - should(`scalarMult (${i})`, () => { - deepStrictEqual(hex(x448.scalarMult(v.scalar, v.u)), 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 = x448.GuBytes; - for (let i = 0, u = k; i < iters; i++) [k, u] = [x448.scalarMult(k, u), 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(x448.getPublicKey(alicePrivate))); - deepStrictEqual(bobPublic, hex(x448.getPublicKey(bobPrivate))); - deepStrictEqual(hex(x448.scalarMult(alicePrivate, bobPublic)), shared); - deepStrictEqual(hex(x448.scalarMult(bobPrivate, alicePublic)), shared); - }); - - describe('wycheproof', () => { - const group = x448vectors.testGroups[0]; - should(`X448`, () => { - for (let i = 0; i < group.tests.length; i++) { - const v = group.tests[i]; - const index = `(${i}, ${v.result}) ${v.comment}`; - if (v.result === 'valid' || v.result === 'acceptable') { - try { - const shared = hex(x448.scalarMult(v.private, v.public)); - deepStrictEqual(shared, v.shared, index); - } 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 { - x448.scalarMult(v.private, v.public); - } catch (error) { - failed = true; - } - deepStrictEqual(failed, true, index); - } else throw new Error('unknown test result'); - } - }); - }); - // should('X448: should convert base point to montgomery using fromPoint', () => { // deepStrictEqual( // hex(ed.montgomeryCurve.UfromPoint(Point.BASE)), @@ -584,87 +489,204 @@ describe('ed448', () => { // } // }); - const VECTORS_RFC8032_CTX = [ - { - secretKey: - 'c4eab05d357007c632f3dbb48489924d552b08fe0c353a0d4a1f00acda2c463afbea67c5e8d2877c5e3bc397a659949ef8021e954e0a12274e', - publicKey: - '43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a9480', - message: '03', - context: '666f6f', - signature: - 'd4f8f6131770dd46f40867d6fd5d5055' + - 'de43541f8c5e35abbcd001b32a89f7d2' + - '151f7647f11d8ca2ae279fb842d60721' + - '7fce6e042f6815ea000c85741de5c8da' + - '1144a6a1aba7f96de42505d7a7298524' + - 'fda538fccbbb754f578c1cad10d54d0d' + - '5428407e85dcbc98a49155c13764e66c' + - '3c00', - }, - ]; + 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); + }); + } + }); - 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); + 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); }); - } + }); - 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', - }, - ]; + describe('RFC7748 X448 ECDH', () => { + // 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(`scalarMult (${i})`, () => { + deepStrictEqual(hex(x448.scalarMult(v.scalar, v.u)), v.outputU); + }); + } - 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); + 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(`scalarMult iterated ${iters}x`, () => { + let k = x448.GuBytes; + for (let i = 0, u = k; i < iters; i++) [k, u] = [x448.scalarMult(k, u), k]; + deepStrictEqual(hex(k), scalar); + }); + } + + should('getSharedKey', () => { + const alicePrivate = + '9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b'; + const alicePublic = + '9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0'; + const bobPrivate = + '1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d'; + const bobPublic = + '3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609'; + const shared = + '07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d'; + deepStrictEqual(alicePublic, hex(x448.getPublicKey(alicePrivate))); + deepStrictEqual(bobPublic, hex(x448.getPublicKey(bobPrivate))); + deepStrictEqual(hex(x448.scalarMult(alicePrivate, bobPublic)), shared); + deepStrictEqual(hex(x448.scalarMult(bobPrivate, alicePublic)), shared); }); - } - should('X448 base point', () => { - const { x, y } = Point.BASE; - const { Fp } = ed448.CURVE; - // const invX = Fp.invert(x * x); // x² - const u = Fp.div(Fp.create(y * y), Fp.create(x * x)); // (y²/x²) - // const u = Fp.create(y * y * invX); - deepStrictEqual(numberToBytesLE(u, 56), x448.GuBytes); + describe('wycheproof', () => { + const group = x448vectors.testGroups[0]; + should(`X448`, () => { + for (let i = 0; i < group.tests.length; i++) { + const v = group.tests[i]; + const index = `(${i}, ${v.result}) ${v.comment}`; + if (v.result === 'valid' || v.result === 'acceptable') { + try { + const shared = hex(x448.scalarMult(v.private, v.public)); + deepStrictEqual(shared, v.shared, index); + } 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 { + x448.scalarMult(v.private, v.public); + } catch (error) { + failed = true; + } + deepStrictEqual(failed, true, index); + } else throw new Error('unknown test result'); + } + }); + }); + + should('have proper base point', () => { + const { x, y } = Point.BASE; + const { Fp } = ed448.CURVE; + // const invX = Fp.invert(x * x); // x² + const u = Fp.div(Fp.create(y * y), Fp.create(x * x)); // (y²/x²) + // const u = Fp.create(y * y * invX); + deepStrictEqual(numberToBytesLE(u, 56), x448.GuBytes); + }); }); });