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 28 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 _checkSignatureLength(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 _checkSignatureLength(bytes calldata, uint256) internal returns(bool) => ALWAYS(true);
}

rule validationDataLastBitOneIfCheckSignaturesFails(address sender,
Expand Down
51 changes: 49 additions & 2 deletions modules/4337/contracts/Safe4337Module.sol
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,43 @@ 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.
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
* @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 result True if length check passes, false otherwise.
*/
function _checkSignatureLength(bytes calldata signatures, uint256 threshold) internal pure returns (bool) {
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
uint256 offset = threshold * 0x41;
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved

if (signatures.length < offset) return false;
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
bool pointsAtEnd = true;
for (uint256 i = 0; i < threshold; i++) {
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
/* solhint-disable no-inline-assembly */
/// @solidity memory-safe-assembly
assembly {
let signaturePos := mul(0x41, i)
// read the Safe signature type (uint8) from calldata.
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
let signatureType := byte(0, calldataload(add(signatures.offset, add(signaturePos, 0x40))))

// signatureType = 0 indicates that signature is a smart contract signature in Safe Signature Encoding
if iszero(signatureType) {
// For Safe smart contract signature the s value points to the start of the signature data in the signatures
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
// s value is relative to the signature data.
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
let signatureStartPointer := calldataload(add(signatures.offset, add(signaturePos, 0x20)))
pointsAtEnd := eq(signatureStartPointer, offset)
let contractSignatureLen := calldataload(add(signatures.offset, signatureStartPointer))
// Update the required position of the next offset.
offset := add(add(signatureStartPointer, 0x20), contractSignatureLen)
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
}
}
/* solhint-enable no-inline-assembly */
// If the signature data pointer is not pointing to the expected location, return false.
if (!pointsAtEnd) return false;
}
return signatures.length <= offset;
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @dev Validates that the user operation is correctly signed and returns an ERC-4337 packed validation data
* of `validAfter || validUntil || authorizer`:
Expand All @@ -222,10 +259,20 @@ 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) {

// checkSignatures function in Safe contract does not force a fixed size on signature length.
// A malicious actor can append additional bytes in the signature than needed and force gas usage to reach verificationGasLimit.
// _checkSignatureLength ensures that there are no additional bytes in the signatures than required.
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
bool validSignature = _checkSignatureLength(signatures, ISafe(payable(userOp.sender)).getThreshold());

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

if (validSignature) {
// The timestamps are validated by the entry point, therefore we will not check them again
validationData = _packValidationData(false, validUntil, validAfter);
} catch {
} else {
validationData = _packValidationData(true, validUntil, validAfter);
}
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
}
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
93 changes: 91 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,91 @@ 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 not be able to execute contract calls twice', async () => {
const { user1, safe, validator, entryPoint } = await setupTests()

Expand Down
90 changes: 88 additions & 2 deletions modules/4337/test/erc4337/ReferenceEntryPoint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('Safe4337Module - Reference EntryPoint', () => {
proxyCreationCode,
chainId: Number(await chainId()),
}
const safe = await Safe4337.withSigner(user.address, safeGlobalConfig)
const safe = Safe4337.withSigner(user.address, safeGlobalConfig)

return {
user,
Expand Down Expand Up @@ -156,7 +156,7 @@ describe('Safe4337Module - Reference EntryPoint', () => {
const { user, relayer, safe: parentSafe, validator, entryPoint, safeGlobalConfig } = await setupTests()

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

const accountBalance = ethers.parseEther('1.0')
await user.sendTransaction({ to: daughterSafe.address, value: accountBalance })
Expand Down Expand Up @@ -215,6 +215,92 @@ describe('Safe4337Module - Reference EntryPoint', () => {
expect(await ethers.provider.getBalance(daughterSafe.address)).to.be.eq(accountBalance - transfer - deposits)
})

it('should revert on invalid signature length - EOA signature', async () => {
const { user, relayer, safe, validator, entryPoint } = await setupTests()

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

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

const signature = buildSignatureBytes([await signSafeOp(user, await validator.getAddress(), safeOp, await chainId())])
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 on invalid signature length - Smart contract signature (NOTE: would require a staked paymaster for ERC-4337)', async () => {
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
const { user, relayer, safe: parentSafe, validator, entryPoint, safeGlobalConfig } = await setupTests()

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

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

const transfer = ethers.parseEther('0.1')
const safeOp = buildSafeUserOpTransaction(
daughterSafe.address,
user.address,
transfer,
'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 user.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')
})

function isEventLog(log: Log): log is EventLog {
return typeof (log as Partial<EventLog>).eventName === 'string'
}
Expand Down
34 changes: 34 additions & 0 deletions modules/4337/test/erc4337/Safe4337Module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,40 @@ describe('Safe4337Module', () => {

expect(await safeFromEntryPoint.validateUserOp.staticCall(userOp, ethers.ZeroHash, 0)).to.eq(packedValidationData)
})

it('should indicate failed validation data when signature length contains additional bytes', async () => {
const { user, safeModule, validator, entryPoint } = await setupTests()

const validAfter = BigInt(ethers.hexlify(ethers.randomBytes(3)))
const validUntil = validAfter + BigInt(ethers.hexlify(ethers.randomBytes(3)))

const safeOp = buildSafeUserOpTransaction(
await safeModule.getAddress(),
user.address,
0,
'0x',
'0',
await entryPoint.getAddress(),
false,
false,
{
validAfter,
validUntil,
},
)

const safeOpHash = calculateSafeOperationHash(await validator.getAddress(), safeOp, await chainId())
const signature = buildSignatureBytes([await signHash(user, safeOpHash)]).concat('00')
const userOp = buildPackedUserOperationFromSafeUserOperation({
safeOp,
signature,
})
const packedValidationData = packValidationData(1, validUntil, validAfter)
const entryPointImpersonator = await ethers.getSigner(await entryPoint.getAddress())
const safeFromEntryPoint = safeModule.connect(entryPointImpersonator)

expect(await safeFromEntryPoint.validateUserOp.staticCall(userOp, ethers.ZeroHash, 0)).to.eq(packedValidationData)
})
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
})

describe('execUserOp', () => {
Expand Down
Loading