diff --git a/README.md b/README.md index 9fead4a7..8ecf6102 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,12 @@ contracts │ ├── bn │ │ └── U512 — "A hyperoptimized uint512 implementation" │ ├── crypto -│ │ ├── ECDSA256 — "ECDSA verification over any 256-bit curves" -│ │ ├── ECDSA384 — "ECDSA verification over any 384-bit curves" -│ │ ├── ECDSA512 — "ECDSA verification over any 512-bit curves" -│ │ └── RSASSAPSS — "RSASSA-PSS verification with MGF1" +│ │ ├── EC256 — "Elliptic curve arithmetic over a 256-bit prime field" +│ │ ├── ECDSA256 — "ECDSA verification over any 256-bit curve" +│ │ ├── ECDSA384 — "ECDSA verification over any 384-bit curve" +│ │ ├── ECDSA512 — "ECDSA verification over any 512-bit curve" +│ │ ├── Schnorr256 — "Schnorr signature verification over any 256-bit curve" +│ │ └── RSASSAPSS — "RSASSA-PSS signature verification with MGF1" │ ├── data—structures │ │ ├── AvlTree — "AVL tree implementation with an iterator traversal" │ │ ├── CartesianMerkleTree — "CMT reference implementation" diff --git a/contracts/libs/crypto/EC256.sol b/contracts/libs/crypto/EC256.sol new file mode 100644 index 00000000..05d947b0 --- /dev/null +++ b/contracts/libs/crypto/EC256.sol @@ -0,0 +1,477 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +/** + * @notice Cryptography module + * + * Elliptic curve arithmetic over a 256-bit prime field (Weierstrass curve y^2 = x^3 + ax + b (mod p)). + */ +library EC256 { + /** + * @notice 256-bit curve parameters. + * @param a The curve coefficient a. + * @param b The curve coefficient b. + * @param p The base field size. + * @param n The scalar field size. + * @param gx The x-coordinate of the basepoint G. + * @param gy The y-coordinate of the basepoint G. + */ + struct Curve { + uint256 a; + uint256 b; + uint256 p; + uint256 n; + uint256 gx; + uint256 gy; + } + + /** + * @notice Affine representation of a curve point. + * @param x The x-coordinate. + * @param y The y-coordinate. + */ + struct APoint { + uint256 x; + uint256 y; + } + + /** + * @notice Jacobian representation of a curve point. + * @param x The Jacobian X coordinate. + * @param y The Jacobian Y coordinate. + * @param z The Jacobian Z coordinate. + */ + struct JPoint { + uint256 x; + uint256 y; + uint256 z; + } + + /** + * @notice Returns the generator (base) point of the curve in affine form. + * @param ec The curve parameters. + * @return aPoint_ The basepoint (gx, gy). + */ + function basepoint(Curve memory ec) internal pure returns (APoint memory aPoint_) { + return APoint(ec.gx, ec.gy); + } + + /** + * @notice Returns the generator (base) point of the curve in jacobian form. + * @param ec The curve parameters. + * @return jPoint_ The basepoint (gx, gy, 1). + */ + function jbasepoint(Curve memory ec) internal pure returns (JPoint memory jPoint_) { + return JPoint(ec.gx, ec.gy, 1); + } + + /** + * @notice Reduces an arbitrary uint256 into the scalar field [0, n). + * @param ec The curve parameters. + * @param u256_ The integer to reduce. + * @return scalar_ The result of u256_ mod n. + */ + function toScalar(Curve memory ec, uint256 u256_) internal pure returns (uint256 scalar_) { + return u256_ % ec.n; + } + + /** + * @notice Checks whether an affine point lies on the curve. + * @param ec The curve parameters. + * @param aPoint_ The affine point to test. + * @return result_ True if `aPoint_` satisfies y^2 = x^3 + ax + b (mod p). + */ + function isOnCurve( + Curve memory ec, + APoint memory aPoint_ + ) internal pure returns (bool result_) { + return _isOnCurve(aPoint_.x, aPoint_.y, ec.a, ec.b, ec.p); + } + + /** + * @notice Checks whether a scalar is in the valid range [0, n). + * @param ec The curve parameters. + * @param scalar_ The scalar to test. + * @return result_ True if scalar < n. + */ + function isValidScalar(Curve memory ec, uint256 scalar_) internal pure returns (bool result_) { + return scalar_ < ec.n; + } + + /** + * @notice Converts a point from Jacobian to affine coordinates. + * @param ec The curve parameters. + * @param jPoint_ The Jacobian point (X, Y, Z). + * @return aPoint_ The equivalent affine point (x, y). + */ + function toAffine( + Curve memory ec, + JPoint memory jPoint_ + ) internal view returns (APoint memory aPoint_) { + (aPoint_.x, aPoint_.y) = _affineFromJacobian(jPoint_, ec.p); + } + + /** + * @notice Converts an affine point to Jacobian coordinates. + * @param aPoint_ The affine point (x, y). + * @return jPoint_ The Jacobian representation (x, y, 1). + */ + function toJacobian(APoint memory aPoint_) internal pure returns (JPoint memory jPoint_) { + return JPoint(aPoint_.x, aPoint_.y, 1); + } + + /** + * @notice Checks whether a Jacobian point is the point at infinity. + * @param jPoint_ The Jacobian point to test. + * @return result_ True if Z == 0. + */ + function isJacobianInfinity(JPoint memory jPoint_) internal pure returns (bool result_) { + return jPoint_.z == 0; + } + + /** + * @notice Returns the Jacobian representation of the point at infinity. + * @return jPoint_ The point at infinity (0, 0, 0). + */ + function jinfinity() internal pure returns (JPoint memory jPoint_) { + return JPoint(0, 0, 0); + } + + /** + * @notice Compares two Jacobian points for equality in affine coordinates. + * @param ec The curve parameters. + * @param jPoint1_ The first Jacobian point. + * @param jPoint2_ The second Jacobian point. + * @return result_ True if their affine representations match. + */ + function jEqual( + Curve memory ec, + JPoint memory jPoint1_, + JPoint memory jPoint2_ + ) internal view returns (bool result_) { + APoint memory aPoint1_ = toAffine(ec, jPoint1_); + APoint memory aPoint2_ = toAffine(ec, jPoint2_); + + return aPoint1_.x == aPoint2_.x && aPoint1_.y == aPoint2_.y; + } + + /** + * @notice Point multiplication: R = u*P using 4-bit windowed method. + * @param ec The curve parameters. + * @param jPoint_ The Jacobian point P. + * @param scalar_ The scalar u. + * @return jPoint2_ The Jacobian representation of result point R. + */ + function jMultShamir( + Curve memory ec, + JPoint memory jPoint_, + uint256 scalar_ + ) internal pure returns (JPoint memory jPoint2_) { + unchecked { + JPoint[16] memory jPoints_ = _preComputeJacobianPoints(ec, jPoint_); + + for (uint256 i = 0; i < 64; ++i) { + jPoint2_ = jDoublePoint(ec, jPoint2_); + jPoint2_ = jDoublePoint(ec, jPoint2_); + jPoint2_ = jDoublePoint(ec, jPoint2_); + jPoint2_ = jDoublePoint(ec, jPoint2_); + + // Read 4 bits of u1 which corresponds to the lookup index in the table. + uint256 pos_ = scalar_ >> 252; + scalar_ <<= 4; + + jPoint2_ = jAddPoint(ec, jPoints_[pos_], jPoint2_); + } + } + } + + /** + * @notice Simultaneous double-scalar multiplication: R = u1*P1 + u2*P2 via Strauss–Shamir. + * @param ec The curve parameters. + * @param jPoint1_ The first Jacobian point P1. + * @param jPoint2_ The second Jacobian point P2. + * @param scalar1_ The first scalar u1. + * @param scalar2_ The second scalar u2. + * @return jPoint3_ The Jacobian representation of result point R. + */ + function jMultShamir2( + Curve memory ec, + JPoint memory jPoint1_, + JPoint memory jPoint2_, + uint256 scalar1_, + uint256 scalar2_ + ) internal pure returns (JPoint memory jPoint3_) { + unchecked { + JPoint[16] memory jPoints_ = _preComputeJacobianPoints2(ec, jPoint1_, jPoint2_); + + for (uint256 i = 0; i < 128; ++i) { + jPoint3_ = jDoublePoint(ec, jPoint3_); + jPoint3_ = jDoublePoint(ec, jPoint3_); + + // Read 2 bits of u1, and 2 bits of u2. Combining the two gives the lookup index in the table. + uint256 pos_ = ((scalar1_ >> 252) & 0xc) | ((scalar2_ >> 254) & 0x3); + scalar1_ <<= 2; + scalar2_ <<= 2; + + jPoint3_ = jAddPoint(ec, jPoints_[pos_], jPoint3_); + } + } + } + + /** + * @notice Adds two Jacobian points: R = P1 + P2. + * @param ec The curve parameters. + * @param jPoint1_ The first Jacobian point P1. + * @param jPoint2_ The second Jacobian point P2. + * @return jPoint3_ The Jacobian representation of result point R. + */ + function jAddPoint( + Curve memory ec, + JPoint memory jPoint1_, + JPoint memory jPoint2_ + ) internal pure returns (JPoint memory jPoint3_) { + if (isJacobianInfinity(jPoint1_)) { + return JPoint(jPoint2_.x, jPoint2_.y, jPoint2_.z); + } + + if (isJacobianInfinity(jPoint2_)) { + return JPoint(jPoint1_.x, jPoint1_.y, jPoint1_.z); + } + + (uint256 x_, uint256 y_, uint256 z_) = _jAdd(jPoint1_, jPoint2_, ec.p, ec.a); + + return JPoint(x_, y_, z_); + } + + /** + * @notice Doubles a Jacobian point: R = 2*P. + * @param ec The curve parameters. + * @param jPoint1_ The Jacobian point P to double. + * @return jPoint2_ The Jacobian representation of result point R. + */ + function jDoublePoint( + Curve memory ec, + JPoint memory jPoint1_ + ) internal pure returns (JPoint memory jPoint2_) { + if (isJacobianInfinity(jPoint1_)) { + return JPoint(jPoint1_.x, jPoint1_.y, jPoint1_.z); + } + + (uint256 x_, uint256 y_, uint256 z_) = _jDouble( + jPoint1_.x, + jPoint1_.y, + jPoint1_.z, + ec.p, + ec.a + ); + + return JPoint(x_, y_, z_); + } + + /** + * @dev Point addition on the jacobian coordinates + * Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-1998-cmo-2 + */ + function _jAdd( + JPoint memory jPoint1_, + JPoint memory jPoint2_, + uint256 p_, + uint256 a_ + ) private pure returns (uint256 jx3_, uint256 jy3_, uint256 jz3_) { + assembly ("memory-safe") { + let zz1_ := mulmod(mload(add(jPoint1_, 0x40)), mload(add(jPoint1_, 0x40)), p_) + let s1_ := mulmod( + mload(add(jPoint1_, 0x20)), + mulmod( + mulmod(mload(add(jPoint2_, 0x40)), mload(add(jPoint2_, 0x40)), p_), + mload(add(jPoint2_, 0x40)), + p_ + ), + p_ + ) + let r_ := addmod( + mulmod( + mload(add(jPoint2_, 0x20)), + mulmod(zz1_, mload(add(jPoint1_, 0x40)), p_), + p_ + ), + sub(p_, s1_), + p_ + ) + let u1_ := mulmod( + mload(jPoint1_), + mulmod(mload(add(jPoint2_, 0x40)), mload(add(jPoint2_, 0x40)), p_), + p_ + ) + let h_ := addmod(mulmod(mload(jPoint2_), zz1_, p_), sub(p_, u1_), p_) + + // detect edge cases where inputs are identical + switch and(iszero(r_), iszero(h_)) + // case 0: points are different + case 0 { + let hh_ := mulmod(h_, h_, p_) + + jx3_ := addmod( + addmod(mulmod(r_, r_, p_), sub(p_, mulmod(h_, hh_, p_)), p_), + sub(p_, mulmod(2, mulmod(u1_, hh_, p_), p_)), + p_ + ) + jy3_ := addmod( + mulmod(r_, addmod(mulmod(u1_, hh_, p_), sub(p_, jx3_), p_), p_), + sub(p_, mulmod(s1_, mulmod(h_, hh_, p_), p_)), + p_ + ) + jz3_ := mulmod( + h_, + mulmod(mload(add(jPoint1_, 0x40)), mload(add(jPoint2_, 0x40)), p_), + p_ + ) + } + // case 1: points are equal + case 1 { + let yy_ := mulmod(mload(add(jPoint2_, 0x20)), mload(add(jPoint2_, 0x20)), p_) + let zz_ := mulmod(mload(add(jPoint2_, 0x40)), mload(add(jPoint2_, 0x40)), p_) + let xx_ := mulmod(mload(jPoint2_), mload(jPoint2_), p_) + let m_ := addmod(mulmod(3, xx_, p_), mulmod(a_, mulmod(zz_, zz_, p_), p_), p_) + let s_ := mulmod(4, mulmod(mload(jPoint2_), yy_, p_), p_) + + jx3_ := addmod(mulmod(m_, m_, p_), sub(p_, mulmod(2, s_, p_)), p_) + + // cut the computation to avoid stack too deep + let rytmp1_ := sub(p_, mulmod(8, mulmod(yy_, yy_, p_), p_)) + let rytmp2_ := addmod(s_, sub(p_, jx3_), p_) + jy3_ := addmod(mulmod(m_, rytmp2_, p_), rytmp1_, p_) + + jz3_ := mulmod( + 2, + mulmod(mload(add(jPoint2_, 0x20)), mload(add(jPoint2_, 0x40)), p_), + p_ + ) + } + } + } + + /** + * @dev Point doubling on the jacobian coordinates + * Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-1998-cmo-2 + */ + function _jDouble( + uint256 jx1_, + uint256 jy1_, + uint256 jz1_, + uint256 p_, + uint256 a_ + ) private pure returns (uint256 jx2_, uint256 jy2_, uint256 jz2_) { + assembly ("memory-safe") { + let yy_ := mulmod(jy1_, jy1_, p_) + let zz_ := mulmod(jz1_, jz1_, p_) + let m_ := addmod( + mulmod(3, mulmod(jx1_, jx1_, p_), p_), + mulmod(a_, mulmod(zz_, zz_, p_), p_), + p_ + ) + let s_ := mulmod(4, mulmod(jx1_, yy_, p_), p_) + + jx2_ := addmod(mulmod(m_, m_, p_), sub(p_, mulmod(2, s_, p_)), p_) + jy2_ := addmod( + mulmod(m_, addmod(s_, sub(p_, jx2_), p_), p_), + sub(p_, mulmod(8, mulmod(yy_, yy_, p_), p_)), + p_ + ) + jz2_ := mulmod(2, mulmod(jy1_, jz1_, p_), p_) + } + } + + /** + * @dev Internal conversion from Jacobian to affine coordinates. + */ + function _affineFromJacobian( + JPoint memory jPoint_, + uint256 p_ + ) private view returns (uint256 ax_, uint256 ay_) { + if (isJacobianInfinity(jPoint_)) { + return (0, 0); + } + + uint256 zInverse_ = Math.invModPrime(jPoint_.z, p_); + + assembly ("memory-safe") { + let zzInverse_ := mulmod(zInverse_, zInverse_, p_) + + ax_ := mulmod(mload(jPoint_), zzInverse_, p_) + ay_ := mulmod(mload(add(jPoint_, 0x20)), mulmod(zzInverse_, zInverse_, p_), p_) + } + } + + /** + * @dev Internal curve equation check in affine coordinates. + */ + function _isOnCurve( + uint256 ax_, + uint256 ay_, + uint256 a_, + uint256 b_, + uint256 p_ + ) private pure returns (bool result_) { + assembly ("memory-safe") { + let lhs_ := mulmod(ay_, ay_, p_) + let rhs_ := addmod(mulmod(addmod(mulmod(ax_, ax_, p_), a_, p_), ax_, p_), b_, p_) + + // Should conform with the Weierstrass equation + result_ := and(and(lt(ax_, p_), lt(ay_, p_)), eq(lhs_, rhs_)) + } + } + + /** + * @dev Precomputes 4-bit window lookup table for one point (Shamir's trick) + */ + function _preComputeJacobianPoints( + Curve memory ec, + JPoint memory jPoint_ + ) private pure returns (JPoint[16] memory jPoints_) { + jPoints_[0x00] = jinfinity(); + jPoints_[0x01] = JPoint(jPoint_.x, jPoint_.y, jPoint_.z); + jPoints_[0x02] = jDoublePoint(ec, jPoints_[0x01]); + jPoints_[0x04] = jDoublePoint(ec, jPoints_[0x02]); + jPoints_[0x08] = jDoublePoint(ec, jPoints_[0x04]); + jPoints_[0x03] = jAddPoint(ec, jPoints_[0x01], jPoints_[0x02]); + jPoints_[0x06] = jDoublePoint(ec, jPoints_[0x03]); + jPoints_[0x0c] = jDoublePoint(ec, jPoints_[0x06]); + jPoints_[0x05] = jAddPoint(ec, jPoints_[0x01], jPoints_[0x04]); + jPoints_[0x0a] = jDoublePoint(ec, jPoints_[0x05]); + jPoints_[0x07] = jAddPoint(ec, jPoints_[0x01], jPoints_[0x06]); + jPoints_[0x0e] = jDoublePoint(ec, jPoints_[0x07]); + jPoints_[0x09] = jAddPoint(ec, jPoints_[0x01], jPoints_[0x08]); + jPoints_[0x0b] = jAddPoint(ec, jPoints_[0x01], jPoints_[0x0a]); + jPoints_[0x0d] = jAddPoint(ec, jPoints_[0x01], jPoints_[0x0c]); + jPoints_[0x0f] = jAddPoint(ec, jPoints_[0x01], jPoints_[0x0e]); + } + + /** + * @dev Precomputes 2-bit window lookup table for two points (Shamir's trick) + */ + function _preComputeJacobianPoints2( + Curve memory ec, + JPoint memory jPoint1_, + JPoint memory jPoint2_ + ) private pure returns (JPoint[16] memory jPoints_) { + jPoints_[0x00] = jinfinity(); + jPoints_[0x01] = JPoint(jPoint1_.x, jPoint1_.y, jPoint1_.z); + jPoints_[0x04] = JPoint(jPoint2_.x, jPoint2_.y, jPoint2_.z); + jPoints_[0x02] = jDoublePoint(ec, jPoints_[0x01]); + jPoints_[0x08] = jDoublePoint(ec, jPoints_[0x04]); + jPoints_[0x03] = jAddPoint(ec, jPoints_[0x01], jPoints_[0x02]); + jPoints_[0x05] = jAddPoint(ec, jPoints_[0x01], jPoints_[0x04]); + jPoints_[0x06] = jAddPoint(ec, jPoints_[0x02], jPoints_[0x04]); + jPoints_[0x07] = jAddPoint(ec, jPoints_[0x03], jPoints_[0x04]); + jPoints_[0x09] = jAddPoint(ec, jPoints_[0x01], jPoints_[0x08]); + jPoints_[0x0a] = jAddPoint(ec, jPoints_[0x02], jPoints_[0x08]); + jPoints_[0x0b] = jAddPoint(ec, jPoints_[0x03], jPoints_[0x08]); + jPoints_[0x0c] = jAddPoint(ec, jPoints_[0x04], jPoints_[0x08]); + jPoints_[0x0d] = jAddPoint(ec, jPoints_[0x01], jPoints_[0x0c]); + jPoints_[0x0e] = jAddPoint(ec, jPoints_[0x02], jPoints_[0x0c]); + jPoints_[0x0f] = jAddPoint(ec, jPoints_[0x03], jPoints_[0x0c]); + } +} diff --git a/contracts/libs/crypto/ECDSA256.sol b/contracts/libs/crypto/ECDSA256.sol index e3b729c3..88b36e84 100644 --- a/contracts/libs/crypto/ECDSA256.sol +++ b/contracts/libs/crypto/ECDSA256.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.21; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {EC256} from "./EC256.sol"; + /** * @notice Cryptography module * @@ -12,39 +14,13 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; * For more information, please refer to the OpenZeppelin documentation. */ library ECDSA256 { - /** - * @notice 256-bit curve parameters. - */ - struct Parameters { - uint256 a; - uint256 b; - uint256 gx; - uint256 gy; - uint256 p; - uint256 n; - uint256 lowSmax; - } - - // solhint-disable-next-line contract-name-capwords - struct _JPoint { - uint256 x; - uint256 y; - uint256 z; - } - - // solhint-disable-next-line contract-name-capwords - struct _Inputs { - uint256 r; - uint256 s; - uint256 x; - uint256 y; - } + using EC256 for *; error LengthIsNot64(); /** * @notice The function to verify the ECDSA signature - * @param curveParams_ the 256-bit curve parameters. `lowSmax` is `n / 2`. + * @param ec the 256-bit curve parameters. * @param hashedMessage_ the already hashed message to be verified. * @param signature_ the ECDSA signature. Equals to `bytes(r) + bytes(s)`. * @param pubKey_ the full public key of a signer. Equals to `bytes(x) + bytes(y)`. @@ -53,21 +29,20 @@ library ECDSA256 { * If your `s > n / 2`, change it to `s = n - s`. */ function verify( - Parameters memory curveParams_, + EC256.Curve memory ec, bytes32 hashedMessage_, bytes memory signature_, bytes memory pubKey_ ) internal view returns (bool) { unchecked { - _Inputs memory inputs_; + uint256 r_; + uint256 s_; + EC256.APoint memory p_; - (inputs_.r, inputs_.s) = _split(signature_); - (inputs_.x, inputs_.y) = _split(pubKey_); + (r_, s_) = _split(signature_); + (p_.x, p_.y) = _split(pubKey_); - if ( - !_isProperSignature(inputs_.r, inputs_.s, curveParams_.n, curveParams_.lowSmax) || - !_isOnCurve(inputs_.x, inputs_.y, curveParams_.a, curveParams_.b, curveParams_.p) - ) { + if (!_isProperSignature(ec, r_, s_) || !ec.isOnCurve(p_)) { return false; } @@ -75,46 +50,19 @@ library ECDSA256 { uint256 u2_; { - uint256 w_ = Math.invModPrime(inputs_.s, curveParams_.n); - u1_ = mulmod(uint256(hashedMessage_), w_, curveParams_.n); - u2_ = mulmod(inputs_.r, w_, curveParams_.n); + uint256 w_ = Math.invModPrime(s_, ec.n); + u1_ = mulmod(uint256(hashedMessage_), w_, ec.n); + u2_ = mulmod(r_, w_, ec.n); } - uint256 x_; - - { - _JPoint[16] memory points_ = _preComputeJacobianPoints( - inputs_.x, - inputs_.y, - curveParams_.gx, - curveParams_.gy, - curveParams_.p, - curveParams_.a - ); - - (x_, ) = _jMultShamir(points_, u1_, u2_, curveParams_.p, curveParams_.a); - } - - return x_ % curveParams_.n == inputs_.r; - } - } - - /** - * @dev Checks if (x, y) are valid coordinates of a point on the curve. - * In particular this function checks that x < P and y < P. - */ - function _isOnCurve( - uint256 x_, - uint256 y_, - uint256 a_, - uint256 b_, - uint256 p_ - ) private pure returns (bool result_) { - assembly ("memory-safe") { - let lhs_ := mulmod(y_, y_, p_) - let rhs_ := addmod(mulmod(addmod(mulmod(x_, x_, p_), a_, p_), x_, p_), b_, p_) + EC256.JPoint memory point_ = ec.jMultShamir2( + p_.toJacobian(), + ec.jbasepoint(), + u1_, + u2_ + ); - result_ := and(and(lt(x_, p_), lt(y_, p_)), eq(lhs_, rhs_)) // Should conform with the Weierstrass equation + return ec.toAffine(point_).x % ec.n == r_; } } @@ -123,253 +71,11 @@ library ECDSA256 { * In particular, this checks that `s` is in the "lower-range", making the signature non-malleable */ function _isProperSignature( + EC256.Curve memory ec, uint256 r_, - uint256 s_, - uint256 n_, - uint256 lowSmax_ + uint256 s_ ) private pure returns (bool) { - return r_ > 0 && r_ < n_ && s_ > 0 && s_ <= lowSmax_; - } - - /** - * @dev Reduce from jacobian to affine coordinates - * @param point_ - point with jacobian coordinate x, y and z - * @return ax_ - affine coordinate x - * @return ay_ - affine coordinate y - */ - function _affineFromJacobian( - _JPoint memory point_, - uint256 p_ - ) private view returns (uint256 ax_, uint256 ay_) { - if (point_.z == 0) return (0, 0); - - uint256 zInverse_ = Math.invModPrime(point_.z, p_); - - assembly ("memory-safe") { - let zzInverse_ := mulmod(zInverse_, zInverse_, p_) - - ax_ := mulmod(mload(point_), zzInverse_, p_) - ay_ := mulmod(mload(add(point_, 0x20)), mulmod(zzInverse_, zInverse_, p_), p_) - } - } - - /** - * @dev Point addition on the jacobian coordinates - * Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-1998-cmo-2 - */ - function _jAdd( - _JPoint memory point1_, - _JPoint memory point2_, - uint256 p_, - uint256 a_ - ) private pure returns (uint256 resX_, uint256 resY_, uint256 resZ_) { - assembly ("memory-safe") { - let zz1_ := mulmod(mload(add(point1_, 0x40)), mload(add(point1_, 0x40)), p_) - let s1_ := mulmod( - mload(add(point1_, 0x20)), - mulmod( - mulmod(mload(add(point2_, 0x40)), mload(add(point2_, 0x40)), p_), - mload(add(point2_, 0x40)), - p_ - ), - p_ - ) - let r_ := addmod( - mulmod(mload(add(point2_, 0x20)), mulmod(zz1_, mload(add(point1_, 0x40)), p_), p_), - sub(p_, s1_), - p_ - ) - let u1_ := mulmod( - mload(point1_), - mulmod(mload(add(point2_, 0x40)), mload(add(point2_, 0x40)), p_), - p_ - ) - let h_ := addmod(mulmod(mload(point2_), zz1_, p_), sub(p_, u1_), p_) - - // detect edge cases where inputs are identical - switch and(iszero(r_), iszero(h_)) - // case 0: points are different - case 0 { - let hh_ := mulmod(h_, h_, p_) - - resX_ := addmod( - addmod(mulmod(r_, r_, p_), sub(p_, mulmod(h_, hh_, p_)), p_), - sub(p_, mulmod(2, mulmod(u1_, hh_, p_), p_)), - p_ - ) - resY_ := addmod( - mulmod(r_, addmod(mulmod(u1_, hh_, p_), sub(p_, resX_), p_), p_), - sub(p_, mulmod(s1_, mulmod(h_, hh_, p_), p_)), - p_ - ) - resZ_ := mulmod( - h_, - mulmod(mload(add(point1_, 0x40)), mload(add(point2_, 0x40)), p_), - p_ - ) - } - // case 1: points are equal - case 1 { - let yy_ := mulmod(mload(add(point2_, 0x20)), mload(add(point2_, 0x20)), p_) - let zz_ := mulmod(mload(add(point2_, 0x40)), mload(add(point2_, 0x40)), p_) - let xx_ := mulmod(mload(point2_), mload(point2_), p_) - let m_ := addmod(mulmod(3, xx_, p_), mulmod(a_, mulmod(zz_, zz_, p_), p_), p_) - let s_ := mulmod(4, mulmod(mload(point2_), yy_, p_), p_) - - resX_ := addmod(mulmod(m_, m_, p_), sub(p_, mulmod(2, s_, p_)), p_) - - // cut the computation to avoid stack too deep - let rytmp1_ := sub(p_, mulmod(8, mulmod(yy_, yy_, p_), p_)) - let rytmp2_ := addmod(s_, sub(p_, resX_), p_) - resY_ := addmod(mulmod(m_, rytmp2_, p_), rytmp1_, p_) - - resZ_ := mulmod( - 2, - mulmod(mload(add(point2_, 0x20)), mload(add(point2_, 0x40)), p_), - p_ - ) - } - } - } - - /** - * @dev Point doubling on the jacobian coordinates - * Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-1998-cmo-2 - */ - function _jDouble( - uint256 x_, - uint256 y_, - uint256 z_, - uint256 p_, - uint256 a_ - ) private pure returns (uint256 resX_, uint256 resY_, uint256 resZ_) { - assembly ("memory-safe") { - let yy_ := mulmod(y_, y_, p_) - let zz_ := mulmod(z_, z_, p_) - let m_ := addmod( - mulmod(3, mulmod(x_, x_, p_), p_), - mulmod(a_, mulmod(zz_, zz_, p_), p_), - p_ - ) - let s_ := mulmod(4, mulmod(x_, yy_, p_), p_) - - resX_ := addmod(mulmod(m_, m_, p_), sub(p_, mulmod(2, s_, p_)), p_) - resY_ := addmod( - mulmod(m_, addmod(s_, sub(p_, resX_), p_), p_), - sub(p_, mulmod(8, mulmod(yy_, yy_, p_), p_)), - p_ - ) - resZ_ := mulmod(2, mulmod(y_, z_, p_), p_) - } - } - - /** - * @dev Compute G·u1 + P·u2 using the precomputed points for G and P (see {_preComputeJacobianPoints}). - * - * Uses Strauss Shamir trick for EC multiplication - * https://stackoverflow.com/questions/50993471/ec-scalar-multiplication-with-strauss-shamir-method - */ - function _jMultShamir( - _JPoint[16] memory points_, - uint256 u1_, - uint256 u2_, - uint256 p_, - uint256 a_ - ) private view returns (uint256 resX_, uint256 resY_) { - _JPoint memory point_; - - unchecked { - for (uint256 i = 0; i < 128; ++i) { - if (point_.z > 0) { - (point_.x, point_.y, point_.z) = _jDouble( - point_.x, - point_.y, - point_.z, - p_, - a_ - ); - (point_.x, point_.y, point_.z) = _jDouble( - point_.x, - point_.y, - point_.z, - p_, - a_ - ); - } - // Read 2 bits of u1, and 2 bits of u2. Combining the two gives the lookup index in the table. - uint256 pos_ = ((u1_ >> 252) & 0xc) | ((u2_ >> 254) & 0x3); - // Points that have z = 0 are points at infinity. They are the additive 0 of the group - // - if the lookup point is a 0, we can skip it - // - otherwise: - // - if the current point (x, y, z) is 0, we use the lookup point as our new value (0+P=P) - // - if the current point (x, y, z) is not 0, both points are valid and we can use `_jAdd` - if (points_[pos_].z != 0) { - if (point_.z == 0) { - (point_.x, point_.y, point_.z) = ( - points_[pos_].x, - points_[pos_].y, - points_[pos_].z - ); - } else { - (point_.x, point_.y, point_.z) = _jAdd(points_[pos_], point_, p_, a_); - } - } - u1_ <<= 2; - u2_ <<= 2; - } - } - return _affineFromJacobian(point_, p_); - } - - /** - * @dev Precompute a matrice of useful jacobian points associated with a given P. This can be seen as a 4x4 matrix - * that contains combination of P and G (generator) up to 3 times each - */ - function _preComputeJacobianPoints( - uint256 x_, - uint256 y_, - uint256 gx_, - uint256 gy_, - uint256 p_, - uint256 a_ - ) private pure returns (_JPoint[16] memory points_) { - points_[0x00] = _JPoint(0, 0, 0); - points_[0x01] = _JPoint(x_, y_, 1); - points_[0x04] = _JPoint(gx_, gy_, 1); - points_[0x02] = _jDoublePoint(points_[0x01], p_, a_); - points_[0x08] = _jDoublePoint(points_[0x04], p_, a_); - points_[0x03] = _jAddPoint(points_[0x01], points_[0x02], p_, a_); - points_[0x05] = _jAddPoint(points_[0x01], points_[0x04], p_, a_); - points_[0x06] = _jAddPoint(points_[0x02], points_[0x04], p_, a_); - points_[0x07] = _jAddPoint(points_[0x03], points_[0x04], p_, a_); - points_[0x09] = _jAddPoint(points_[0x01], points_[0x08], p_, a_); - points_[0x0a] = _jAddPoint(points_[0x02], points_[0x08], p_, a_); - points_[0x0b] = _jAddPoint(points_[0x03], points_[0x08], p_, a_); - points_[0x0c] = _jAddPoint(points_[0x04], points_[0x08], p_, a_); - points_[0x0d] = _jAddPoint(points_[0x01], points_[0x0c], p_, a_); - points_[0x0e] = _jAddPoint(points_[0x02], points_[0x0c], p_, a_); - points_[0x0f] = _jAddPoint(points_[0x03], points_[0x0c], p_, a_); - } - - function _jAddPoint( - _JPoint memory point1_, - _JPoint memory point2_, - uint256 p_, - uint256 a_ - ) private pure returns (_JPoint memory) { - (uint256 x_, uint256 y_, uint256 z_) = _jAdd(point1_, point2_, p_, a_); - - return _JPoint(x_, y_, z_); - } - - function _jDoublePoint( - _JPoint memory point_, - uint256 p_, - uint256 a_ - ) private pure returns (_JPoint memory) { - (uint256 x_, uint256 y_, uint256 z_) = _jDouble(point_.x, point_.y, point_.z, p_, a_); - - return _JPoint(x_, y_, z_); + return r_ > 0 && r_ < ec.n && s_ > 0 && s_ <= (ec.n >> 1); } /** diff --git a/contracts/libs/crypto/ECDSA384.sol b/contracts/libs/crypto/ECDSA384.sol index 4e492705..03d3b293 100644 --- a/contracts/libs/crypto/ECDSA384.sol +++ b/contracts/libs/crypto/ECDSA384.sol @@ -28,7 +28,6 @@ library ECDSA384 { bytes gy; bytes p; bytes n; - bytes lowSmax; } // solhint-disable-next-line contract-name-capwords @@ -39,7 +38,6 @@ library ECDSA384 { uint512 gy; uint512 p; uint512 n; - uint512 lowSmax; } // solhint-disable-next-line contract-name-capwords @@ -52,7 +50,7 @@ library ECDSA384 { /** * @notice The function to verify the ECDSA signature - * @param curveParams_ the 384-bit curve parameters. `lowSmax` is `n / 2`. + * @param curveParams_ the 384-bit curve parameters. * @param hashedMessage_ the already hashed message to be verified. * @param signature_ the ECDSA signature. Equals to `bytes(r) + bytes(s)`. * @param pubKey_ the full public key of a signer. Equals to `bytes(x) + bytes(y)`. @@ -78,8 +76,7 @@ library ECDSA384 { gx: U512.fromBytes(curveParams_.gx), gy: U512.fromBytes(curveParams_.gy), p: U512.fromBytes(curveParams_.p), - n: U512.fromBytes(curveParams_.n), - lowSmax: U512.fromBytes(curveParams_.lowSmax) + n: U512.fromBytes(curveParams_.n) }); call512 call_ = U512.initCall(); @@ -89,7 +86,7 @@ library ECDSA384 { U512.eqU256(inputs_.r, 0) || U512.cmp(inputs_.r, params_.n) >= 0 || U512.eqU256(inputs_.s, 0) || - U512.cmp(inputs_.s, params_.lowSmax) > 0 + U512.cmp(inputs_.s, U512.shr(params_.n, 1)) > 0 ) { return false; } diff --git a/contracts/libs/crypto/ECDSA512.sol b/contracts/libs/crypto/ECDSA512.sol index 3f62c359..827783b7 100644 --- a/contracts/libs/crypto/ECDSA512.sol +++ b/contracts/libs/crypto/ECDSA512.sol @@ -28,7 +28,6 @@ library ECDSA512 { bytes gy; bytes p; bytes n; - bytes lowSmax; } // solhint-disable-next-line contract-name-capwords @@ -39,7 +38,6 @@ library ECDSA512 { uint512 gy; uint512 p; uint512 n; - uint512 lowSmax; } // solhint-disable-next-line contract-name-capwords @@ -52,7 +50,7 @@ library ECDSA512 { /** * @notice The function to verify the ECDSA signature - * @param curveParams_ the 512-bit curve parameters. `lowSmax` is `n / 2`. + * @param curveParams_ the 512-bit curve parameters. * @param hashedMessage_ the already hashed message to be verified. * @param signature_ the ECDSA signature. Equals to `bytes(r) + bytes(s)`. * @param pubKey_ the full public key of a signer. Equals to `bytes(x) + bytes(y)`. @@ -78,8 +76,7 @@ library ECDSA512 { gx: U512.fromBytes(curveParams_.gx), gy: U512.fromBytes(curveParams_.gy), p: U512.fromBytes(curveParams_.p), - n: U512.fromBytes(curveParams_.n), - lowSmax: U512.fromBytes(curveParams_.lowSmax) + n: U512.fromBytes(curveParams_.n) }); call512 call_ = U512.initCall(); @@ -89,7 +86,7 @@ library ECDSA512 { U512.eqU256(inputs_.r, 0) || U512.cmp(inputs_.r, params_.n) >= 0 || U512.eqU256(inputs_.s, 0) || - U512.cmp(inputs_.s, params_.lowSmax) > 0 + U512.cmp(inputs_.s, U512.shr(params_.n, 1)) > 0 ) { return false; } diff --git a/contracts/libs/crypto/Schnorr256.sol b/contracts/libs/crypto/Schnorr256.sol new file mode 100644 index 00000000..a3eebff8 --- /dev/null +++ b/contracts/libs/crypto/Schnorr256.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {EC256} from "./EC256.sol"; +import {MemoryUtils} from "../utils/MemoryUtils.sol"; + +/** + * @notice Cryptography module + * + * This library provides functionality for Schnorr signature verification over any 256-bit curve. + */ +library Schnorr256 { + using MemoryUtils for *; + using EC256 for *; + + error LengthIsNot64(); + error LengthIsNot96(); + + /** + * @notice The function to verify the Schnorr signature + * @param ec the 256-bit curve parameters. + * @param hashedMessage_ the already hashed message to be verified. + * @param signature_ the Schnorr signature. Equals to `bytes(R) + bytes(e)`. + * @param pubKey_ the full public key of a signer. Equals to `bytes(x) + bytes(y)`. + */ + function verify( + EC256.Curve memory ec, + bytes32 hashedMessage_, + bytes memory signature_, + bytes memory pubKey_ + ) internal view returns (bool) { + (EC256.APoint memory r_, uint256 e_) = _parseSignature(signature_); + EC256.APoint memory p_ = _parsePubKey(pubKey_); + + if (!ec.isOnCurve(r_) || !ec.isOnCurve(p_) || !ec.isValidScalar(e_)) { + return false; + } + + EC256.JPoint memory lhs_ = ec.jMultShamir(ec.jbasepoint(), e_); + + uint256 c_ = ec.toScalar( + uint256(keccak256(abi.encodePacked(ec.gx, ec.gy, r_.x, r_.y, hashedMessage_))) + ); + + EC256.JPoint memory rhs_ = ec.jMultShamir(p_.toJacobian(), c_); + rhs_ = ec.jAddPoint(rhs_, r_.toJacobian()); + + return ec.jEqual(lhs_, rhs_); + } + + /** + * @dev Helper function for splitting 96-byte signature into R (affine point) and e (scalar) components. + */ + function _parseSignature( + bytes memory signature_ + ) private pure returns (EC256.APoint memory r_, uint256 e_) { + if (signature_.length != 96) { + revert LengthIsNot96(); + } + + (r_.x, r_.y, e_) = abi.decode(signature_, (uint256, uint256, uint256)); + } + + /** + * @dev Helper function for converting 64-byte pub key into affine point. + */ + function _parsePubKey(bytes memory pubKey_) private pure returns (EC256.APoint memory p_) { + if (pubKey_.length != 64) { + revert LengthIsNot64(); + } + + (p_.x, p_.y) = abi.decode(pubKey_, (uint256, uint256)); + } +} diff --git a/contracts/mock/libs/crypto/EC256Mock.sol b/contracts/mock/libs/crypto/EC256Mock.sol new file mode 100644 index 00000000..9c7c8f3d --- /dev/null +++ b/contracts/mock/libs/crypto/EC256Mock.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {EC256} from "../../../libs/crypto/EC256.sol"; + +contract EC256Mock { + using EC256 for *; + + EC256.Curve public secp256k1CurveParams = + EC256.Curve({ + a: 0x0000000000000000000000000000000000000000000000000000000000000000, + b: 0x0000000000000000000000000000000000000000000000000000000000000007, + gx: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, + gy: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8, + p: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f, + n: 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + }); + + function affineInfinity() external view returns (EC256.APoint memory) { + return secp256k1CurveParams.toAffine(EC256.jinfinity()); + } + + function basepoint() external view returns (EC256.APoint memory) { + return secp256k1CurveParams.basepoint(); + } +} diff --git a/contracts/mock/libs/crypto/ECDSA256Mock.sol b/contracts/mock/libs/crypto/ECDSA256Mock.sol index 3b0d0519..c34bdde2 100644 --- a/contracts/mock/libs/crypto/ECDSA256Mock.sol +++ b/contracts/mock/libs/crypto/ECDSA256Mock.sol @@ -2,30 +2,29 @@ pragma solidity ^0.8.21; import {ECDSA256} from "../../../libs/crypto/ECDSA256.sol"; +import {EC256} from "../../../libs/crypto/EC256.sol"; contract ECDSA256Mock { using ECDSA256 for *; - ECDSA256.Parameters private _secp256r1CurveParams = - ECDSA256.Parameters({ + EC256.Curve private _secp256r1CurveParams = + EC256.Curve({ a: 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC, b: 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B, gx: 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296, gy: 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5, p: 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF, - n: 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551, - lowSmax: 0x7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8 + n: 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 }); - ECDSA256.Parameters private _brainpoolP256r1CurveParams = - ECDSA256.Parameters({ + EC256.Curve private _brainpoolP256r1CurveParams = + EC256.Curve({ a: 0x7D5A0975FC2C3057EEF67530417AFFE7FB8055C126DC5C6CE94A4B44F330B5D9, b: 0x26DC5C6CE94A4B44F330B5D9BBD77CBF958416295CF7E1CE6BCCDC18FF8C07B6, gx: 0x8BD2AEB9CB7E57CB2C4B482FFC81B7AFB9DE27E1E3BD23C23A4453BD9ACE3262, gy: 0x547EF835C3DAC4FD97F8461A14611DC9C27745132DED8E545C1D54C72F046997, p: 0xA9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5377, - n: 0xA9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7, - lowSmax: 0x54fdabedd0f754de1f3305484ec1c6b9371dfb11ea9310141009a40e8fb729bb + n: 0xA9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7 }); function verifySECP256r1( diff --git a/contracts/mock/libs/crypto/ECDSA384Mock.sol b/contracts/mock/libs/crypto/ECDSA384Mock.sol index b29c035f..c9e7cceb 100644 --- a/contracts/mock/libs/crypto/ECDSA384Mock.sol +++ b/contracts/mock/libs/crypto/ECDSA384Mock.sol @@ -17,8 +17,7 @@ contract ECDSA384Mock { gx: hex"aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7", gy: hex"3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f", p: hex"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff", - n: hex"ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973", - lowSmax: hex"7fffffffffffffffffffffffffffffffffffffffffffffffe3b1a6c0fa1b96efac0d06d9245853bd76760cb5666294b9" + n: hex"ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973" }); ECDSA384.Parameters private _brainpoolP384r1CurveParams = @@ -28,8 +27,7 @@ contract ECDSA384Mock { gx: hex"1d1c64f068cf45ffa2a63a81b7c13f6b8847a3e77ef14fe3db7fcafe0cbd10e8e826e03436d646aaef87b2e247d4af1e", gy: hex"8abe1d7520f9c2a45cb1eb8e95cfd55262b70b29feec5864e19c054ff99129280e4646217791811142820341263c5315", p: hex"8cb91e82a3386d280f5d6f7e50e641df152f7109ed5456b412b1da197fb71123acd3a729901d1a71874700133107ec53", - n: hex"8cb91e82a3386d280f5d6f7e50e641df152f7109ed5456b31f166e6cac0425a7cf3ab6af6b7fc3103b883202e9046565", - lowSmax: hex"465c8f41519c369407aeb7bf287320ef8a97b884f6aa2b598f8b3736560212d3e79d5b57b5bfe1881dc41901748232b2" + n: hex"8cb91e82a3386d280f5d6f7e50e641df152f7109ed5456b31f166e6cac0425a7cf3ab6af6b7fc3103b883202e9046565" }); function verifySECP384r1( diff --git a/contracts/mock/libs/crypto/ECDSA512Mock.sol b/contracts/mock/libs/crypto/ECDSA512Mock.sol index c8ca9ec7..97e58189 100644 --- a/contracts/mock/libs/crypto/ECDSA512Mock.sol +++ b/contracts/mock/libs/crypto/ECDSA512Mock.sol @@ -13,8 +13,7 @@ contract ECDSA512Mock { gx: hex"81aee4bdd82ed9645a21322e9c4c6a9385ed9f70b5d916c1b43b62eef4d0098eff3b1f78e2d0d48d50d1687b93b97d5f7c6d5047406a5e688b352209bcb9f822", gy: hex"7dde385d566332ecc0eabfa9cf7822fdf209f70024a57b1aa000c55b881f8111b2dcde494a5f485e5bca4bd88a2763aed1ca2b2fa8f0540678cd1e0f3ad80892", p: hex"aadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca703308717d4d9b009bc66842aecda12ae6a380e62881ff2f2d82c68528aa6056583a48f3", - n: hex"aadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca70330870553e5c414ca92619418661197fac10471db1d381085ddaddb58796829ca90069", - lowSmax: hex"556ecedc6df4e2459fea735719e4fe03e59846d9d9e4e9076b31ce65381984382a9f2e20a654930ca0c3308cbfd608238ed8e9c0842eed6edac3cb414e548034" + n: hex"aadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca70330870553e5c414ca92619418661197fac10471db1d381085ddaddb58796829ca90069" }); function verifyBrainpoolP512r1WithoutHashing( diff --git a/contracts/mock/libs/crypto/Schnorr256Mock.sol b/contracts/mock/libs/crypto/Schnorr256Mock.sol new file mode 100644 index 00000000..2b8e2426 --- /dev/null +++ b/contracts/mock/libs/crypto/Schnorr256Mock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {Schnorr256} from "../../../libs/crypto/Schnorr256.sol"; +import {EC256} from "../../../libs/crypto/EC256.sol"; + +contract Schnorr256Mock { + EC256.Curve private _secp256k1CurveParams = + EC256.Curve({ + a: 0x0000000000000000000000000000000000000000000000000000000000000000, + b: 0x0000000000000000000000000000000000000000000000000000000000000007, + gx: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798, + gy: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8, + p: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f, + n: 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + }); + + function verifySECP256k1( + bytes32 hashedMessage_, + bytes memory signature_, + bytes memory pubKey_ + ) external view returns (bool isVerified_) { + return Schnorr256.verify(_secp256k1CurveParams, hashedMessage_, signature_, pubKey_); + } +} diff --git a/package-lock.json b/package-lock.json index 686e27c3..ecd44310 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solarity/solidity-lib", - "version": "3.0.2", + "version": "3.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solarity/solidity-lib", - "version": "3.0.2", + "version": "3.1.0", "license": "MIT", "dependencies": { "@openzeppelin/contracts": "5.2.0", @@ -19,6 +19,7 @@ "devDependencies": { "@iden3/js-crypto": "^1.1.0", "@iden3/js-merkletree": "^1.3.1", + "@noble/curves": "^1.9.0", "@nomicfoundation/hardhat-chai-matchers": "^2.0.8", "@nomicfoundation/hardhat-ethers": "^3.0.8", "@nomicfoundation/hardhat-network-helpers": "^1.0.12", @@ -1172,13 +1173,29 @@ } }, "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz", + "integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==", "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.3.2" + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1658,6 +1675,19 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@scure/bip39": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", @@ -3837,6 +3867,19 @@ "node": ">=14.0.0" } }, + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/ethers/node_modules/@types/node": { "version": "22.7.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", @@ -8314,6 +8357,19 @@ "dev": true, "license": "MIT" }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/viem/node_modules/ws": { "version": "8.13.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", diff --git a/package.json b/package.json index 30844e7b..da12a42d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solarity/solidity-lib", - "version": "3.0.2", + "version": "3.1.0", "license": "MIT", "author": "Distributed Lab", "readme": "README.md", @@ -46,6 +46,7 @@ "devDependencies": { "@iden3/js-crypto": "^1.1.0", "@iden3/js-merkletree": "^1.3.1", + "@noble/curves": "^1.9.0", "@nomicfoundation/hardhat-chai-matchers": "^2.0.8", "@nomicfoundation/hardhat-ethers": "^3.0.8", "@nomicfoundation/hardhat-network-helpers": "^1.0.12", diff --git a/test/libs/crypto/EC256.test.ts b/test/libs/crypto/EC256.test.ts new file mode 100644 index 00000000..28977e88 --- /dev/null +++ b/test/libs/crypto/EC256.test.ts @@ -0,0 +1,35 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { Reverter } from "@/test/helpers/reverter"; + +import { EC256Mock } from "@ethers-v6"; + +describe("EC256", () => { + const reverter = new Reverter(); + + let ec256: EC256Mock; + + before(async () => { + const EC256Mock = await ethers.getContractFactory("EC256Mock"); + + ec256 = await EC256Mock.deploy(); + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe("toAffine", () => { + it("should return zero affine", async () => { + expect(await ec256.affineInfinity()).to.deep.equal([0, 0]); + }); + }); + + describe("basepoint", () => { + it("should return correct basepoint", async () => { + const secp256k1 = await ec256.secp256k1CurveParams(); + + expect(await ec256.basepoint()).to.deep.equal([secp256k1.gx, secp256k1.gy]); + }); + }); +}); diff --git a/test/libs/crypto/Schnorr256.test.ts b/test/libs/crypto/Schnorr256.test.ts new file mode 100644 index 00000000..2dc7ef5b --- /dev/null +++ b/test/libs/crypto/Schnorr256.test.ts @@ -0,0 +1,108 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { Reverter } from "@/test/helpers/reverter"; + +import { Schnorr256Mock } from "@ethers-v6"; + +import { secp256k1 } from "@noble/curves/secp256k1"; +import { bytesToNumberBE } from "@noble/curves/abstract/utils"; +import { AffinePoint } from "@noble/curves/abstract/curve"; + +describe("Schnorr256", () => { + const schnorrKeyPair = () => { + const privKey = bytesToNumberBE(secp256k1.utils.randomPrivateKey()); + const pubKey = secp256k1.ProjectivePoint.BASE.multiply(privKey).toAffine(); + + return { privKey, pubKey }; + }; + + const serializePoint = (p: AffinePoint) => { + return ethers.solidityPacked(["uint256", "uint256"], [p.x, p.y]); + }; + + const schnorrSign = function (hashedMessage: string, privKey: bigint) { + const randomness = schnorrKeyPair(); + const k = randomness.privKey; + const R = randomness.pubKey; + + const c = BigInt( + ethers.solidityPackedKeccak256( + ["uint256", "uint256", "uint256", "uint256", "bytes32"], + [secp256k1.CURVE.Gx, secp256k1.CURVE.Gy, R.x, R.y, hashedMessage], + ), + ); + const e = (k + c * privKey) % secp256k1.CURVE.n; + + return ethers.solidityPacked(["uint256", "uint256", "uint256"], [R.x, R.y, e]); + }; + + const prepareParameters = function (message: string) { + const { privKey, pubKey } = schnorrKeyPair(); + + const hashedMessage = ethers.keccak256(message); + + const signature = schnorrSign(hashedMessage, privKey); + + return { + hashedMessage, + signature, + pubKey: serializePoint(pubKey), + }; + }; + + const reverter = new Reverter(); + + let schnorr: Schnorr256Mock; + + before(async () => { + const Schnorr256 = await ethers.getContractFactory("Schnorr256Mock"); + + schnorr = await Schnorr256.deploy(); + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe("verify", () => { + it("should verify the signature", async () => { + const { hashedMessage, signature, pubKey } = prepareParameters("0x1337"); + + expect(await schnorr.verifySECP256k1(hashedMessage, signature, pubKey)).to.be.true; + }); + + it("should revert if signature or public key has an invalid length", async () => { + const { hashedMessage, signature, pubKey } = prepareParameters("0x1337"); + + const wrongSig = "0x0101"; + + await expect(schnorr.verifySECP256k1(hashedMessage, wrongSig, pubKey)).to.be.revertedWithCustomError( + schnorr, + "LengthIsNot96", + ); + + const wrongPubKey = "0x0101"; + + await expect(schnorr.verifySECP256k1(hashedMessage, signature, wrongPubKey)).to.be.revertedWithCustomError( + schnorr, + "LengthIsNot64", + ); + }); + + it("should not verify if signature or public key is invalid", async () => { + const { hashedMessage, signature, pubKey } = prepareParameters("0x1337"); + + const r = signature.slice(2, 130); + const e = signature.slice(130); + + const wrongSig1 = "0x" + ethers.toBeHex(0, 0x40).slice(2) + e; + expect(await schnorr.verifySECP256k1(hashedMessage, wrongSig1, pubKey)).to.be.false; + + const wrongSig2 = "0x" + r + ethers.toBeHex((1n << 256n) - 1n, 0x20).slice(2); + expect(await schnorr.verifySECP256k1(hashedMessage, wrongSig2, pubKey)).to.be.false; + + const wrongPubKey = "0x" + ethers.toBeHex(0, 0x40).slice(2); + expect(await schnorr.verifySECP256k1(hashedMessage, signature, wrongPubKey)).to.be.false; + }); + }); +});