Skip to content
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
45 changes: 10 additions & 35 deletions src/Util/Signatures.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

library Signatures {
error InvalidSignatureLength();
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

library Signatures {
/** Hash of the message to sign */
function getPostageMessageHash(
bytes32 _chunkAddr,
Expand All @@ -25,7 +25,7 @@ library Signatures {
bytes32 messageHash = getPostageMessageHash(_chunkAddr, _postageId, _index, _timeStamp);
bytes32 ethMessageHash = getEthSignedMessageHash(messageHash);

return recoverSigner(ethMessageHash, _signature) == _signer;
return verifySignature(ethMessageHash, _signature, _signer);
}

function getEthSignedMessageHash(bytes32 _messageHash) internal pure returns (bytes32) {
Expand All @@ -36,38 +36,13 @@ library Signatures {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash));
}

function recoverSigner(
function verifySignature(
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
) internal pure returns (address) {
(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);

return ecrecover(_ethSignedMessageHash, v, r, s);
}

function splitSignature(bytes memory sig) internal pure returns (bytes32 r_, bytes32 s_, uint8 v_) {
if (sig.length != 65) {
revert InvalidSignatureLength();
}

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)
bytes memory _signature,
address _signer
) internal pure returns (bool) {
(address recovered, ECDSA.RecoverError error) = ECDSA.tryRecover(_ethSignedMessageHash, _signature);
return error == ECDSA.RecoverError.NoError && recovered == _signer;
}

function getSocMessageHash(bytes32 _identifier, bytes32 _chunkAddr) internal pure returns (bytes32) {
Expand All @@ -83,6 +58,6 @@ library Signatures {
bytes32 messageHash = getSocMessageHash(_identifier, _chunkAddr);
bytes32 ethMessageHash = getEthSignedMessageHash(messageHash);

return recoverSigner(ethMessageHash, _signature) == _signer;
return verifySignature(ethMessageHash, _signature, _signer);
}
}
27 changes: 27 additions & 0 deletions src/test/SignaturesHarness.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "../Util/Signatures.sol";

/// @dev Thin wrapper for unit-testing the Signatures library.
contract SignaturesHarness {
function socVerify(
address signer,
bytes memory signature,
bytes32 identifier,
bytes32 chunkAddr
) external pure returns (bool) {
return Signatures.socVerify(signer, signature, identifier, chunkAddr);
}

function postageVerify(
address signer,
bytes memory signature,
bytes32 chunkAddr,
bytes32 postageId,
uint64 index,
uint64 timeStamp
) external pure returns (bool) {
return Signatures.postageVerify(signer, signature, chunkAddr, postageId, index, timeStamp);
}
}
12 changes: 12 additions & 0 deletions test/Redistribution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
const height_0_n_2 = 2;

//fake
const overlay_f = '0xf4153f4153f4153f4153f4153f4153f4153f4153f4153f4153f4153f4153f415';

Check warning on line 68 in test/Redistribution.test.ts

View workflow job for this annotation

GitHub Actions / Check Code Quality & Run Tests

'overlay_f' is assigned a value but never used
const depth_f = '0x0000000000000000000000000000000000000000000000000000000000000007';
const reveal_nonce_f = '0xf4153f4153f4153f4153f4153f4153f4153f4153f4153f4153f4153f4153f415';

Expand Down Expand Up @@ -118,12 +118,12 @@
const { depth: depth_5, hash: hash_5 } = node5_proof1;
const height_5 = 0;

let node_6: string;

Check warning on line 121 in test/Redistribution.test.ts

View workflow job for this annotation

GitHub Actions / Check Code Quality & Run Tests

'node_6' is assigned a value but never used
const overlay_6 = '0x141680b0d9c7ab250672fd4603ac13e39e47de6e2c93d71bbdc66459a6c5e39f';

Check warning on line 122 in test/Redistribution.test.ts

View workflow job for this annotation

GitHub Actions / Check Code Quality & Run Tests

'overlay_6' is assigned a value but never used
const stakeAmount_6 = '100000000000000000';

Check warning on line 123 in test/Redistribution.test.ts

View workflow job for this annotation

GitHub Actions / Check Code Quality & Run Tests

'stakeAmount_6' is assigned a value but never used

const nonce_6 = '0xb5555b33b5555b33b5555b33b5555b33b5555b33b5555b33b5555b33b5555b33';

Check warning on line 125 in test/Redistribution.test.ts

View workflow job for this annotation

GitHub Actions / Check Code Quality & Run Tests

'nonce_6' is assigned a value but never used
const hash_6 = '0xb5555b33b5555b33b5555b33b5555b33b5555b33b5555b33b5555b33b5555b33';

Check warning on line 126 in test/Redistribution.test.ts

View workflow job for this annotation

GitHub Actions / Check Code Quality & Run Tests

'hash_6' is assigned a value but never used
const depth_6 = '0x06';
const reveal_nonce_6 = '0xb5555b33b5555b33b5555b33b5555b33b5555b33b5555b33b5555b33b5555b33';

Expand Down Expand Up @@ -1173,6 +1173,18 @@
).to.be.revertedWith(errors.claim.socVerificationFailed);
});

it('rejects SOC address(0) signer bypass at claim', async function () {
const { proofParams } = await generatedSampling(true);
const socProof = proofParams.proof1.socProof![0];

socProof.signer = ethers.constants.AddressZero;
socProof.signature = '0x' + '00'.repeat(65);

await expect(
r_node_5.claim(proofParams.proof1, proofParams.proof2, proofParams.proofLast)
).to.be.revertedWith(errors.claim.socVerificationFailed);
});

it('SOC attachment does not match with witness', async function () {
const { proofParams } = await generatedSampling(true);

Expand Down
86 changes: 86 additions & 0 deletions test/Signatures.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { expect } from './util/chai';
import { ethers } from 'hardhat';
import { hexlify } from 'ethers/lib/utils';
import { randomBytes } from 'crypto';

describe('Signatures', () => {
let harness: Awaited<ReturnType<typeof deployHarness>>;

async function deployHarness() {
const factory = await ethers.getContractFactory('SignaturesHarness');
return factory.deploy();
}

before(async () => {
harness = await deployHarness();
});

it('rejects SOC verification bypass via address(0) and malformed signature', async () => {
const identifier = randomBytes(32);
const chunkAddr = randomBytes(32);
const malformedSignature = '0x' + '00'.repeat(65);

const verified = await harness.socVerify(
ethers.constants.AddressZero,
malformedSignature,
hexlify(identifier),
hexlify(chunkAddr)
);

expect(verified).to.be.false;
});

it('rejects SOC verification with invalid signature length', async () => {
const identifier = randomBytes(32);
const chunkAddr = randomBytes(32);
const shortSignature = '0x' + '00'.repeat(64);

const verified = await harness.socVerify(
ethers.Wallet.createRandom().address,
shortSignature,
hexlify(identifier),
hexlify(chunkAddr)
);

expect(verified).to.be.false;
});

it('rejects SOC verification with high-s malleable signature', async () => {
const wallet = ethers.Wallet.createRandom();
const identifier = randomBytes(32);
const chunkAddr = randomBytes(32);
const messageHash = ethers.utils.solidityKeccak256(['bytes32', 'bytes32'], [identifier, chunkAddr]);
const signature = await wallet.signMessage(ethers.utils.arrayify(messageHash));

const { r, s, v } = ethers.utils.splitSignature(signature);
const malleableS = ethers.BigNumber.from('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141').sub(
ethers.BigNumber.from(s)
);
const malleableSignature = ethers.utils.hexConcat([
r,
ethers.utils.hexZeroPad(malleableS.toHexString(), 32),
ethers.utils.hexlify(v === 27 ? 28 : 27),
]);

const verified = await harness.socVerify(
wallet.address,
malleableSignature,
hexlify(identifier),
hexlify(chunkAddr)
);

expect(verified).to.be.false;
});

it('accepts a valid SOC signature', async () => {
const wallet = ethers.Wallet.createRandom();
const identifier = randomBytes(32);
const chunkAddr = randomBytes(32);
const messageHash = ethers.utils.solidityKeccak256(['bytes32', 'bytes32'], [identifier, chunkAddr]);
const signature = await wallet.signMessage(ethers.utils.arrayify(messageHash));

const verified = await harness.socVerify(wallet.address, signature, hexlify(identifier), hexlify(chunkAddr));

expect(verified).to.be.true;
});
});
Loading