Skip to content
Merged
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
7 changes: 7 additions & 0 deletions src/extensions/passkeys/Passkeys.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ contract Passkeys is ISapientCompact {
WebAuthn.WebAuthnAuth _webAuthnAuth, bool _requireUserVerification, bytes32 _x, bytes32 _y
);

/// @notice Error thrown when the signature length is invalid
error InvalidSignatureLength();

function _rootForPasskey(
bool _requireUserVerification,
bytes32 _x,
Expand Down Expand Up @@ -96,6 +99,10 @@ contract Passkeys is ISapientCompact {

(_x, pointer) = LibBytes.readBytes32(_signature, pointer);
(_y, pointer) = LibBytes.readBytes32(_signature, pointer);

if (pointer != _signature.length) {
revert InvalidSignatureLength();
}
} else {
(_webAuthnAuth, _requireUserVerification, _x, _y, _metadata) =
abi.decode(_signature[1:], (WebAuthn.WebAuthnAuth, bool, bytes32, bytes32, bytes32));
Expand Down
2 changes: 2 additions & 0 deletions src/extensions/sessions/SessionErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ library SessionErrors {
error InvalidValue();
/// @notice Invalid node type in session configuration
error InvalidNodeType(uint256 flag);
/// @notice Error thrown when the signature length is invalid
error InvalidSignatureLength();
/// @notice Error thrown when the payload kind is invalid
error InvalidPayloadKind();
/// @notice Error thrown when the calls length is invalid
Expand Down
5 changes: 5 additions & 0 deletions src/extensions/sessions/SessionSig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ library SessionSig {
}
}

if (pointer != encodedSignature.length) {
// Invalid signature length
revert SessionErrors.InvalidSignatureLength();
}

return sig;
}

Expand Down
9 changes: 9 additions & 0 deletions src/modules/Payload.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ library Payload {
/// @notice Error thrown when the kind is invalid
error InvalidKind(uint8 kind);

/// @notice Error thrown when the encoding is invalid
error InvalidPackedLength();

/// @dev keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
bytes32 private constant EIP712_DOMAIN_TYPEHASH = 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;

Expand Down Expand Up @@ -212,6 +215,12 @@ library Payload {
// Last 2 bits are directly mapped to the behavior on error
_decoded.calls[i].behaviorOnError = (flags & 0xC0) >> 6;
}

if (pointer != packed.length) {
revert InvalidPackedLength();
}

return _decoded;
}

function hashCall(
Expand Down
67 changes: 67 additions & 0 deletions test/extensions/passkeys/Passkeys.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,73 @@ contract PasskeysTest is AdvTest {
assertEq(vars.recoveredRoot, vars.expectedRoot, "Recovered root should match expected root");
}

function test_recoverSapientSignatureCompact_paddedFails(RecoverParams memory params, bytes memory padding) public {
vm.assume(padding.length > 0);
vm.etch(P256_VERIFIER, P256_VERIFIER_RUNTIME_CODE);

recoverSapientSignatureCompact_valid_vars memory vars;

params.pkSeed = boundP256Pk(params.pkSeed);
vars.wallet = vm.createWallet(params.pkSeed);

if (params.embedMetadata) {
vm.assume(params.metadataHash != bytes32(0));
} else {
params.metadataHash = bytes32(0);
}

(vars.pubX, vars.pubY) = vm.publicKeyP256(vars.wallet.privateKey);
vars.pkParams.x = bytes32(vars.pubX);
vars.pkParams.y = bytes32(vars.pubY);
vars.pkParams.requireUserVerification = params.requireUserVerification;
vars.pkParams.metadataHash = params.metadataHash;
vars.pkParams.credentialId = "";

uint8 flags = 0x01;
if (params.requireUserVerification) {
flags |= 0x04;
}

vars.generatedAuthenticatorData = abi.encodePacked(params.rpIdHash, flags, params.signCount);

string memory raw = vm.toBase64URL(abi.encodePacked(params.digest));
if (bytes(raw)[bytes(raw).length - 1] == "=") {
assembly {
mstore(raw, sub(mload(raw), 1))
}
}
vars.base64UrlChallenge = raw;
vars.typeValue = "webauthn.get";
vars.originValue = generateRandomString(params.originValueSeed);
vm.assume(bytes(vars.originValue).length > 0);

vars.clientDataJSON = string.concat(
'{"type":"', vars.typeValue, '","challenge":"', vars.base64UrlChallenge, '","origin":"', vars.originValue, '"}'
);
vars.sigParams.clientDataJson = vars.clientDataJSON;
vars.sigParams.authenticatorData = vars.generatedAuthenticatorData;

vars.clientDataJSONHash = sha256(bytes(vars.clientDataJSON));
vars.messageHash = sha256(abi.encodePacked(vars.generatedAuthenticatorData, vars.clientDataJSONHash));
(bytes32 rVal, bytes32 sVal) = vm.signP256(vars.wallet.privateKey, vars.messageHash);

bytes32 halfN = bytes32(0x7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8);
if (sVal > halfN) {
sVal = bytes32(0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 - uint256(sVal));
}

vars.sigParams.r = bytes32(rVal);
vars.sigParams.s = bytes32(sVal);
vars.encodedSignature =
PrimitivesRPC.passkeysEncodeSignature(vm, vars.pkParams, vars.sigParams, params.embedMetadata);

vars.expectedRoot = PrimitivesRPC.passkeysComputeRoot(vm, vars.pkParams);

bytes memory paddedSignature = abi.encodePacked(vars.encodedSignature, padding);
vm.expectRevert(abi.encodeWithSelector(Passkeys.InvalidSignatureLength.selector));
passkeysImp.recoverSapientSignatureCompact(params.digest, paddedSignature);
}

struct recoverSapientSignatureCompact_invalidSignature_vars {
Vm.Wallet wallet;
Vm.Wallet wrongWallet;
Expand Down
76 changes: 76 additions & 0 deletions test/extensions/recovery/Recovery.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,82 @@ contract RecoveryTest is AdvTest {
assertEq(vars.recoveredRoot, vars.rpcRoot);
}

function test_fail_padded_signature(
test_recover_sapient_signature_compact_params memory params,
bytes memory padding
) external {
vm.assume(padding.length > 0);

boundToLegalPayload(params.payload);
params.startTime = bound(params.startTime, 1, type(uint64).max / 2); // Safer upper bound perhaps
params.requiredDeltaTime = bound(params.requiredDeltaTime, 0, type(uint24).max);
params.minTimestamp = bound(params.minTimestamp, 0, params.startTime);
uint256 minPassedTime = params.requiredDeltaTime == type(uint64).max ? type(uint64).max : params.requiredDeltaTime;

params.passedTime = bound(params.passedTime, minPassedTime, type(uint64).max);
vm.warp(params.startTime);

params.signerPk = boundPk(params.signerPk);

test_recover_sapient_signature_compact_vars memory vars;
vars.recoveryPayloadHash = recovery.recoveryPayloadHash(params.wallet, params.payload);
vars.payloadHash = Payload.hashFor(params.payload, params.wallet);
(vars.v, vars.r, vars.s) = vm.sign(params.signerPk, vars.recoveryPayloadHash);

vars.yParityAndS = bytes32((uint256(vars.v - 27) << 255) | uint256(vars.s));
vars.signature = abi.encodePacked(vars.r, vars.yParityAndS);

vars.signerAddr = vm.addr(params.signerPk);

recovery.queuePayload(params.wallet, vars.signerAddr, params.payload, vars.signature);

vm.warp(block.timestamp + params.passedTime);

vars.parts = "";
for (uint256 i = 0; i < params.suffixes.length; i++) {
vars.parts = string.concat(
vars.parts,
"signer:",
vm.toString(params.suffixes[i].signer),
":",
vm.toString(params.suffixes[i].requiredDeltaTime),
":",
vm.toString(params.suffixes[i].minTimestamp),
" "
);
}

vars.parts = string.concat(
vars.parts,
"signer:",
vm.toString(vars.signerAddr),
":",
vm.toString(params.requiredDeltaTime),
":",
vm.toString(params.minTimestamp)
);

for (uint256 i = 0; i < params.prefixes.length; i++) {
vars.parts = string.concat(
vars.parts,
" signer:",
vm.toString(params.prefixes[i].signer),
":",
vm.toString(params.prefixes[i].requiredDeltaTime),
":",
vm.toString(params.prefixes[i].minTimestamp)
);
}

vars.rpcRoot = PrimitivesRPC.recoveryHashFromLeaves(vm, vars.parts);

vars.encoded = PrimitivesRPC.recoveryTrim(vm, vars.parts, vars.signerAddr);
bytes memory signature = abi.encodePacked(vars.encoded, padding);
vm.prank(params.wallet);
vm.expectRevert(); // Unspecified revert
vars.recoveredRoot = recovery.recoverSapientSignatureCompact(vars.payloadHash, signature);
}

function test_recover_sapient_signature_compact_fail_minTimestamp(
test_recover_sapient_signature_compact_params memory params
) external {
Expand Down
94 changes: 82 additions & 12 deletions test/extensions/sessions/SessionManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,15 @@ contract SessionManagerTest is SessionTestBase {
emitter = new Emitter();
}

/// @notice Valid explicit session test.
function testValidExplicitSessionSignature(
function _prepareTestValidExplicitSessionSignature(
address wallet,
bytes4 selector,
uint256 param,
uint256 value,
address explicitTarget,
address explicitTarget2,
bool useChainId
) public {
) public returns (bytes32 imageHash, Payload.Decoded memory payload, bytes memory encodedSig) {
vm.assume(explicitTarget != explicitTarget2);
vm.assume(value > 0);
vm.assume(param > 0);
Expand Down Expand Up @@ -92,7 +91,7 @@ contract SessionManagerTest is SessionTestBase {
// Call 1: call not requiring incrementUsageLimit
// Call 2: call requiring incrementUsageLimit
// Call 0: the required incrementUsageLimit call (self–call)
Payload.Decoded memory payload = _buildPayload(3);
payload = _buildPayload(3);

// --- Explicit Call 1 ---
payload.calls[1] = Payload.Call({
Expand Down Expand Up @@ -141,22 +140,61 @@ contract SessionManagerTest is SessionTestBase {
permissionIdxs[1] = 0; // Call 1
permissionIdxs[2] = 1; // Call 2

(bytes32 imageHash, bytes memory encodedSig) =
_validExplicitSessionSignature(wallet, payload, sessionPerms, permissionIdxs);
(imageHash, encodedSig) = _validExplicitSessionSignature(wallet, payload, sessionPerms, permissionIdxs);
}

/// @notice Valid explicit session test.
function testValidExplicitSessionSignature(
address wallet,
bytes4 selector,
uint256 param,
uint256 value,
address explicitTarget,
address explicitTarget2,
bool useChainId
) public {
(bytes32 imageHash, Payload.Decoded memory payload, bytes memory encodedSig) =
_prepareTestValidExplicitSessionSignature(
wallet, selector, param, value, explicitTarget, explicitTarget2, useChainId
);

vm.prank(wallet);
bytes32 actualImageHash = sessionManager.recoverSapientSignature(payload, encodedSig);
assertEq(imageHash, actualImageHash);
}

/// @notice Valid explicit session test with multiple signers.
function testValidExplicitSessionMixing(
/// @notice Valid explicit session test, fails when padding added.
function testValidExplicitSessionSignature_padded_fails(
address wallet,
bytes4 selector,
uint256 param,
uint256 value,
address explicitTarget,
bool useChainId
address explicitTarget2,
bool useChainId,
bytes memory padding
) public {
vm.assume(padding.length > 0);

(bytes32 imageHash, Payload.Decoded memory payload, bytes memory encodedSig) =
_prepareTestValidExplicitSessionSignature(
wallet, selector, param, value, explicitTarget, explicitTarget2, useChainId
);

bytes memory paddedSignature = abi.encodePacked(encodedSig, padding);

vm.prank(wallet);
vm.expectRevert(abi.encodeWithSelector(SessionErrors.InvalidSignatureLength.selector));
sessionManager.recoverSapientSignature(payload, paddedSignature);
}

function _prepareTestValidExplicitSessionMixing(
address wallet,
bytes4 selector,
uint256 param,
address explicitTarget,
bool useChainId
) internal returns (bytes32 imageHash, Payload.Decoded memory payload, bytes memory encodedSig) {
Vm.Wallet memory sessionWallet2 = vm.createWallet("session2");
vm.assume(param > 0);
vm.assume(explicitTarget != wallet);
Expand Down Expand Up @@ -225,7 +263,7 @@ contract SessionManagerTest is SessionTestBase {
// Call 1: using session 1 (non cumulative signer)
// Call 2: using session 2 (cumulative signer)
// Call 0: the required incrementUsageLimit call (self–call)
Payload.Decoded memory payload = _buildPayload(3);
payload = _buildPayload(3);

// --- Explicit Call 1 ---
payload.calls[1] = Payload.Call({
Expand Down Expand Up @@ -266,7 +304,7 @@ contract SessionManagerTest is SessionTestBase {
topology = PrimitivesRPC.sessionExplicitAdd(vm, sessionPermsJson, topology);
sessionPermsJson = _sessionPermissionsToJSON(sessionPerms2);
topology = PrimitivesRPC.sessionExplicitAdd(vm, sessionPermsJson, topology);
bytes32 imageHash = PrimitivesRPC.sessionImageHash(vm, topology);
imageHash = PrimitivesRPC.sessionImageHash(vm, topology);

string[] memory callSignatures = new string[](3);
// Sign call 1 with signer 1
Expand All @@ -284,14 +322,46 @@ contract SessionManagerTest is SessionTestBase {
explicitSigners[0] = sessionWallet.addr;
explicitSigners[1] = sessionWallet2.addr;
address[] memory implicitSigners = new address[](0);
bytes memory encodedSig =
encodedSig =
PrimitivesRPC.sessionEncodeCallSignatures(vm, topology, callSignatures, explicitSigners, implicitSigners);
}

/// @notice Valid explicit session test with multiple signers.
function testValidExplicitSessionMixing(
address wallet,
bytes4 selector,
uint256 param,
address explicitTarget,
bool useChainId
) public {
(bytes32 imageHash, Payload.Decoded memory payload, bytes memory encodedSig) =
_prepareTestValidExplicitSessionMixing(wallet, selector, param, explicitTarget, useChainId);

vm.prank(wallet);
bytes32 actualImageHash = sessionManager.recoverSapientSignature(payload, encodedSig);
assertEq(imageHash, actualImageHash);
}

/// @notice Valid explicit session test with multiple signers fails when padding added.
function testValidExplicitSessionMixing_padded_fails(
address wallet,
bytes4 selector,
uint256 param,
address explicitTarget,
bool useChainId,
bytes memory padding
) public {
vm.assume(padding.length > 0);
(bytes32 imageHash, Payload.Decoded memory payload, bytes memory encodedSig) =
_prepareTestValidExplicitSessionMixing(wallet, selector, param, explicitTarget, useChainId);

bytes memory paddedSignature = abi.encodePacked(encodedSig, padding);

vm.prank(wallet);
vm.expectRevert(abi.encodeWithSelector(SessionErrors.InvalidSignatureLength.selector));
sessionManager.recoverSapientSignature(payload, paddedSignature);
}

function testIncrementReentrancy() external {
MockERC20 token = new MockERC20();
CanReenter canReenter = new CanReenter();
Expand Down
Loading