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

Signature length check #453

Merged
merged 52 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
9c9136b
[#754] WIP: Signature length check
akshay-ap Jul 1, 2024
c512034
[#754] Update comment in SafeL2Mod
akshay-ap Jul 1, 2024
6ffaaf8
[#754] Add signature length checks in Safe4337Module
akshay-ap Jul 4, 2024
78ef010
[#754] Fix lint issues
akshay-ap Jul 4, 2024
f61aec6
[#754] Summarize _checkSignatureLength function
akshay-ap Jul 4, 2024
c72d37f
[#754] Update comment
akshay-ap Jul 4, 2024
7fc14c1
[#754] Remove commented code
akshay-ap Jul 4, 2024
2754975
[#754] Use calldataload instruction
akshay-ap Jul 4, 2024
032c7a3
[#754] Fixes
akshay-ap Jul 4, 2024
17e6d46
[#754] Fixes
akshay-ap Jul 4, 2024
9e22daf
[#754] Update tests
akshay-ap Jul 4, 2024
dd43fe3
Update modules/4337/certora/specs/Safe4337Module.spec
akshay-ap Jul 8, 2024
d5cb574
Update modules/4337/certora/specs/ValidationDataLastBitOne.spec
akshay-ap Jul 8, 2024
fce0584
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 8, 2024
9e526d6
[#754] Add comment on signature encoding in _checkSignatureLength
akshay-ap Jul 8, 2024
8450bb7
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 8, 2024
9c131c6
[#754] Update gas estimates and fix compilation error
akshay-ap Jul 8, 2024
c8702f6
[#754] Fix lint issue
akshay-ap Jul 8, 2024
d83e133
[#754] Adjust signature check logic
akshay-ap Jul 8, 2024
affd342
[#754] Update comment, adjust _validateSignatures
akshay-ap Jul 8, 2024
9047334
Update modules/4337/certora/specs/ValidationDataLastBitOne.spec
akshay-ap Jul 8, 2024
001c6ce
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 8, 2024
af3b708
[#754] Rename variables, add comments
akshay-ap Jul 8, 2024
d8ba708
[#754] Lint fix
akshay-ap Jul 8, 2024
22fc197
[#754] Add tests
akshay-ap Jul 9, 2024
53345a6
[#754] Rename variable
akshay-ap Jul 9, 2024
1c31fda
[#754] Update comment
akshay-ap Jul 9, 2024
195f792
Update modules/4337/test/erc4337/ERC4337ModuleNew.spec.ts
akshay-ap Jul 9, 2024
3975d8f
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 10, 2024
16c7b8b
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 10, 2024
3a37597
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 10, 2024
5fa8afd
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 10, 2024
c2a0770
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 10, 2024
e7c2348
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 10, 2024
3ef2a22
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 10, 2024
db87077
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 10, 2024
f7ecfc6
[#754] Update Natspec
akshay-ap Jul 10, 2024
979b6a4
[#754] Update comments in _checkSignatureLength
akshay-ap Jul 10, 2024
ca9e575
[#754] Update variable name to expectedOffset
akshay-ap Jul 10, 2024
1ec9813
[#754] Add tests
akshay-ap Jul 10, 2024
be6a6f5
Update modules/4337/contracts/Safe4337Module.sol
akshay-ap Jul 11, 2024
2743e53
[#754] Rename function, remove if-else
akshay-ap Jul 11, 2024
025234f
[#754] Update comment on fixed signature part
akshay-ap Jul 11, 2024
c54c89d
[#754] Run tests
akshay-ap Jul 11, 2024
db5fcba
Update modules/4337/test/erc4337/ReferenceEntryPoint.spec.ts
akshay-ap Jul 11, 2024
ab66109
Update modules/4337/test/erc4337/ReferenceEntryPoint.spec.ts
akshay-ap Jul 11, 2024
31798cb
Update modules/4337/test/erc4337/ReferenceEntryPoint.spec.ts
akshay-ap Jul 11, 2024
42b216e
[#754] Update test
akshay-ap Jul 11, 2024
0306c39
[#754] Update tests
akshay-ap Jul 11, 2024
16e7b14
Refactor Length Check Implementation
nlordell Jul 11, 2024
405a5ba
Cleanup Post-Review
nlordell Jul 11, 2024
09c4000
[#754] Lint fix
akshay-ap Jul 12, 2024
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
1 change: 1 addition & 0 deletions modules/4337/certora/specs/Safe4337Module.spec
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ methods {
function getOperationHash(
Safe4337Module.PackedUserOperation userOp
) external returns(bytes32) envfree => PER_CALLEE_CONSTANT;
function _checkSignaturesLength(bytes calldata, uint256) internal returns(bool) => ALWAYS(true);
}
persistent ghost ERC2771MessageSender() returns address;

Expand Down
1 change: 1 addition & 0 deletions modules/4337/certora/specs/ValidationDataLastBitOne.spec
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ methods {
function getOperationHash(
Safe4337Module.PackedUserOperation userOp
) external returns(bytes32) envfree => PER_CALLEE_CONSTANT;
function _checkSignaturesLength(bytes calldata, uint256) internal returns(bool) => ALWAYS(true);
}

rule validationDataLastBitOneIfCheckSignaturesFails(address sender,
Expand Down
63 changes: 58 additions & 5 deletions modules/4337/contracts/Safe4337Module.sol
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,53 @@ contract Safe4337Module is IAccount, HandlerContext, CompatibilityFallbackHandle
operationHash = keccak256(operationData);
}

/**
* @dev Checks if the signatures length is correct and does not contain additional bytes. The function does not
* check the integrity of the signature encoding, as this is expected to be checked by the {Safe} implementation
* of {checkSignatures}.
* @param signatures signatures data
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
* @param threshold Indicates the number of iterations to perform in the loop.
* @return isValid True if length check passes, false otherwise.
*/
function _checkSignaturesLength(bytes calldata signatures, uint256 threshold) internal pure returns (bool isValid) {
uint256 maxLength = threshold * 0x41;

// Make sure that `signatures` bytes are at least as long as the static part of the signatures for the specified
// threshold (i.e. we have at least 65 bytes per signer). This avoids out-of-bound access reverts when decoding
// the signature in order to adhere to the ERC-4337 specification.
if (signatures.length < maxLength) {
return false;
}

for (uint256 i = 0; i < threshold; i++) {
// Each signature is 0x41 (65) bytes long, where fixed part of a Safe contract signature is encoded as:
// {32-bytes signature verifier}{32-bytes dynamic data position}{1-byte signature type}
// and the dynamic part is encoded as:
// {32-bytes signature length}{bytes signature data}
//
// For each signature we check whether or not the signature is a contract signature (signature type of 0).
// If it is, we need to read the length of the contract signature bytes from the signature data, and add it
// to the maximum signatures length.
//
// In order to keep the implementation simpler, and unlike in the length check above, we intentionally
// revert here on out-of-bound bytes array access as well as arithmetic overflow, as you would have to
// **intentionally** build invalid `signatures` data to trigger these conditions. Furthermore, there are no
// security issues associated with reverting in these cases, just not optimally following the ERC-4337
// standard (specifically: "SHOULD return `SIG_VALIDATION_FAILED` (and not revert) on signature mismatch").

uint256 signaturePos = i * 0x41;
uint8 signatureType = uint8(signatures[signaturePos + 0x40]);

if (signatureType == 0) {
uint256 signatureOffset = uint256(bytes32(signatures[signaturePos + 0x20:]));
uint256 signatureLength = uint256(bytes32(signatures[signatureOffset:]));
maxLength += 0x20 + signatureLength;
}
}

isValid = signatures.length <= maxLength;
}

/**
* @dev Validates that the user operation is correctly signed and returns an ERC-4337 packed validation data
* of `validAfter || validUntil || authorizer`:
Expand All @@ -222,12 +269,18 @@ contract Safe4337Module is IAccount, HandlerContext, CompatibilityFallbackHandle
*/
function _validateSignatures(PackedUserOperation calldata userOp) internal view returns (uint256 validationData) {
(bytes memory operationData, uint48 validAfter, uint48 validUntil, bytes calldata signatures) = _getSafeOp(userOp);
try ISafe(payable(userOp.sender)).checkSignatures(keccak256(operationData), operationData, signatures) {
// The timestamps are validated by the entry point, therefore we will not check them again
validationData = _packValidationData(false, validUntil, validAfter);
} catch {
validationData = _packValidationData(true, validUntil, validAfter);

// The `checkSignatures` function in the Safe contract does not force a fixed size on signature length.
// A malicious bundler can pad the Safe operation `signatures` with additional bytes, causing the account to pay more gas than needed for user operation validation (capped by `verificationGasLimit`).
// `_checkSignaturesLength` ensures that there are no additional bytes in the `signature` than are required.
bool validSignature = _checkSignaturesLength(signatures, ISafe(payable(userOp.sender)).getThreshold());

try ISafe(payable(userOp.sender)).checkSignatures(keccak256(operationData), operationData, signatures) {} catch {
validSignature = false;
}

// The timestamps are validated by the entry point, therefore we will not check them again
validationData = _packValidationData(!validSignature, validUntil, validAfter);
}

/**
Expand Down
6 changes: 6 additions & 0 deletions modules/4337/contracts/interfaces/Safe.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,10 @@ interface ISafe {
* @param module Module to be enabled.
*/
function enableModule(address module) external;

/**
* @notice Returns the number of required confirmations for a Safe transaction aka the threshold.
* @return Threshold number.
*/
function getThreshold() external view returns (uint256);
}
4 changes: 4 additions & 0 deletions modules/4337/contracts/test/SafeMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ contract SafeMock {
else (success, returnData) = to.call{value: value}(data);
}

function getThreshold() external pure returns (uint256) {
return 1;
}

// solhint-disable-next-line payable-fallback,no-complex-fallback
fallback() external payable {
// solhint-disable-next-line no-inline-assembly
Expand Down
2 changes: 1 addition & 1 deletion modules/4337/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"test:all": "pnpm run test && npm run test:4337",
"coverage": "hardhat coverage",
"codesize": "hardhat codesize",
"benchmark": "pnpm run test benchmark/*.ts",
"benchmark": "pnpm run test test/gas/*.ts",
"deploy-all": "hardhat deploy-contracts --network",
"deploy": "hardhat deploy --network",
"lint": "pnpm run lint:sol && npm run lint:ts",
Expand Down
151 changes: 149 additions & 2 deletions modules/4337/test/erc4337/ERC4337ModuleNew.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { buildSignatureBytes, logUserOperationGas } from '../../src/utils/execut
import {
buildPackedUserOperationFromSafeUserOperation,
buildSafeUserOpTransaction,
calculateSafeOperationData,
getRequiredPrefund,
signSafeOp,
} from '../../src/utils/userOp'
Expand All @@ -31,15 +32,17 @@ describe('Safe4337Module - Newly deployed safe', () => {
const proxyCreationCode = await proxyFactory.proxyCreationCode()
const safeModuleSetup = await getSafeModuleSetup()
const singleton = await getSafeL2Singleton()
const safe = await Safe4337.withSigner(user1.address, {
const safeGlobalConfig = {
safeSingleton: await singleton.getAddress(),
entryPoint: await entryPoint.getAddress(),
erc4337module: await module.getAddress(),
proxyFactory: await proxyFactory.getAddress(),
safeModuleSetup: await safeModuleSetup.getAddress(),
proxyCreationCode,
chainId: Number(await chainId()),
})
}

const safe = Safe4337.withSigner(user1.address, safeGlobalConfig)

return {
user1,
Expand All @@ -50,6 +53,7 @@ describe('Safe4337Module - Newly deployed safe', () => {
entryPoint,
entryPointSimulations,
relayer,
safeGlobalConfig,
}
})

Expand Down Expand Up @@ -119,6 +123,149 @@ describe('Safe4337Module - Newly deployed safe', () => {
expect(await ethers.provider.getBalance(safe.address)).to.be.eq(ethers.parseEther('0'))
})

it('should revert when signature length contains additional bytes - EOA signature', async () => {
nlordell marked this conversation as resolved.
Show resolved Hide resolved
const { user1, safe, validator, entryPoint } = await setupTests()

await entryPoint.depositTo(safe.address, { value: ethers.parseEther('1.0') })

await user1.sendTransaction({ to: safe.address, value: ethers.parseEther('0.5') })
const safeOp = buildSafeUserOpTransaction(
safe.address,
user1.address,
ethers.parseEther('0.5'),
'0x',
'0',
await entryPoint.getAddress(),
false,
false,
{
initCode: safe.getInitCode(),
},
)

// Add additional byte to the signature to make signature length invalid
const signature = buildSignatureBytes([await signSafeOp(user1, await validator.getAddress(), safeOp, await chainId())]).concat('00')
const userOp = buildPackedUserOperationFromSafeUserOperation({
safeOp,
signature,
})
await expect(entryPoint.handleOps([userOp], user1.address))
.to.be.revertedWithCustomError(entryPoint, 'FailedOp')
.withArgs(0, 'AA24 signature error')
})

it('should revert when signature length contains additional bytes - Smart contract signature', async () => {
const { user1, relayer, safe: parentSafe, validator, entryPoint, safeGlobalConfig } = await setupTests()

await parentSafe.deploy(user1)

const daughterSafe = Safe4337.withSigner(parentSafe.address, safeGlobalConfig)

const accountBalance = ethers.parseEther('1.0')
await user1.sendTransaction({ to: daughterSafe.address, value: accountBalance })
expect(await ethers.provider.getBalance(daughterSafe.address)).to.be.eq(accountBalance)

const safeOp = buildSafeUserOpTransaction(
daughterSafe.address,
user1.address,
ethers.parseEther('0.1'),
'0x',
'0x0',
await entryPoint.getAddress(),
false,
false,
{
initCode: daughterSafe.getInitCode(),
},
)

const opData = calculateSafeOperationData(await validator.getAddress(), safeOp, await chainId())
const signature = buildSignatureBytes([
{
signer: parentSafe.address,
data: await user1.signTypedData(
{
verifyingContract: parentSafe.address,
chainId: await chainId(),
},
{
SafeMessage: [{ type: 'bytes', name: 'message' }],
},
{
message: opData,
},
),
dynamic: true,
},
])
const userOp = buildPackedUserOperationFromSafeUserOperation({
safeOp,
signature: signature.concat('00'), // adding '00' invalidates signature length
})

await expect(entryPoint.handleOps([userOp], await relayer.getAddress()))
.to.be.revertedWithCustomError(entryPoint, 'FailedOp')
.withArgs(0, 'AA24 signature error')
})

it('should revert when signature pointer points to invalid part of signature data - Smart contract signature', async () => {
const { user1, relayer, safe: parentSafe, validator, entryPoint, safeGlobalConfig } = await setupTests()

await parentSafe.deploy(user1)

const daughterSafe = Safe4337.withSigner(parentSafe.address, safeGlobalConfig)

const accountBalance = ethers.parseEther('1.0')
await user1.sendTransaction({ to: daughterSafe.address, value: accountBalance })
expect(await ethers.provider.getBalance(daughterSafe.address)).to.be.eq(accountBalance)

const safeOp = buildSafeUserOpTransaction(
daughterSafe.address,
user1.address,
ethers.parseEther('0.1'),
'0x',
'0x0',
await entryPoint.getAddress(),
false,
false,
{
initCode: daughterSafe.getInitCode(),
},
)

const opData = calculateSafeOperationData(await validator.getAddress(), safeOp, await chainId())
const parentSafeSignature = await user1.signTypedData(
{
verifyingContract: parentSafe.address,
chainId: await chainId(),
},
{
SafeMessage: [{ type: 'bytes', name: 'message' }],
},
{
message: opData,
},
)

// The 2nd word of static part of signature containing invalid value pointing to dynamic part
const signature = ethers.concat([
ethers.zeroPadValue(parentSafe.address, 32), // address of the signer
ethers.toBeHex(0, 32), // offset of the start of the signature
'0x00', // contract signature type
ethers.toBeHex(ethers.dataLength(parentSafeSignature), 32), // length of the dynamic signature
parentSafeSignature,
])

const userOp = buildPackedUserOperationFromSafeUserOperation({
safeOp,
signature,
})

await expect(entryPoint.handleOps([userOp], await relayer.getAddress()))
.to.be.revertedWithCustomError(entryPoint, 'FailedOp')
.withArgs(0, 'AA24 signature error')
})

it('should not be able to execute contract calls twice', async () => {
const { user1, safe, validator, entryPoint } = await setupTests()

Expand Down
Loading
Loading