Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: signatures #96

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions contracts/signatures/PostageStampSig.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
69 changes: 69 additions & 0 deletions contracts/signatures/Signature.sol
Original file line number Diff line number Diff line change
@@ -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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the added prefix string would make it an invalid as transaction

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the way how Bee client signs all message
https://github.com/ethersphere/bee/blob/master/pkg/crypto/signer.go#L82-L83
we do not check signed blockchain transactions with this function.

);
}

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 {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you can avoid assembly and send r,s,v directly into recoverSigner, you would simplyfy contract

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but the assemly part sets values for r_, s_ and v_, those are output parameters.

/*
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)
}
}
26 changes: 26 additions & 0 deletions contracts/signatures/SocSig.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
63 changes: 63 additions & 0 deletions test/Signatures.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})