P256.test.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { secp256r1 } = require('@noble/curves/p256');
  4. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  5. const N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
  6. // As in ECDSA, signatures are malleable and the tooling produce both high and low S values.
  7. // We need to ensure that the s value is in the lower half of the order of the curve.
  8. const ensureLowerOrderS = ({ s, recovery, ...rest }) => {
  9. if (s > N / 2n) {
  10. s = N - s;
  11. recovery = 1 - recovery;
  12. }
  13. return { s, recovery, ...rest };
  14. };
  15. const prepareSignature = (
  16. privateKey = secp256r1.utils.randomPrivateKey(),
  17. messageHash = ethers.hexlify(ethers.randomBytes(0x20)),
  18. ) => {
  19. const publicKey = [
  20. secp256r1.getPublicKey(privateKey, false).slice(0x01, 0x21),
  21. secp256r1.getPublicKey(privateKey, false).slice(0x21, 0x41),
  22. ].map(ethers.hexlify);
  23. const { r, s, recovery } = ensureLowerOrderS(secp256r1.sign(messageHash.replace(/0x/, ''), privateKey));
  24. const signature = [r, s].map(v => ethers.toBeHex(v, 0x20));
  25. return { privateKey, publicKey, signature, recovery, messageHash };
  26. };
  27. describe('P256', function () {
  28. async function fixture() {
  29. return { mock: await ethers.deployContract('$P256') };
  30. }
  31. beforeEach(async function () {
  32. Object.assign(this, await loadFixture(fixture));
  33. });
  34. describe('with signature', function () {
  35. beforeEach(async function () {
  36. Object.assign(this, prepareSignature());
  37. });
  38. it('verify valid signature', async function () {
  39. await expect(this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be.true;
  40. await expect(this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
  41. .true;
  42. await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
  43. .true;
  44. });
  45. it('verify improper signature', async function () {
  46. const signature = this.signature;
  47. this.signature[0] = ethers.toBeHex(N, 0x20); // r = N
  48. await expect(this.mock.$verify(this.messageHash, ...signature, ...this.publicKey)).to.eventually.be.false;
  49. await expect(this.mock.$verifySolidity(this.messageHash, ...signature, ...this.publicKey)).to.eventually.be.false;
  50. await expect(this.mock.$verifyNative(this.messageHash, ...signature, ...this.publicKey)).to.eventually.be.false;
  51. });
  52. it('recover public key', async function () {
  53. await expect(this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.eventually.deep.equal(
  54. this.publicKey,
  55. );
  56. });
  57. it('recovers (0,0) for invalid recovery bit', async function () {
  58. await expect(this.mock.$recovery(this.messageHash, 2, ...this.signature)).to.eventually.deep.equal([
  59. ethers.ZeroHash,
  60. ethers.ZeroHash,
  61. ]);
  62. });
  63. it('recovers (0,0) for improper signature', async function () {
  64. const signature = this.signature;
  65. this.signature[0] = ethers.toBeHex(N, 0x20); // r = N
  66. await expect(this.mock.$recovery(this.messageHash, this.recovery, ...signature)).to.eventually.deep.equal([
  67. ethers.ZeroHash,
  68. ethers.ZeroHash,
  69. ]);
  70. });
  71. it('reject signature with flipped public key coordinates ([x,y] >> [y,x])', async function () {
  72. // flip public key
  73. this.publicKey.reverse();
  74. await expect(this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be.false;
  75. await expect(this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
  76. .false;
  77. await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
  78. .false;
  79. });
  80. it('reject signature with flipped signature values ([r,s] >> [s,r])', async function () {
  81. // Preselected signature where `r < N/2` and `s < N/2`
  82. this.signature = [
  83. '0x45350225bad31e89db662fcc4fb2f79f349adbb952b3f652eed1f2aa72fb0356',
  84. '0x513eb68424c42630012309eee4a3b43e0bdc019d179ef0e0c461800845e237ee',
  85. ];
  86. // Corresponding hash and public key
  87. this.messageHash = '0x2ad1f900fe63745deeaedfdf396cb6f0f991c4338a9edf114d52f7d1812040a0';
  88. this.publicKey = [
  89. '0x9e30de165e521257996425d9bf12a7d366925614bf204eabbb78172b48e52e59',
  90. '0x94bf0fe72f99654d7beae4780a520848e306d46a1275b965c4f4c2b8e9a2c08d',
  91. ];
  92. // Make sure it works
  93. await expect(this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be.true;
  94. // Flip signature
  95. this.signature.reverse();
  96. await expect(this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be.false;
  97. await expect(this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
  98. .false;
  99. await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
  100. .false;
  101. await expect(
  102. this.mock.$recovery(this.messageHash, this.recovery, ...this.signature),
  103. ).to.eventually.not.deep.equal(this.publicKey);
  104. });
  105. it('reject signature with invalid message hash', async function () {
  106. // random message hash
  107. this.messageHash = ethers.hexlify(ethers.randomBytes(32));
  108. await expect(this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be.false;
  109. await expect(this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
  110. .false;
  111. await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey)).to.eventually.be
  112. .false;
  113. await expect(
  114. this.mock.$recovery(this.messageHash, this.recovery, ...this.signature),
  115. ).to.eventually.not.deep.equal(this.publicKey);
  116. });
  117. it('fail to recover signature with invalid recovery bit', async function () {
  118. // flip recovery bit
  119. this.recovery = 1 - this.recovery;
  120. await expect(
  121. this.mock.$recovery(this.messageHash, this.recovery, ...this.signature),
  122. ).to.eventually.not.deep.equal(this.publicKey);
  123. });
  124. });
  125. // test cases for https://github.com/C2SP/wycheproof/blob/4672ff74d68766e7785c2cac4c597effccef2c5c/testvectors/ecdsa_secp256r1_sha256_p1363_test.json
  126. describe('wycheproof tests', function () {
  127. for (const { key, tests } of require('./ecdsa_secp256r1_sha256_p1363_test.json').testGroups) {
  128. // parse public key
  129. let [x, y] = [key.wx, key.wy].map(v => ethers.stripZerosLeft('0x' + v, 32));
  130. if (x.length > 66 || y.length > 66) continue;
  131. x = ethers.zeroPadValue(x, 32);
  132. y = ethers.zeroPadValue(y, 32);
  133. // run all tests for this key
  134. for (const { tcId, comment, msg, sig, result } of tests) {
  135. // only keep properly formatted signatures
  136. if (sig.length != 128) continue;
  137. it(`${tcId}: ${comment}`, async function () {
  138. // split signature, and reduce modulo N
  139. let [r, s] = Array(2)
  140. .fill()
  141. .map((_, i) => ethers.toBigInt('0x' + sig.substring(64 * i, 64 * (i + 1))));
  142. // move s to lower part of the curve if needed
  143. if (s <= N && s > N / 2n) s = N - s;
  144. // prepare signature
  145. r = ethers.toBeHex(r, 32);
  146. s = ethers.toBeHex(s, 32);
  147. // hash
  148. const messageHash = ethers.sha256('0x' + msg);
  149. // check verify
  150. await expect(this.mock.$verify(messageHash, r, s, x, y)).to.eventually.equal(result == 'valid');
  151. });
  152. }
  153. }
  154. });
  155. });