diff --git a/contracts/signatures/PostageStampSig.sol b/contracts/signatures/PostageStampSig.sol new file mode 100644 index 0000000..dcc1d5b --- /dev/null +++ b/contracts/signatures/PostageStampSig.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./Signature.sol"; + +contract PostageStampSig is Signature { + /** Hash of the message to sign */ + function getMessageHash( + bytes32 _chunkAddr, + bytes32 _batchId, + uint64 _index, + uint64 _timeStamp + ) public pure returns (bytes32) { + return keccak256(abi.encodePacked(_chunkAddr, _batchId, _index, _timeStamp)); + } + + function verify( + address _signer, // signer Ethereum address to check against + bytes memory _signature, + bytes32 _chunkAddr, + bytes32 _postageId, + uint64 _index, + uint64 _timeStamp + ) public pure returns (bool) { + bytes32 messageHash = getMessageHash(_chunkAddr, _postageId, _index, _timeStamp); + bytes32 ethMessageHash = Signature.getEthSignedMessageHash(messageHash); + + return Signature.recoverSigner(ethMessageHash, _signature) == _signer; + } +} diff --git a/contracts/signatures/Signature.sol b/contracts/signatures/Signature.sol new file mode 100644 index 0000000..c6c1018 --- /dev/null +++ b/contracts/signatures/Signature.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +// based on: https://solidity-by-example.org/signature/ + +/* Signature Verification + +How to Sign and Verify +# Signing +1. Create message to sign +2. Hash the message +3. Sign the hash (off chain, keep your private key secret) + +# Verify +1. Recreate hash from the original message +2. Recover signer from signature and hash +3. Compare recovered signer to claimed signer +*/ + +contract Signature { + /** Appends Ethereum Signed Message prefix to the message hash */ + function getEthSignedMessageHash( + bytes32 _messageHash + ) public pure returns (bytes32) { + /* + Signature is produced by signing a keccak256 hash with the following format: + "\x19Ethereum Signed Message\n" + len(msg) + msg + */ + return + keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash) + ); + } + + function recoverSigner( + bytes32 _ethSignedMessageHash, // it has to be prefixed message: https://ethereum.stackexchange.com/questions/19582/does-ecrecover-in-solidity-expects-the-x19ethereum-signed-message-n-prefix/21037 + bytes memory _signature + ) public pure returns (address) { + (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); + + return ecrecover(_ethSignedMessageHash, v, r, s); + } + + function splitSignature( + bytes memory sig + ) public pure returns (bytes32 r_, bytes32 s_, uint8 v_) { + require(sig.length == 65, "invalid signature length"); + + assembly { + /* + verbose explanation: https://ethereum.stackexchange.com/questions/135591/split-signature-function-in-solidity-by-example-docs + First 32 bytes stores the length of the signature + + add(sig, 32) = pointer of sig + 32 + effectively, skips first 32 bytes of signature + + mload(p) loads next 32 bytes starting at the memory address p into memory + */ + + // first 32 bytes, after the length prefix + r_ := mload(add(sig, 32)) + // second 32 bytes + s_ := mload(add(sig, 64)) + // final byte (first byte of the next 32 bytes) + v_ := byte(0, mload(add(sig, 96))) + } + + // implicitly return (r, s, v) + } +} diff --git a/contracts/signatures/SocSig.sol b/contracts/signatures/SocSig.sol new file mode 100644 index 0000000..2ad8ff2 --- /dev/null +++ b/contracts/signatures/SocSig.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./Signature.sol"; + +contract SocSig is Signature { + /** Hash of the message to sign */ + function getMessageHash( + bytes32 _identifier, + bytes32 _chunkAddr + ) public pure returns (bytes32) { + return keccak256(abi.encodePacked(_identifier, _chunkAddr)); + } + + function verify( + address _signer, // signer Ethereum address to check against + bytes memory _signature, + bytes32 _identifier, + bytes32 _chunkAddr + ) public pure returns (bool) { + bytes32 messageHash = getMessageHash(_identifier, _chunkAddr); + bytes32 ethMessageHash = Signature.getEthSignedMessageHash(messageHash); + + return Signature.recoverSigner(ethMessageHash, _signature) == _signer; + } +} diff --git a/test/Signatures.ts b/test/Signatures.ts new file mode 100644 index 0000000..ec9ce00 --- /dev/null +++ b/test/Signatures.ts @@ -0,0 +1,63 @@ +import { expect } from 'chai' +import { ethers } from 'hardhat' + +describe('Signatues', function () { + it('should recover postage stamp signature', async function () { + const accounts = await ethers.getSigners() + + const PostageStampSig = await ethers.getContractFactory('PostageStampSig') + const postageStampSig = await PostageStampSig.deploy() + await postageStampSig.deployed() + + const signer = accounts[0] + const chunkAddrHex = '0x98371fb1297da62c1355abd8f9a7e43dd20cd5ddb9db7ba7c2d99e35e7afb58f' + const batchIdHex = '0xba73d3e3dfe9bd95595c6fa67f682adf3fde207ab339cffd7cc24a1905389a9c' + const index = BigInt('1039382085632') + const ts = BigInt('1673624779490180673') + + const hash = await postageStampSig.getMessageHash(chunkAddrHex, batchIdHex, index, ts) + const sig = await signer.signMessage(ethers.utils.arrayify(hash)) + + const ethHash = await postageStampSig.getEthSignedMessageHash(hash) + + expect(signer.address).to.equal(await postageStampSig.recoverSigner(ethHash, sig)) + + // Correct signature and message returns true + expect(await postageStampSig.verify(signer.address, sig, chunkAddrHex, batchIdHex, index, ts)).to.equal(true) + + // Incorrect message returns false + expect(await postageStampSig.verify(signer.address, sig, chunkAddrHex, batchIdHex, '0', ts)).to.equal(false) + }) + + it('should recover Single Owner Chunk signature', async function () { + const accounts = await ethers.getSigners() + + const SocSig = await ethers.getContractFactory('SocSig') + const socSig = await SocSig.deploy() + await socSig.deployed() + + const signer = accounts[0] + const topicHex = '0x0000000000000000000000000000000000000000000000000000000000000000' + const chunkAddrHex = '0x98371fb1297da62c1355abd8f9a7e43dd20cd5ddb9db7ba7c2d99e35e7afb58f' + + const hash = await socSig.getMessageHash(topicHex, chunkAddrHex) + const sig = await signer.signMessage(ethers.utils.arrayify(hash)) + + const ethHash = await socSig.getEthSignedMessageHash(hash) + + expect(signer.address).to.equal(await socSig.recoverSigner(ethHash, sig)) + + // Correct signature and message returns true + expect(await socSig.verify(signer.address, sig, topicHex, chunkAddrHex)).to.equal(true) + + // Incorrect message returns false + expect( + await socSig.verify( + signer.address, + sig, + '0x0000000000000000000000000000000000000000000000000000000000000001', + chunkAddrHex, + ), + ).to.equal(false) + }) +})