diff --git a/contracts/libs/crypto/RSASSAPSS.sol b/contracts/libs/crypto/RSASSAPSS.sol new file mode 100644 index 00000000..4a64076d --- /dev/null +++ b/contracts/libs/crypto/RSASSAPSS.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/** + * @notice Cryptography module + * + * This library provides functionality to verify RSASSA-PSS signatures with MGF1 mask generation function. + * + * Users may provide custom hash functions via `Parameters` struct. However, the usage of `sha256` is recommended. + * + * Learn more about the algorithm [here](https://datatracker.ietf.org/doc/html/rfc3447#section-8.1). + */ +library RSASSAPSS { + struct Parameters { + uint256 hashLength; + uint256 saltLength; + function(bytes memory) internal pure returns (bytes memory) hasher; + } + + /** + * @notice Same as `verify` but with `sha256` hash function preconfiguration. + */ + function verifySha256( + bytes memory message_, + bytes memory s_, + bytes memory e_, + bytes memory n_ + ) internal view returns (bool) { + unchecked { + Parameters memory params_ = Parameters({ + hashLength: 32, + saltLength: 32, + hasher: _sha256 + }); + + return verify(params_, message_, s_, e_, n_); + } + } + + /** + * @notice Verifies RSAPSS-SSA signature with custom parameters. + * @param params_ The parameters to specify the hash length, salt length, and hash function of choice. + * @param message_ The arbitrary message to be verified. + * @param s_ The "encrypted" signature + * @param e_ The public key exponent. `65537` is a recommended value. + * @param n_ The modulus of a public key. + */ + function verify( + Parameters memory params_, + bytes memory message_, + bytes memory s_, + bytes memory e_, + bytes memory n_ + ) internal view returns (bool) { + unchecked { + if (s_.length == 0 || e_.length == 0 || n_.length == 0) { + return false; + } + + bytes memory decipher_ = _rsa(s_, e_, n_); + + return _pss(message_, decipher_, params_); + } + } + + /** + * @notice Calculates RSA via modexp (0x05) precompile. + */ + function _rsa( + bytes memory s_, + bytes memory e_, + bytes memory n_ + ) internal view returns (bytes memory decipher_) { + unchecked { + bytes memory input_ = abi.encodePacked(s_.length, e_.length, n_.length, s_, e_, n_); + + decipher_ = new bytes(n_.length); + + assembly { + pop( + staticcall( + sub(gas(), 2000), // gas buffer + 5, + add(input_, 0x20), + mload(input_), + add(decipher_, 0x20), + mload(n_) + ) + ) + } + } + } + + /** + * @notice Checks the PSS encoding. + */ + function _pss( + bytes memory message_, + bytes memory signature_, + Parameters memory params_ + ) private pure returns (bool) { + unchecked { + uint256 hashLength_ = params_.hashLength; + uint256 saltLength_ = params_.saltLength; + uint256 sigBytes_ = signature_.length; + uint256 sigBits_ = (sigBytes_ * 8 - 1) & 7; + + if (message_.length > 2 ** 61 - 1) { + return false; + } + + bytes memory messageHash_ = params_.hasher(message_); + + if (sigBytes_ < hashLength_ + saltLength_ + 2) { + return false; + } + + if (signature_[sigBytes_ - 1] != hex"BC") { + return false; + } + + bytes memory db_ = new bytes(sigBytes_ - hashLength_ - 1); + bytes memory h_ = new bytes(hashLength_); + + for (uint256 i = 0; i < db_.length; ++i) { + db_[i] = signature_[i]; + } + + for (uint256 i = 0; i < hashLength_; ++i) { + h_[i] = signature_[i + db_.length]; + } + + if (uint8(db_[0] & bytes1(uint8(((0xFF << (sigBits_)))))) == 1) { + return false; + } + + bytes memory dbMask_ = _mgf(params_, h_, db_.length); + + for (uint256 i = 0; i < db_.length; ++i) { + db_[i] ^= dbMask_[i]; + } + + if (sigBits_ > 0) { + db_[0] &= bytes1(uint8(0xFF >> (8 - sigBits_))); + } + + uint256 zeroBytes_; + + for ( + zeroBytes_ = 0; + db_[zeroBytes_] == 0 && zeroBytes_ < (db_.length - 1); + ++zeroBytes_ + ) {} + + if (db_[zeroBytes_++] != hex"01") { + return false; + } + + bytes memory salt_ = new bytes(saltLength_); + + for (uint256 i = 0; i < salt_.length; ++i) { + salt_[i] = db_[db_.length - salt_.length + i]; + } + + bytes memory hh_ = params_.hasher( + abi.encodePacked(hex"0000000000000000", messageHash_, salt_) + ); + + /// check bytes equality + if (keccak256(h_) != keccak256(hh_)) { + return false; + } + + return true; + } + } + + /** + * @notice MGF1 mask generation function + */ + function _mgf( + Parameters memory params_, + bytes memory message_, + uint256 maskLen_ + ) private pure returns (bytes memory res_) { + unchecked { + uint256 hashLength_ = params_.hashLength; + + bytes memory cnt_ = new bytes(4); + + require(maskLen_ <= (2 ** 32) * hashLength_, "RSASSAPSS: mask too long"); + + for (uint256 i = 0; i < (maskLen_ + hashLength_ - 1) / hashLength_; ++i) { + cnt_[0] = bytes1(uint8((i >> 24) & 255)); + cnt_[1] = bytes1(uint8((i >> 16) & 255)); + cnt_[2] = bytes1(uint8((i >> 8) & 255)); + cnt_[3] = bytes1(uint8(i & 255)); + + bytes memory hashedResInter_ = params_.hasher(abi.encodePacked(message_, cnt_)); + + res_ = abi.encodePacked(res_, hashedResInter_); + } + + assembly { + mstore(res_, maskLen_) + } + } + } + + /** + * @notice Utility `sha256` wrapper. + */ + function _sha256(bytes memory data) private pure returns (bytes memory) { + unchecked { + return abi.encodePacked(sha256(data)); + } + } +} diff --git a/contracts/mock/libs/crypto/RSASSAPSSMock.sol b/contracts/mock/libs/crypto/RSASSAPSSMock.sol new file mode 100644 index 00000000..7b59116e --- /dev/null +++ b/contracts/mock/libs/crypto/RSASSAPSSMock.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {RSASSAPSS} from "../../../libs/crypto/RSASSAPSS.sol"; + +contract RSASSAPSSMock { + using RSASSAPSS for *; + + function verifySha256( + bytes calldata message_, + bytes calldata s_, + bytes calldata e_, + bytes calldata n_ + ) external view returns (bool) { + return message_.verifySha256(s_, e_, n_); + } +} diff --git a/test/libs/crypto/RSASSAPSS.test.ts b/test/libs/crypto/RSASSAPSS.test.ts new file mode 100644 index 00000000..c11384c3 --- /dev/null +++ b/test/libs/crypto/RSASSAPSS.test.ts @@ -0,0 +1,46 @@ +import { ethers } from "hardhat"; +import { expect } from "chai"; +import { Reverter } from "@/test/helpers/reverter"; + +import { RSASSAPSSMock } from "@ethers-v6"; + +describe("RSASSAPSS", () => { + const reverter = new Reverter(); + + let rsassapss: RSASSAPSSMock; + + before(async () => { + const RSASSAPSSMock = await ethers.getContractFactory("RSASSAPSSMock"); + + rsassapss = await RSASSAPSSMock.deploy(); + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe("SHA256", () => { + it("should verify signature", async () => { + const message = + "0x308203c3a003020102020874442b6b708ef7a2304106092a864886f70d01010a3034a00f300d06096086480165030402010500a11c301a06092a864886f70d010108300d06096086480165030402010500a203020120302f310b3009060355040613025048310c300a060355040a0c034446413112301006035504030c09435343413031303037301e170d3232313231323136303030305a170d3333303430313135353935395a302d310b3009060355040613025048310c300a060355040a13034446413110300e060355040313074453303131313330820122300d06092a864886f70d01010105000382010f003082010a0282010100ab630b320a41ecf8886a904ab50fabcfad658f5af8a9f8aaecefe0dc5e2ea99eba3deccba3f58885f8574fe0ad5c889763afc2b68e66b5928403d508724ad1e7fd05c573c053e04660fd31128cff2e2f574ec92430202f5dafa6df66b46fb16ece1372424d3aa3b975428c59f18fe1f32e6c328b64f58f95e05684dfff2d21a85cb73bcb32ac172c8f782fa2ea942118379833bec37cab64de493ddae79014ed0e6fcaa2ca4cdc3bdb0442ba550cde8355194c3c3934b2d8bfa513fcf5788c0569e0527cd20daa5e8e114204661a3d1f21650d01703e7a112602cf8fbefbc329afc18d3d49a68b60e5c89c5152ad6e7f0480b0e4157b26640c569ae477e04f190203010001a38201c7308201c3300e0603551d0f0101ff040403020780301d0603551d0e04160414363257ff5b20debaa6e26c257d3ebb4fa1e7bcf5305e0603551d23045730558014a1436db84f1c134e49b387da56cee801102d4f73a133a431302f310b3009060355040613025048310c300a060355040a0c034446413112301006035504030c0943534341303130303782086b8a5f2f46fa934b302b0603551d1004243022800f32303232313231323136303030305a810f32303233303430313135353935395a30390603551d1104323030811c70617373706f72742e6469726563746f72406466612e676f762e7068a410300e310c300a06035504070c0350484c30390603551d1204323030811c70617373706f72742e6469726563746f72406466612e676f762e7068a410300e310c300a06035504070c0350484c306d0603551d1f046630643030a02ea02c862a68747470733a2f2f706b64646f776e6c6f6164312e6963616f2e696e742f43524c732f50484c2e63726c3030a02ea02c862a68747470733a2f2f706b64646f776e6c6f6164322e6963616f2e696e742f43524c732f50484c2e63726c3020060767810801010602041530131301501302504f130250441302505213025053"; + const s = + "0x875ef62f6832599f41b50ca51a478c92ff47b61f2090157f64b425b1e1ad5612e6abb7d5808d9be5f0eaaa16d2b516ef161534c78d542ffd659107535c2bab643163fb9af27a50389792508d1cdbda347a103404c5e08d2d97c7935994631d42fe7e0caa892dc3ec39d3ac94dbccb3cd0870b21b9c836feed5bc32e9ec6830392bdade1fc9b5280fbaa2ceaa78d9524af3d015cbaf07eebc84a9caec81a4407452573a101b79772056193d207a8398690ed0dd0cc5a6410fd844d313c50934d6e1d556f8e7b39b12525f3cd766c9342fbd892e40408b0c232d888da11fc64d0f09db70971d395d7a1d2aacfe9da78e3c46ce43b3ce5b9fc1e6a90c065cdafa2e8a117d63c00cf9f54e3a3313789f03dd7efc76641c2cf5068ca4512c82fa6c62f6bd36b12523dc46f444b8312d2f6e6ec22cd10eddb19220d9b8ba4cc442dd836335482c6309d56e87492d2fdaefdb7b5ede566ed43eb87955451225846ce2535b803a9ca79034cc3aa41307cc57f0962cb8b2c3b99a5c87150387f7d8de6a18f6a838404c4aa5bb279378fb285c096d4c2664c700ac4e3c0cb44f920928e764dd4b10f22d3cb5bdfac78066b1b0a5ae75528e447b262510d41150a94ab0f645cc61ae99a3719bd29cf3901dde6de7cc162051f34c642a0f7854ef00d4143d755ad72bc71371c3a8dedb94118272f37f853bd171743b0c7a9a8cb96095476c9c"; + const e = ethers.toBeHex(65537n); + const n = + "0xd24081be6cc14fe3fb4b35ab6df1a7f28f373017ef15a26b67ff2dd04773a3ef8942b7ba5f2f91aea469fe757e2e3362a907610b441f3610f528b1f39739a132c4bebc26c37d25b6d12481336fecddfc6bdcd011be4f2912ff0663cb70d9938280813dd3f32f2e6fef184881f784bbd2fd2d165b169d8594c45d832dbaebfdcb532d6542b57413825df5164b577dcdc248dfc4a8b071eb0bef021128e9172b77a18a5a6b00ebc0e07af0a9df6592684805a4ba0db00dddaabf793641ec0da51aecc6160acd56a0194d1161271b8feaaf0ae851ae65f1464c79607bbb237de3dbd0a299e9cda846c362976108d10555b57866a56c87c6a5d92d1888d6260fa90459afb14688b8a53921d2d477d1677518956412e01eb2592b27ad62a3d3a50777c4bee3f348b4788bb7bbd38d6bd902968a7b3640d75f98b78824ee9462e1b2d405b8c1ce7d7dbef479c2979553790b7d7fa8a6f05dad4cf95b92a218b410eb5b9df712d099b6952a07f122d21e95b934e9a765e758397b191fd01c0c4ae669039a0d7003308ab78d03809752bb7b676c3f3bbd9ac4b8cf0162efb50e01c52e3a97fed31d1e73f12b3da7b4df87fd7e93f70f92dc154d0bcef5a39c9631b2b50c7c7b91f4a63d4e3d487c37fc63bbdf87b71cad5581409661d15f77ff738a4d53d23424d999490607ebfd8293b2fb5c269cd8a19477ca88f6f7cb1abe00fb16c9"; + + expect(await rsassapss.verifySha256(message, s, e, n)).to.be.true; + }); + + it("should not verify invalid signature", async () => { + const message = "0x0123456789"; + const s = + "0x875ef62f6832599f41b50ca51a478c92ff47b61f2090157f64b425b1e1ad5612e6abb7d5808d9be5f0eaaa16d2b516ef161534c78d542ffd659107535c2bab643163fb9af27a50389792508d1cdbda347a103404c5e08d2d97c7935994631d42fe7e0caa892dc3ec39d3ac94dbccb3cd0870b21b9c836feed5bc32e9ec6830392bdade1fc9b5280fbaa2ceaa78d9524af3d015cbaf07eebc84a9caec81a4407452573a101b79772056193d207a8398690ed0dd0cc5a6410fd844d313c50934d6e1d556f8e7b39b12525f3cd766c9342fbd892e40408b0c232d888da11fc64d0f09db70971d395d7a1d2aacfe9da78e3c46ce43b3ce5b9fc1e6a90c065cdafa2e8a117d63c00cf9f54e3a3313789f03dd7efc76641c2cf5068ca4512c82fa6c62f6bd36b12523dc46f444b8312d2f6e6ec22cd10eddb19220d9b8ba4cc442dd836335482c6309d56e87492d2fdaefdb7b5ede566ed43eb87955451225846ce2535b803a9ca79034cc3aa41307cc57f0962cb8b2c3b99a5c87150387f7d8de6a18f6a838404c4aa5bb279378fb285c096d4c2664c700ac4e3c0cb44f920928e764dd4b10f22d3cb5bdfac78066b1b0a5ae75528e447b262510d41150a94ab0f645cc61ae99a3719bd29cf3901dde6de7cc162051f34c642a0f7854ef00d4143d755ad72bc71371c3a8dedb94118272f37f853bd171743b0c7a9a8cb96095476c9c"; + const e = ethers.toBeHex(65537n); + const n = + "0xd24081be6cc14fe3fb4b35ab6df1a7f28f373017ef15a26b67ff2dd04773a3ef8942b7ba5f2f91aea469fe757e2e3362a907610b441f3610f528b1f39739a132c4bebc26c37d25b6d12481336fecddfc6bdcd011be4f2912ff0663cb70d9938280813dd3f32f2e6fef184881f784bbd2fd2d165b169d8594c45d832dbaebfdcb532d6542b57413825df5164b577dcdc248dfc4a8b071eb0bef021128e9172b77a18a5a6b00ebc0e07af0a9df6592684805a4ba0db00dddaabf793641ec0da51aecc6160acd56a0194d1161271b8feaaf0ae851ae65f1464c79607bbb237de3dbd0a299e9cda846c362976108d10555b57866a56c87c6a5d92d1888d6260fa90459afb14688b8a53921d2d477d1677518956412e01eb2592b27ad62a3d3a50777c4bee3f348b4788bb7bbd38d6bd902968a7b3640d75f98b78824ee9462e1b2d405b8c1ce7d7dbef479c2979553790b7d7fa8a6f05dad4cf95b92a218b410eb5b9df712d099b6952a07f122d21e95b934e9a765e758397b191fd01c0c4ae669039a0d7003308ab78d03809752bb7b676c3f3bbd9ac4b8cf0162efb50e01c52e3a97fed31d1e73f12b3da7b4df87fd7e93f70f92dc154d0bcef5a39c9631b2b50c7c7b91f4a63d4e3d487c37fc63bbdf87b71cad5581409661d15f77ff738a4d53d23424d999490607ebfd8293b2fb5c269cd8a19477ca88f6f7cb1abe00fb16c9"; + + expect(await rsassapss.verifySha256(message, s, e, n)).to.be.false; + }); + }); +});