From b51110d78efc6b4045ece646f0a5451d10e5d7a4 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Tue, 9 Apr 2024 16:47:15 +0300 Subject: [PATCH 1/2] added tss functions --- contracts/registration/Registration.sol | 25 +- contracts/utils/PoseidonSMT.sol | 2 + contracts/utils/TSSSigner.sol | 60 +++ package-lock.json | 38 ++ package.json | 5 +- test/helpers/TSSMerkleTree.ts | 48 +++ test/helpers/TSSSigner.ts | 25 ++ test/helpers/index.ts | 2 + test/registration/Registration.test.ts | 469 +++++++++++++++--------- 9 files changed, 490 insertions(+), 184 deletions(-) create mode 100644 contracts/utils/TSSSigner.sol create mode 100644 test/helpers/TSSMerkleTree.ts create mode 100644 test/helpers/TSSSigner.ts diff --git a/contracts/registration/Registration.sol b/contracts/registration/Registration.sol index cd7331b..546c682 100644 --- a/contracts/registration/Registration.sol +++ b/contracts/registration/Registration.sol @@ -8,13 +8,15 @@ import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.s import {VerifierHelper} from "@solarity/solidity-lib/libs/zkp/snarkjs/VerifierHelper.sol"; import {PoseidonSMT} from "../utils/PoseidonSMT.sol"; +import {TSSSigner} from "../utils/TSSSigner.sol"; import {RSAVerifier} from "../utils/RSAVerifier.sol"; -contract Registration is PoseidonSMT, Initializable { +contract Registration is PoseidonSMT, TSSSigner, Initializable { using VerifierHelper for address; using RSAVerifier for bytes; uint256 public constant E = 65537; + string public constant ICAO_PREFIX = "Rarimo CSCA root"; bytes32 public constant REVOKED = keccak256("REVOKED"); struct PassportInfo { @@ -41,10 +43,12 @@ contract Registration is PoseidonSMT, Initializable { function __Registration_init( uint256 treeHeight_, + address signer_, address verifier_, bytes32 icaoMasterTreeMerkleRoot_ ) external initializer { __PoseidonSMT_init(treeHeight_); + __TSSSigner_init(signer_); verifier = verifier_; icaoMasterTreeMerkleRoot = icaoMasterTreeMerkleRoot_; @@ -167,6 +171,25 @@ contract Registration is PoseidonSMT, Initializable { emit ReissuedIdentity(bytes32(passportKey_), bytes32(identityKey_)); } + function changeICAOMasterTreeRoot( + bytes32 newRoot_, + uint64 timestamp, + bytes memory proof_ + ) external { + bytes32 leaf_ = keccak256(abi.encodePacked(ICAO_PREFIX, newRoot_, timestamp)); + + _useNonce(timestamp); + _checkMerkleSignature(leaf_, proof_); + + icaoMasterTreeMerkleRoot = newRoot_; + } + + function changeSigner(bytes memory newSignerPubKey_, bytes memory signature_) external { + _checkSignature(keccak256(newSignerPubKey_), signature_); + + signer = _convertPubKeyToAddress(newSignerPubKey_); + } + function getPassportInfo( bytes memory passportPublicKey_ ) diff --git a/contracts/utils/PoseidonSMT.sol b/contracts/utils/PoseidonSMT.sol index 4254468..38dea3b 100644 --- a/contracts/utils/PoseidonSMT.sol +++ b/contracts/utils/PoseidonSMT.sol @@ -55,4 +55,6 @@ contract PoseidonSMT { ) ); } + + uint256[46] private _gap; } diff --git a/contracts/utils/TSSSigner.sol b/contracts/utils/TSSSigner.sol new file mode 100644 index 0000000..77f6257 --- /dev/null +++ b/contracts/utils/TSSSigner.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; + +abstract contract TSSSigner { + using ECDSA for bytes32; + using MerkleProof for bytes32[]; + + uint256 public constant P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F; + + address public signer; + + mapping(uint64 => bool) internal _nonces; + + function __TSSSigner_init(address signer_) internal { + signer = signer_; + } + + function _useNonce(uint64 nonce_) internal { + require(!_nonces[nonce_], "TSSSigners: nonce used"); + + _nonces[nonce_] = true; + } + + function _checkSignature(bytes32 signHash_, bytes memory signature_) internal view { + address signer_ = signHash_.recover(signature_); + + require(signer == signer_, "TSSSigners: invalid signature"); + } + + function _checkMerkleSignature(bytes32 merkleLeaf_, bytes memory proof_) internal view { + (bytes32[] memory merklePath_, bytes memory signature_) = abi.decode( + proof_, + (bytes32[], bytes) + ); + + bytes32 merkleRoot_ = merklePath_.processProof(merkleLeaf_); + + _checkSignature(merkleRoot_, signature_); + } + + function _convertPubKeyToAddress(bytes memory pubKey_) internal pure returns (address) { + require(pubKey_.length == 64, "TSSSigners: wrong pubKey length"); + + (uint256 x_, uint256 y_) = abi.decode(pubKey_, (uint256, uint256)); + + // @dev y^2 = x^3 + 7, x != 0, y != 0 (mod P) + require(x_ != 0 && y_ != 0 && x_ != P && y_ != P, "TSSSigners: zero pubKey"); + require( + mulmod(y_, y_, P) == addmod(mulmod(mulmod(x_, x_, P), x_, P), 7, P), + "TSSSigners: pubKey not on the curve" + ); + + return address(uint160(uint256(keccak256(pubKey_)))); + } + + uint256[48] private _gap; +} diff --git a/package-lock.json b/package-lock.json index 9c71a01..074a8d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "hardhat-contract-sizer": "^2.10.0", "hardhat-gas-reporter": "^2.0.2", "husky": "^9.0.11", + "merkletreejs": "^0.3.11", "mocha": "^10.3.0", "prettier": "^3.2.5", "prettier-plugin-solidity": "^1.3.1", @@ -4442,6 +4443,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-reverse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-reverse/-/buffer-reverse-1.0.1.tgz", + "integrity": "sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==", + "dev": true + }, "node_modules/buffer-to-arraybuffer": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/buffer-to-arraybuffer/-/buffer-to-arraybuffer-0.0.5.tgz", @@ -5293,6 +5300,12 @@ "sha3": "^2.1.1" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "dev": true + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -9089,6 +9102,22 @@ "node": ">= 8" } }, + "node_modules/merkletreejs": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/merkletreejs/-/merkletreejs-0.3.11.tgz", + "integrity": "sha512-LJKTl4iVNTndhL+3Uz/tfkjD0klIWsHlUzgtuNnNrsf7bAlXR30m+xYB7lHr5Z/l6e/yAIsr26Dabx6Buo4VGQ==", + "dev": true, + "dependencies": { + "bignumber.js": "^9.0.1", + "buffer-reverse": "^1.0.1", + "crypto-js": "^4.2.0", + "treeify": "^1.1.0", + "web3-utils": "^1.3.4" + }, + "engines": { + "node": ">= 7.6.0" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -12860,6 +12889,15 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-command-line-args": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", diff --git a/package.json b/package.json index 74adbd4..a2eeb58 100644 --- a/package.json +++ b/package.json @@ -71,12 +71,12 @@ "chai": "^4.4.1", "circomlibjs": "^0.1.7", "dotenv": "16.4.5", - "hardhat": "2.20.1", - "typechain": "8.3.2", "ethers": "^6.11.1", + "hardhat": "2.20.1", "hardhat-contract-sizer": "^2.10.0", "hardhat-gas-reporter": "^2.0.2", "husky": "^9.0.11", + "merkletreejs": "^0.3.11", "mocha": "^10.3.0", "prettier": "^3.2.5", "prettier-plugin-solidity": "^1.3.1", @@ -85,6 +85,7 @@ "solidity-coverage": "^0.8.11", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", + "typechain": "8.3.2", "typescript": "^5.4.3" } } diff --git a/test/helpers/TSSMerkleTree.ts b/test/helpers/TSSMerkleTree.ts new file mode 100644 index 0000000..a654374 --- /dev/null +++ b/test/helpers/TSSMerkleTree.ts @@ -0,0 +1,48 @@ +import { ethers } from "hardhat"; + +import { MerkleTree } from "merkletreejs"; + +import { TSSSigner } from "./TSSSigner"; + +export class TSSMerkleTree { + public tree: MerkleTree; + + constructor(public tssSigner: TSSSigner) { + const leaves = Array.from({ length: 10 }, () => ethers.randomBytes(32)); + + this.tree = new MerkleTree( + leaves, + (e: Buffer) => { + const hash = ethers.solidityPackedKeccak256(["bytes"], [e]); + + return Buffer.from(hash.slice(2), "hex"); + }, + { sortPairs: true }, + ); + } + + public addLeaf(leaf: string) { + this.tree.addLeaf(Buffer.from(leaf.slice(2), "hex")); + } + + public getPath(leaf: string): Array { + return this.tree.getProof(leaf).map((el) => "0x" + el.data.toString("hex")); + } + + public getProof(leaf: string, addLeaf: boolean = true): string { + if (addLeaf) { + this.addLeaf(leaf); + } + + const root = this.getRoot(); + const path = this.getPath(leaf); + + const signature = this.tssSigner.sign(root); + + return ethers.AbiCoder.defaultAbiCoder().encode(["bytes32[]", "bytes"], [path, signature]); + } + + public getRoot(): string { + return "0x" + this.tree.getRoot().toString("hex"); + } +} diff --git a/test/helpers/TSSSigner.ts b/test/helpers/TSSSigner.ts new file mode 100644 index 0000000..5be1fa5 --- /dev/null +++ b/test/helpers/TSSSigner.ts @@ -0,0 +1,25 @@ +import { ethers } from "hardhat"; +import { BigNumberish, BytesLike, HDNodeWallet } from "ethers"; + +export class TSSSigner { + constructor(public signer: HDNodeWallet) {} + + public changeICAOMasterTreeRoot(newRoot: string, timestamp: BigNumberish): string { + const hash = ethers.solidityPackedKeccak256( + ["string", "bytes32", "uint64"], + ["Rarimo CSCA root", newRoot, timestamp], + ); + + return this.sign(hash); + } + + public signChangeSigner(newPubKey: string): string { + const hash = ethers.solidityPackedKeccak256(["bytes"], [newPubKey]); + + return this.sign(hash); + } + + public sign(hash: BytesLike) { + return ethers.Signature.from(this.signer.signingKey.sign(hash)).serialized; + } +} diff --git a/test/helpers/index.ts b/test/helpers/index.ts index 36e8c4e..b6e2522 100644 --- a/test/helpers/index.ts +++ b/test/helpers/index.ts @@ -1,3 +1,5 @@ export * from "./reverter"; export * from "./poseidon-hash"; export * from "./poseidon-deploy"; +export * from "./TSSSigner"; +export * from "./TSSMerkleTree"; diff --git a/test/registration/Registration.test.ts b/test/registration/Registration.test.ts index 7eff8ed..156bc9e 100644 --- a/test/registration/Registration.test.ts +++ b/test/registration/Registration.test.ts @@ -1,11 +1,14 @@ import { ethers } from "hardhat"; +import { HDNodeWallet } from "ethers"; import { expect } from "chai"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { Reverter, deployPoseidons, getPoseidon, poseidonHash } from "@/test/helpers/"; +import { Reverter, deployPoseidons, getPoseidon } from "@/test/helpers/"; -import { RegistrationMock, VerifierMock, RegistrationVerifier } from "@ethers-v6"; +import { RegistrationMock, RegistrationVerifier } from "@ethers-v6"; import { VerifierHelper } from "@/generated-types/ethers/contracts/registration/Registration"; +import { TSSMerkleTree, TSSSigner } from "../helpers"; + const TREE_SIZE = 80; const icaoMerkleRoot = "0x2c50ce3aa92bc3dd0351a89970b02630415547ea83c487befbc8b1795ea90c45"; @@ -18,13 +21,18 @@ const passportPubKey = describe("Registration", () => { const reverter = new Reverter(); + let signHelper: TSSSigner; + let merkleTree: TSSMerkleTree; + let OWNER: SignerWithAddress; + let SIGNER: HDNodeWallet; + let registration: RegistrationMock; - let verifierMock: VerifierMock; let registrationVerifier: RegistrationVerifier; before("setup", async () => { [OWNER] = await ethers.getSigners(); + SIGNER = ethers.Wallet.createRandom(); await deployPoseidons(OWNER, [1, 2, 3, 5], false); @@ -36,248 +44,347 @@ describe("Registration", () => { PoseidonUnit5L: await (await getPoseidon(5)).getAddress(), }, }); - const VerifierMock = await ethers.getContractFactory("VerifierMock"); const RegistrationVerifier = await ethers.getContractFactory("RegistrationVerifier"); registration = await Registration.deploy(); - verifierMock = await VerifierMock.deploy(); registrationVerifier = await RegistrationVerifier.deploy(); + await registration.__Registration_init( + TREE_SIZE, + SIGNER.address, + await registrationVerifier.getAddress(), + icaoMerkleRoot, + ); + + signHelper = new TSSSigner(SIGNER); + merkleTree = new TSSMerkleTree(signHelper); + await reverter.snapshot(); }); afterEach(reverter.revert); - const register = async ( - identityOverride?: string, - signatureOverride?: string, - proofOverride?: VerifierHelper.ProofPointsStruct, - ) => { - const signature = - "0xa7ea14a7734f7d789c4f0493bd71cc34a10986c2ce2bfad2118f05d21b4aaf65ca15f6f408e2af0ecdbf37fbaca88998d365f39d865a8face1ca4205ca94a59630af03b2c565f29dea5f8b317a7c5f04db16f2ff115c21c9696e9d45a635c2f066cfd50df09d41328888d2c8fc386e9ebc170b56977af65a5ad74db1f0d25d46"; - const dgCommit = "0x206187ab789aca5d2073582ba86f76c57f741ed2c78ccb96d79a9600b49df8d6"; - - const formattedProof: VerifierHelper.ProofPointsStruct = { - a: [ - "0x03960af5aef41d09feedd5667557b3f66906efc99d618e1b620c258d703f464e", - "0x0198fdc5850cf60eb9322a4698168104b17929bd788a8fc41293e484253ad3e5", - ], - b: [ - [ - "0x0e87a6078b813d04d28c8eece683bdafed42d09164d84d4f985e33ab8dc71632", - "0x046a25322c4b9895633cc5843743c4bbf2a713b7e29de5a9f26a8cad8de89929", + describe("$init flow", () => { + describe("#init", () => { + it("should not initialize twice", async () => { + expect( + registration.__Registration_init( + TREE_SIZE, + SIGNER.address, + await registrationVerifier.getAddress(), + icaoMerkleRoot, + ), + ).to.be.revertedWith("Initializable: contract is already initialized"); + }); + }); + }); + + describe("$registration flow", () => { + const register = async ( + identityOverride?: string, + signatureOverride?: string, + proofOverride?: VerifierHelper.ProofPointsStruct, + ) => { + const signature = + "0xa7ea14a7734f7d789c4f0493bd71cc34a10986c2ce2bfad2118f05d21b4aaf65ca15f6f408e2af0ecdbf37fbaca88998d365f39d865a8face1ca4205ca94a59630af03b2c565f29dea5f8b317a7c5f04db16f2ff115c21c9696e9d45a635c2f066cfd50df09d41328888d2c8fc386e9ebc170b56977af65a5ad74db1f0d25d46"; + const dgCommit = "0x206187ab789aca5d2073582ba86f76c57f741ed2c78ccb96d79a9600b49df8d6"; + + const formattedProof: VerifierHelper.ProofPointsStruct = { + a: [ + "0x03960af5aef41d09feedd5667557b3f66906efc99d618e1b620c258d703f464e", + "0x0198fdc5850cf60eb9322a4698168104b17929bd788a8fc41293e484253ad3e5", + ], + b: [ + [ + "0x0e87a6078b813d04d28c8eece683bdafed42d09164d84d4f985e33ab8dc71632", + "0x046a25322c4b9895633cc5843743c4bbf2a713b7e29de5a9f26a8cad8de89929", + ], + [ + "0x195e73b16694ab3336060030270992f8c3a7645890b21538dbbf14eefd3b7c1d", + "0x15eb5353267f74221773d78bc90ba1f339dae7741fc579d224f5e8095dfccb11", + ], ], - [ - "0x195e73b16694ab3336060030270992f8c3a7645890b21538dbbf14eefd3b7c1d", - "0x15eb5353267f74221773d78bc90ba1f339dae7741fc579d224f5e8095dfccb11", + c: [ + "0x10dabe3bdf9e7cdb86977c8ad8234a9ce9ca7663e221678d24790e691719f210", + "0x02b23d6029d3a89e45d77d09ed8c81cc669dd7b6bbaf43020520e1f6bdf39249", ], - ], - c: [ - "0x10dabe3bdf9e7cdb86977c8ad8234a9ce9ca7663e221678d24790e691719f210", - "0x02b23d6029d3a89e45d77d09ed8c81cc669dd7b6bbaf43020520e1f6bdf39249", - ], + }; + + return registration.register( + identityOverride ?? identityKey, + dgCommit, + signatureOverride ?? signature, + passportPubKey, + proofOverride ?? formattedProof, + ); }; - return registration.register( - identityOverride ?? identityKey, - dgCommit, - signatureOverride ?? signature, - passportPubKey, - proofOverride ?? formattedProof, - ); - }; - - const revoke = async (identityOverride?: string, signatureOverride?: string) => { - const signature = - "0xad778f0761f791cef96619cefd7709c031eeaec86c158eb5d58ad636bc494bd84cd2bc9cf21e27ff17479abbecc7bfa284f9d20505b129db8c02cbf9cacc7f883e4d2552565854ec6db2ec736133eea3d6cd0ce514c413ecc7e73dabe7bd09b96638048aff55cd495b800b93c4d8f6ca52c1bd11727aa03056dafcb83ea18364"; - - return registration.revoke(identityOverride ?? identityKey, signatureOverride ?? signature, passportPubKey); - }; - - const reissueIdentity = async ( - identityOverride?: string, - signatureOverride?: string, - proofOverride?: VerifierHelper.ProofPointsStruct, - ) => { - const signature = - "0x8020affeaf4a48fef2ad2846985a4155e05ced9d2c94be1cd2ff86fbdef8196bbbea2ab0a51b28cbb2630b232f9101d1ea09c3f4cf1599e8771219367d9f06bf3ea4968f0412926880d50cfaff35254c56f8a08e303d6ec5a3c48480b4366d4e80a6aa367af8bf9a9f42ea713c00650058d7bd7ca6ce6f4bc8782111b17bc8b9"; - const dgCommit = "0x13b1f399cfaa3d3c26dfbf6c2acd28a747eb044cfc86c2366b10ef059e590192"; - - const formattedProof: VerifierHelper.ProofPointsStruct = { - a: [ - "0x0bb9104059b8be014598f86077bd78d8cbe255e823bde3f99d9927b60ef890a7", - "0x2da9a7f0b765069ca6f0cfa13a97c18c9c8fff4ad6f7d9ec02e0ffa5c7249fcf", - ], - b: [ - [ - "0x1e7c560008de96f6bb5b16558ad75c9bb15e26e70bbb47558f2cd051cc871b6a", - "0x05311d0224e4269d26f3bd737f794dcf6c1481b6ec4ca9866b949fca6a70aacb", + const revoke = async (identityOverride?: string, signatureOverride?: string) => { + const signature = + "0xad778f0761f791cef96619cefd7709c031eeaec86c158eb5d58ad636bc494bd84cd2bc9cf21e27ff17479abbecc7bfa284f9d20505b129db8c02cbf9cacc7f883e4d2552565854ec6db2ec736133eea3d6cd0ce514c413ecc7e73dabe7bd09b96638048aff55cd495b800b93c4d8f6ca52c1bd11727aa03056dafcb83ea18364"; + + return registration.revoke(identityOverride ?? identityKey, signatureOverride ?? signature, passportPubKey); + }; + + const reissueIdentity = async ( + identityOverride?: string, + signatureOverride?: string, + proofOverride?: VerifierHelper.ProofPointsStruct, + ) => { + const signature = + "0x8020affeaf4a48fef2ad2846985a4155e05ced9d2c94be1cd2ff86fbdef8196bbbea2ab0a51b28cbb2630b232f9101d1ea09c3f4cf1599e8771219367d9f06bf3ea4968f0412926880d50cfaff35254c56f8a08e303d6ec5a3c48480b4366d4e80a6aa367af8bf9a9f42ea713c00650058d7bd7ca6ce6f4bc8782111b17bc8b9"; + const dgCommit = "0x13b1f399cfaa3d3c26dfbf6c2acd28a747eb044cfc86c2366b10ef059e590192"; + + const formattedProof: VerifierHelper.ProofPointsStruct = { + a: [ + "0x0bb9104059b8be014598f86077bd78d8cbe255e823bde3f99d9927b60ef890a7", + "0x2da9a7f0b765069ca6f0cfa13a97c18c9c8fff4ad6f7d9ec02e0ffa5c7249fcf", + ], + b: [ + [ + "0x1e7c560008de96f6bb5b16558ad75c9bb15e26e70bbb47558f2cd051cc871b6a", + "0x05311d0224e4269d26f3bd737f794dcf6c1481b6ec4ca9866b949fca6a70aacb", + ], + [ + "0x1d8ea47009bd910d9fc8ae73d8846d8da662254d553c1e92da691c7598a18d05", + "0x0a396c298164888db55c66f957a02b2ba6bdcf7e5882639b60f6bcaf77c24af3", + ], ], - [ - "0x1d8ea47009bd910d9fc8ae73d8846d8da662254d553c1e92da691c7598a18d05", - "0x0a396c298164888db55c66f957a02b2ba6bdcf7e5882639b60f6bcaf77c24af3", + c: [ + "0x0b1ff154590f8a790eac03f121e4a1496afa8cb527ae71f13d2699e31684d296", + "0x1f3496bf0bff888cac2734271621fe65fbb97300dbab972f8df3132574fc8c39", ], - ], - c: [ - "0x0b1ff154590f8a790eac03f121e4a1496afa8cb527ae71f13d2699e31684d296", - "0x1f3496bf0bff888cac2734271621fe65fbb97300dbab972f8df3132574fc8c39", - ], + }; + + return registration.reissueIdentity( + identityOverride ?? newIdentityKey, + dgCommit, + signatureOverride ?? signature, + passportPubKey, + proofOverride ?? formattedProof, + ); }; - return registration.reissueIdentity( - identityOverride ?? newIdentityKey, - dgCommit, - signatureOverride ?? signature, - passportPubKey, - proofOverride ?? formattedProof, - ); - }; + describe("#register", () => { + it("should register", async () => { + await register(); - describe("#init", () => { - it("should not initialize twice", async () => { - await registration.__Registration_init(TREE_SIZE, await registrationVerifier.getAddress(), icaoMerkleRoot); + const passportInfo = await registration.getPassportInfo(passportPubKey); - expect( - registration.__Registration_init(TREE_SIZE, await registrationVerifier.getAddress(), icaoMerkleRoot), - ).to.be.revertedWith("Initializable: contract is already initialized"); - }); - }); + expect(passportInfo.passportInfo_.activeIdentity).to.equal(ethers.toBeHex(identityKey, 32)); + expect(passportInfo.passportInfo_.identityReissueCounter).to.equal(0n); + }); - describe("#register", () => { - beforeEach(async () => { - await registration.__Registration_init(TREE_SIZE, await registrationVerifier.getAddress(), icaoMerkleRoot); - }); + it("should not register with wrong AA", async () => { + const signature = + "0xb7ea14a7734f7d789c4f0493bd71cc34a10986c2ce2bfad2118f05d21b4aaf65ca15f6f408e2af0ecdbf37fbaca88998d365f39d865a8face1ca4205ca94a59630af03b2c565f29dea5f8b317a7c5f04db16f2ff115c21c9696e9d45a635c2f066cfd50df09d41328888d2c8fc386e9ebc170b56977af65a5ad74db1f0d25d46"; - it("should register", async () => { - await register(); + expect(register(identityKey, signature)).to.be.revertedWith("Registration: invalid passport signature"); + }); - const passportInfo = await registration.getPassportInfo(passportPubKey); + it("should not register with wrong ZK proof", async () => { + const signature = + "0xa7ea14a7734f7d789c4f0493bd71cc34a10986c2ce2bfad2118f05d21b4aaf65ca15f6f408e2af0ecdbf37fbaca88998d365f39d865a8face1ca4205ca94a59630af03b2c565f29dea5f8b317a7c5f04db16f2ff115c21c9696e9d45a635c2f066cfd50df09d41328888d2c8fc386e9ebc170b56977af65a5ad74db1f0d25d46"; + const formattedProof: VerifierHelper.ProofPointsStruct = { + a: [0, 0], + b: [ + [0, 0], + [0, 0], + ], + c: [0, 0], + }; - expect(passportInfo.passportInfo_.activeIdentity).to.equal(ethers.toBeHex(identityKey, 32)); - expect(passportInfo.passportInfo_.identityReissueCounter).to.equal(0n); - }); + expect(register(identityKey, signature, formattedProof)).to.be.revertedWith("Registration: invalid zk proof"); + }); - it("should not register with wrong AA", async () => { - const signature = - "0xb7ea14a7734f7d789c4f0493bd71cc34a10986c2ce2bfad2118f05d21b4aaf65ca15f6f408e2af0ecdbf37fbaca88998d365f39d865a8face1ca4205ca94a59630af03b2c565f29dea5f8b317a7c5f04db16f2ff115c21c9696e9d45a635c2f066cfd50df09d41328888d2c8fc386e9ebc170b56977af65a5ad74db1f0d25d46"; + it("should revert if passport already registered", async () => { + await registration.mockPassportData(passportPubKey, identityKey); - expect(register(identityKey, signature)).to.be.revertedWith("Registration: invalid passport signature"); - }); + expect(register()).to.be.revertedWith("Registration: passport already registered"); + }); - it("should not register with wrong ZK proof", async () => { - const signature = - "0xa7ea14a7734f7d789c4f0493bd71cc34a10986c2ce2bfad2118f05d21b4aaf65ca15f6f408e2af0ecdbf37fbaca88998d365f39d865a8face1ca4205ca94a59630af03b2c565f29dea5f8b317a7c5f04db16f2ff115c21c9696e9d45a635c2f066cfd50df09d41328888d2c8fc386e9ebc170b56977af65a5ad74db1f0d25d46"; - const formattedProof: VerifierHelper.ProofPointsStruct = { - a: [0, 0], - b: [ - [0, 0], - [0, 0], - ], - c: [0, 0], - }; + it("should revert if identity already registered", async () => { + await registration.mockIdentityData(identityKey, passportPubKey); + + expect(register()).to.be.revertedWith("Registration: identity already registered"); + }); - expect(register(identityKey, signature, formattedProof)).to.be.revertedWith("Registration: invalid zk proof"); + it("should revert if identity is zero", async () => { + expect(register("0")).to.be.revertedWith("Registration: identity can not be zero"); + }); }); - it("should revert if passport already registered", async () => { - await registration.mockPassportData(passportPubKey, identityKey); + describe("#revoke", () => { + it("should revoke", async () => { + await register(); + await revoke(); - expect(register()).to.be.revertedWith("Registration: passport already registered"); - }); + const passportInfo = await registration.getPassportInfo(passportPubKey); - it("should revert if identity already registered", async () => { - await registration.mockIdentityData(identityKey, passportPubKey); + const revoked = ethers.keccak256(ethers.toUtf8Bytes("REVOKED")); - expect(register()).to.be.revertedWith("Registration: identity already registered"); - }); + expect(passportInfo.passportInfo_.activeIdentity).to.equal(revoked); + }); - it("should revert if identity is zero", async () => { - expect(register("0")).to.be.revertedWith("Registration: identity can not be zero"); - }); - }); + it("should not revoke with the same signature", async () => { + const signature = + "0xa7ea14a7734f7d789c4f0493bd71cc34a10986c2ce2bfad2118f05d21b4aaf65ca15f6f408e2af0ecdbf37fbaca88998d365f39d865a8face1ca4205ca94a59630af03b2c565f29dea5f8b317a7c5f04db16f2ff115c21c9696e9d45a635c2f066cfd50df09d41328888d2c8fc386e9ebc170b56977af65a5ad74db1f0d25d46"; - describe("#revoke", () => { - beforeEach(async () => { - await registration.__Registration_init(TREE_SIZE, await registrationVerifier.getAddress(), icaoMerkleRoot); - }); + await register(); - it("should revoke", async () => { - await register(); - await revoke(); + expect(revoke(identityKey, signature)).to.be.revertedWith("Registration: signature used"); + }); - const passportInfo = await registration.getPassportInfo(passportPubKey); + it("should revert if passport already revoked", async () => { + await register(); - const revoked = ethers.keccak256(ethers.toUtf8Bytes("REVOKED")); + await registration.mockPassportData(passportPubKey, newIdentityKey); - expect(passportInfo.passportInfo_.activeIdentity).to.equal(revoked); - }); + expect(revoke()).to.be.revertedWith("Registration: passport already revoked"); + }); - it("should not revoke with the same signature", async () => { - const signature = - "0xa7ea14a7734f7d789c4f0493bd71cc34a10986c2ce2bfad2118f05d21b4aaf65ca15f6f408e2af0ecdbf37fbaca88998d365f39d865a8face1ca4205ca94a59630af03b2c565f29dea5f8b317a7c5f04db16f2ff115c21c9696e9d45a635c2f066cfd50df09d41328888d2c8fc386e9ebc170b56977af65a5ad74db1f0d25d46"; + it("should revert if identity already revoked", async () => { + await register(); - await register(); + await registration.mockIdentityData(identityKey, passportPubKey.slice(0, -2) + "aa"); - expect(revoke(identityKey, signature)).to.be.revertedWith("Registration: signature used"); + expect(revoke()).to.be.revertedWith("Registration: identity already revoked"); + }); + + it("should revert if identity is zero", async () => { + expect(revoke("0")).to.be.revertedWith("Registration: identity can not be zero"); + }); }); - it("should revert if passport already revoked", async () => { - await register(); + describe("#reissueIdentity", () => { + it("should reissue identity", async () => { + await register(); + await revoke(); + await reissueIdentity(); - await registration.mockPassportData(passportPubKey, newIdentityKey); + const passportInfo = await registration.getPassportInfo(passportPubKey); - expect(revoke()).to.be.revertedWith("Registration: passport already revoked"); - }); + expect(passportInfo.passportInfo_.activeIdentity).to.equal(ethers.toBeHex(newIdentityKey, 32)); + expect(passportInfo.passportInfo_.identityReissueCounter).to.equal(1n); + }); - it("should revert if identity already revoked", async () => { - await register(); + it("should revert if passport is not revoked", async () => { + await register(); + await revoke(); - await registration.mockIdentityData(identityKey, passportPubKey.slice(0, -2) + "aa"); + await registration.mockPassportData(passportPubKey, newIdentityKey); - expect(revoke()).to.be.revertedWith("Registration: identity already revoked"); - }); + expect(reissueIdentity()).to.be.revertedWith("Registration: passport is not revoked"); + }); + + it("should revert if identity is already registered", async () => { + await register(); + await revoke(); + + await registration.mockIdentityData(newIdentityKey, passportPubKey); + + expect(reissueIdentity()).to.be.revertedWith("Registration: identity already registered"); + }); - it("should revert if identity is zero", async () => { - expect(revoke("0")).to.be.revertedWith("Registration: identity can not be zero"); + it("should revert if identity is zero", async () => { + expect(reissueIdentity("0")).to.be.revertedWith("Registration: identity can not be zero"); + }); }); }); - describe("#reissueIdentity", () => { - beforeEach(async () => { - await registration.__Registration_init(TREE_SIZE, await registrationVerifier.getAddress(), icaoMerkleRoot); - }); + describe("$TSS flow", () => { + describe("#changeSigner", () => { + const newSigner = ethers.Wallet.createRandom(); + const tssPublicKey = "0x" + newSigner.signingKey.publicKey.slice(4); - it("should reissue identity", async () => { - await register(); - await revoke(); - await reissueIdentity(); + it("should not change signer if invalid signature", async () => { + const signature = signHelper.signChangeSigner(newSigner.privateKey); + const tx = registration.changeSigner(tssPublicKey, signature); - const passportInfo = await registration.getPassportInfo(passportPubKey); + await expect(tx).to.be.revertedWith("TSSSigners: invalid signature"); + }); - expect(passportInfo.passportInfo_.activeIdentity).to.equal(ethers.toBeHex(newIdentityKey, 32)); - expect(passportInfo.passportInfo_.identityReissueCounter).to.equal(1n); - }); + it("should not change signer if wrong pubKey length", async () => { + const signature = signHelper.signChangeSigner(newSigner.privateKey); + const tx = registration.changeSigner(newSigner.privateKey, signature); - it("should revert if passport is not revoked", async () => { - await register(); - await revoke(); + await expect(tx).to.be.revertedWith("TSSSigners: wrong pubKey length"); + }); - await registration.mockPassportData(passportPubKey, newIdentityKey); + it("should not change signer if zero pubKey", async () => { + const pBytes = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"; + const zeroBytes = "0000000000000000000000000000000000000000000000000000000000000000"; + const notNull = "0000000000000000000000000000000000000000000000000000000000000001"; - expect(reissueIdentity()).to.be.revertedWith("Registration: passport is not revoked"); - }); + const zeroPubKeys = [ + "0x" + zeroBytes + notNull, + "0x" + pBytes + notNull, + "0x" + notNull + zeroBytes, + "0x" + notNull + pBytes, + ]; + + for (const pubKey of zeroPubKeys) { + const signature = signHelper.signChangeSigner(pubKey); + const tx = registration.changeSigner(pubKey, signature); + + await expect(tx).to.be.revertedWith("TSSSigners: zero pubKey"); + } + }); + + it("should not change signer if pubKey not on the curve", async () => { + const wrongPubKey = + "0x10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010"; + + const signature = signHelper.signChangeSigner(wrongPubKey); + const tx = registration.changeSigner(wrongPubKey, signature); + + await expect(tx).to.be.revertedWith("TSSSigners: pubKey not on the curve"); + }); + + it("should change signer if all conditions are met", async () => { + expect(await registration.getFunction("signer").staticCall()).to.eq(SIGNER.address); - it("should revert if identity is already registered", async () => { - await register(); - await revoke(); + const signature = signHelper.signChangeSigner(tssPublicKey); - await registration.mockIdentityData(newIdentityKey, passportPubKey); + await registration.changeSigner(tssPublicKey, signature); - expect(reissueIdentity()).to.be.revertedWith("Registration: identity already registered"); + expect(await registration.getFunction("signer").staticCall()).to.eq(newSigner.address); + }); }); - it("should revert if identity is zero", async () => { - expect(reissueIdentity("0")).to.be.revertedWith("Registration: identity can not be zero"); + describe("#changeICAOMasterTreeRoot", () => { + const newIcaoMerkleRoot = "0x3c50ce3aa92bc3dd0351a89970b02630415547ea83c487befbc8b1795ea90c45"; + const timestamp = "123456"; + + it("should change the root", async () => { + expect(await registration.icaoMasterTreeMerkleRoot()).to.equal(icaoMerkleRoot); + + const leaf = ethers.solidityPackedKeccak256( + ["string", "bytes32", "uint64"], + ["Rarimo CSCA root", newIcaoMerkleRoot, timestamp], + ); + + const proof = merkleTree.getProof(leaf, true); + + await registration.changeICAOMasterTreeRoot(newIcaoMerkleRoot, timestamp, proof); + + expect(await registration.icaoMasterTreeMerkleRoot()).to.equal(newIcaoMerkleRoot); + }); + + it("should not reuse the signature", async () => { + const leaf = ethers.solidityPackedKeccak256( + ["string", "bytes32", "uint64"], + ["Rarimo CSCA root", newIcaoMerkleRoot, timestamp], + ); + + const proof = merkleTree.getProof(leaf, true); + + await registration.changeICAOMasterTreeRoot(newIcaoMerkleRoot, timestamp, proof); + + expect(registration.changeICAOMasterTreeRoot(newIcaoMerkleRoot, timestamp, proof)).to.be.revertedWith( + "TSSSigners: nonce used", + ); + }); }); }); }); From ae4e4db97acc996d2029d600c5e2d97baddcc04f Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Tue, 9 Apr 2024 16:49:17 +0300 Subject: [PATCH 2/2] fix error name --- contracts/utils/TSSSigner.sol | 10 +++++----- test/registration/Registration.test.ts | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/utils/TSSSigner.sol b/contracts/utils/TSSSigner.sol index 77f6257..8ac12c6 100644 --- a/contracts/utils/TSSSigner.sol +++ b/contracts/utils/TSSSigner.sol @@ -19,7 +19,7 @@ abstract contract TSSSigner { } function _useNonce(uint64 nonce_) internal { - require(!_nonces[nonce_], "TSSSigners: nonce used"); + require(!_nonces[nonce_], "TSSSigner: nonce used"); _nonces[nonce_] = true; } @@ -27,7 +27,7 @@ abstract contract TSSSigner { function _checkSignature(bytes32 signHash_, bytes memory signature_) internal view { address signer_ = signHash_.recover(signature_); - require(signer == signer_, "TSSSigners: invalid signature"); + require(signer == signer_, "TSSSigner: invalid signature"); } function _checkMerkleSignature(bytes32 merkleLeaf_, bytes memory proof_) internal view { @@ -42,15 +42,15 @@ abstract contract TSSSigner { } function _convertPubKeyToAddress(bytes memory pubKey_) internal pure returns (address) { - require(pubKey_.length == 64, "TSSSigners: wrong pubKey length"); + require(pubKey_.length == 64, "TSSSigner: wrong pubKey length"); (uint256 x_, uint256 y_) = abi.decode(pubKey_, (uint256, uint256)); // @dev y^2 = x^3 + 7, x != 0, y != 0 (mod P) - require(x_ != 0 && y_ != 0 && x_ != P && y_ != P, "TSSSigners: zero pubKey"); + require(x_ != 0 && y_ != 0 && x_ != P && y_ != P, "TSSSigner: zero pubKey"); require( mulmod(y_, y_, P) == addmod(mulmod(mulmod(x_, x_, P), x_, P), 7, P), - "TSSSigners: pubKey not on the curve" + "TSSSigner: pubKey not on the curve" ); return address(uint160(uint256(keccak256(pubKey_)))); diff --git a/test/registration/Registration.test.ts b/test/registration/Registration.test.ts index 156bc9e..3799ee5 100644 --- a/test/registration/Registration.test.ts +++ b/test/registration/Registration.test.ts @@ -301,14 +301,14 @@ describe("Registration", () => { const signature = signHelper.signChangeSigner(newSigner.privateKey); const tx = registration.changeSigner(tssPublicKey, signature); - await expect(tx).to.be.revertedWith("TSSSigners: invalid signature"); + await expect(tx).to.be.revertedWith("TSSSigner: invalid signature"); }); it("should not change signer if wrong pubKey length", async () => { const signature = signHelper.signChangeSigner(newSigner.privateKey); const tx = registration.changeSigner(newSigner.privateKey, signature); - await expect(tx).to.be.revertedWith("TSSSigners: wrong pubKey length"); + await expect(tx).to.be.revertedWith("TSSSigner: wrong pubKey length"); }); it("should not change signer if zero pubKey", async () => { @@ -327,7 +327,7 @@ describe("Registration", () => { const signature = signHelper.signChangeSigner(pubKey); const tx = registration.changeSigner(pubKey, signature); - await expect(tx).to.be.revertedWith("TSSSigners: zero pubKey"); + await expect(tx).to.be.revertedWith("TSSSigner: zero pubKey"); } }); @@ -338,7 +338,7 @@ describe("Registration", () => { const signature = signHelper.signChangeSigner(wrongPubKey); const tx = registration.changeSigner(wrongPubKey, signature); - await expect(tx).to.be.revertedWith("TSSSigners: pubKey not on the curve"); + await expect(tx).to.be.revertedWith("TSSSigner: pubKey not on the curve"); }); it("should change signer if all conditions are met", async () => { @@ -382,7 +382,7 @@ describe("Registration", () => { await registration.changeICAOMasterTreeRoot(newIcaoMerkleRoot, timestamp, proof); expect(registration.changeICAOMasterTreeRoot(newIcaoMerkleRoot, timestamp, proof)).to.be.revertedWith( - "TSSSigners: nonce used", + "TSSSigner: nonce used", ); }); });