Skip to content
This repository has been archived by the owner on Jun 27, 2024. It is now read-only.

Commit

Permalink
🔒 Add RecoveryManager
Browse files Browse the repository at this point in the history
  • Loading branch information
ernestognw committed Apr 21, 2024
1 parent 997213d commit 667b1f2
Show file tree
Hide file tree
Showing 19 changed files with 2,215 additions and 325 deletions.
92 changes: 61 additions & 31 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,31 +1,61 @@
PlumaaTest:test_GivenAExpiredRequest(uint48) (runs: 256, μ: 101028, ~: 101028)
PlumaaTest:test_GivenANonSafeOwner(address,bytes,bytes) (runs: 256, μ: 28910, ~: 28884)
PlumaaTest:test_GivenATamperedExecutingData(bytes) (runs: 256, μ: 198003, ~: 197880)
PlumaaTest:test_GivenATamperedExecutingDeadline(uint48) (runs: 256, μ: 196801, ~: 196801)
PlumaaTest:test_GivenATamperedExecutingOperation() (gas: 196195)
PlumaaTest:test_GivenATamperedExecutingSignature(bytes) (runs: 256, μ: 143577, ~: 143369)
PlumaaTest:test_GivenATamperedExecutingTo(address) (runs: 256, μ: 196833, ~: 196833)
PlumaaTest:test_GivenATamperedExecutingValue(uint256) (runs: 256, μ: 196760, ~: 196758)
PlumaaTest:test_GivenATamperedVerifyingData(bytes) (runs: 256, μ: 164060, ~: 163994)
PlumaaTest:test_GivenATamperedVerifyingDeadline(uint48) (runs: 256, μ: 162943, ~: 162943)
PlumaaTest:test_GivenATamperedVerifyingOperation() (gas: 162374)
PlumaaTest:test_GivenATamperedVerifyingSignature(bytes) (runs: 256, μ: 116966, ~: 116858)
PlumaaTest:test_GivenATamperedVerifyingTo(address) (runs: 256, μ: 162987, ~: 162987)
PlumaaTest:test_GivenATamperedVerifyingValue(uint256) (runs: 256, μ: 162888, ~: 162886)
PlumaaTest:test_GivenAValidExecutingOwner(uint256,bytes,uint48,uint32) (runs: 256, μ: 254899, ~: 256461)
PlumaaTest:test_GivenAValidVerifyingOwner(address,uint256,uint48,uint8,bytes,uint32) (runs: 256, μ: 168368, ~: 168438)
PlumaaTest:test_GivenAnInvalidExecutingNonce(uint32) (runs: 256, μ: 196422, ~: 196422)
PlumaaTest:test_GivenAnInvalidExecutingOwner() (gas: 136159)
PlumaaTest:test_GivenAnInvalidVerifyingNonce(uint32) (runs: 256, μ: 162856, ~: 162856)
PlumaaTest:test_GivenAnInvalidVerifyingOwner(address,uint256,uint48,uint8,bytes,uint32) (runs: 256, μ: 102888, ~: 102958)
PlumaaTest:test_GivenTheSafeOwner(bytes,bytes) (runs: 256, μ: 50087, ~: 49912)
PlumaaTest:test_WhenInitialized() (gas: 51200)
RSAOwnerManagerTest:test_GivenASignatureFromANonOwner(bytes) (runs: 256, μ: 77796, ~: 77522)
RSAOwnerManagerTest:test_GivenASignatureFromTheOwner(bytes) (runs: 256, μ: 143419, ~: 143146)
RSAOwnerManagerTest:test_GivenAnInvalidSignature(bytes) (runs: 256, μ: 55418, ~: 55307)
RSAOwnerManagerTest:test_WhenCalling_setOwner(bytes,bytes,address) (runs: 256, μ: 22346, ~: 22271)
RSAOwnerManagerTest:test_WhenCalling_useOwnerNonce(uint32) (runs: 256, μ: 35579, ~: 35579)
RSAOwnerManagerTest:test_WhenInitialized() (gas: 45150)
SafeManagerTests:test_GivenACallFromOtherThanTheSafe(address) (runs: 256, μ: 16460, ~: 16460)
SafeManagerTests:test_GivenACallFromTheSafe() (gas: 27645)
SafeManagerTests:test_WhenInitialized() (gas: 12307)
PlumaaTest:test_GivenAExpiredRequest(uint48) (runs: 257, μ: 73499, ~: 73499)
PlumaaTest:test_GivenANonSafeOwner(address,(bytes,bytes)) (runs: 275, μ: 24020, ~: 24001)
PlumaaTest:test_GivenATamperedExecutingData(bytes) (runs: 275, μ: 191453, ~: 191333)
PlumaaTest:test_GivenATamperedExecutingDeadline(uint48) (runs: 274, μ: 190183, ~: 190183)
PlumaaTest:test_GivenATamperedExecutingOperation() (gas: 189600)
PlumaaTest:test_GivenATamperedExecutingSignature(bytes) (runs: 275, μ: 136963, ~: 136769)
PlumaaTest:test_GivenATamperedExecutingTo(address) (runs: 275, μ: 190259, ~: 190257)
PlumaaTest:test_GivenATamperedExecutingValue(uint256) (runs: 275, μ: 190141, ~: 190139)
PlumaaTest:test_GivenATamperedVerifyingData(bytes) (runs: 275, μ: 160263, ~: 160196)
PlumaaTest:test_GivenATamperedVerifyingDeadline(uint48) (runs: 275, μ: 159116, ~: 159116)
PlumaaTest:test_GivenATamperedVerifyingOperation() (gas: 158505)
PlumaaTest:test_GivenATamperedVerifyingSignature(bytes) (runs: 275, μ: 113168, ~: 113068)
PlumaaTest:test_GivenATamperedVerifyingTo(address) (runs: 275, μ: 159140, ~: 159138)
PlumaaTest:test_GivenATamperedVerifyingValue(uint256) (runs: 275, μ: 159061, ~: 159059)
PlumaaTest:test_GivenAValidExecutingOwner(uint256,bytes,uint48,uint32) (runs: 274, μ: 251099, ~: 252556)
PlumaaTest:test_GivenAValidSignature() (gas: 143353)
PlumaaTest:test_GivenAValidVerifyingOwner(address,uint256,uint48,uint8,bytes,uint32) (runs: 275, μ: 164587, ~: 164651)
PlumaaTest:test_GivenAnInvalidExecutingNonce(uint32) (runs: 275, μ: 189873, ~: 189873)
PlumaaTest:test_GivenAnInvalidExecutingOwner() (gas: 138285)
PlumaaTest:test_GivenAnInvalidSignature() (gas: 131240)
PlumaaTest:test_GivenAnInvalidVerifyingNonce(uint32) (runs: 275, μ: 158962, ~: 158962)
PlumaaTest:test_GivenAnInvalidVerifyingOwner(address,uint256,uint48,uint8,bytes,uint32) (runs: 275, μ: 107884, ~: 107947)
PlumaaTest:test_GivenTheAuthorizerIsNotTheSafe(address,address) (runs: 275, μ: 25978, ~: 25978)
PlumaaTest:test_GivenTheAuthorizerIsTheSafe(address) (runs: 275, μ: 91576, ~: 91906)
PlumaaTest:test_GivenTheCallerIsNotTheSafe(uint256,address[],address) (runs: 275, μ: 288471, ~: 290093)
PlumaaTest:test_GivenTheCallerIsTheSafe(uint256,address[]) (runs: 275, μ: 306925, ~: 287018)
PlumaaTest:test_GivenTheRevokerIsNotTheSafe(address,address) (runs: 275, μ: 52146, ~: 52146)
PlumaaTest:test_GivenTheRevokerIsTheSafe(address) (runs: 275, μ: 63820, ~: 64171)
PlumaaTest:test_GivenTheSafeOwner((bytes,bytes)) (runs: 275, μ: 115231, ~: 102160)
PlumaaTest:test_GivenTheSwapperIsNotTheSafe(address,address,address) (runs: 275, μ: 47907, ~: 47839)
PlumaaTest:test_GivenTheSwapperIsTheSafe(address,address) (runs: 275, μ: 109351, ~: 109212)
PlumaaTest:test_GivenValidSignatures(uint256,string[],(bytes,bytes)) (runs: 274, μ: 1328430, ~: 1330330)
PlumaaTest:test_WhenInitialized() (gas: 87929)
RSAOwnerManagerTest:test_GivenASignatureFromANonOwner(bytes) (runs: 275, μ: 99610, ~: 99352)
RSAOwnerManagerTest:test_GivenASignatureFromTheOwner(bytes) (runs: 275, μ: 165137, ~: 164879)
RSAOwnerManagerTest:test_GivenAnInvalidSignature(bytes) (runs: 275, μ: 77232, ~: 77130)
RSAOwnerManagerTest:test_WhenCalling_setOwner((bytes,bytes),address) (runs: 275, μ: 84142, ~: 73997)
RSAOwnerManagerTest:test_WhenCalling_useOwnerNonce(uint32) (runs: 274, μ: 35527, ~: 35527)
RSAOwnerManagerTest:test_WhenInitialized() (gas: 69897)
RecoveryManagerTest:test_GivenANewThresholdEqualToZero(address[]) (runs: 275, μ: 290726, ~: 290360)
RecoveryManagerTest:test_GivenANewThresholdEqualToZeroAfterAuthorization(address,address[]) (runs: 275, μ: 323424, ~: 301482)
RecoveryManagerTest:test_GivenANewThresholdEqualToZeroAfterRevoking(address,address[]) (runs: 275, μ: 280810, ~: 258984)
RecoveryManagerTest:test_GivenANewThresholdHigherThanTheRecoverersLength(uint256,address[]) (runs: 275, μ: 279012, ~: 277837)
RecoveryManagerTest:test_GivenANewThresholdHigherThanTheRecoverersLengthAfterAuthorization(address,uint256,address[]) (runs: 273, μ: 337862, ~: 350319)
RecoveryManagerTest:test_GivenANewThresholdHigherThanTheRecoverersLengthAfterRevoking(address,uint256,address[]) (runs: 273, μ: 294226, ~: 307633)
RecoveryManagerTest:test_GivenAThresholdHigherThanRecoverersLength(uint256,address[]) (runs: 273, μ: 10581120, ~: 10721633)
RecoveryManagerTest:test_GivenAThresholdSmallerThanTheRecoverersLengthAndNon_zero(uint256,address[]) (runs: 274, μ: 11077045, ~: 11105122)
RecoveryManagerTest:test_GivenAValidNewThreshold(uint256,address[]) (runs: 274, μ: 285844, ~: 279956)
RecoveryManagerTest:test_GivenAValidNewThresholdAfterAuthorization(address,uint256,address[]) (runs: 275, μ: 344627, ~: 356231)
RecoveryManagerTest:test_GivenAValidNewThresholdAfterRevoking(uint256,address,address[]) (runs: 275, μ: 297782, ~: 309598)
RecoveryManagerTest:test_GivenAZeroThreshold(address[]) (runs: 275, μ: 9922226, ~: 9959359)
RecoveryManagerTest:test_GivenDuplicatedSignatures(uint256,string[],(bytes,bytes)) (runs: 274, μ: 1262601, ~: 1256212)
RecoveryManagerTest:test_GivenInvalidSignatures(uint256,string[],(bytes,bytes)) (runs: 274, μ: 1250191, ~: 1251427)
RecoveryManagerTest:test_GivenNoSignatures(string[],(bytes,bytes)) (runs: 275, μ: 1190443, ~: 1208360)
RecoveryManagerTest:test_GivenSignaturesLengthSmallerThanTheThreshold(uint256,string[],(bytes,bytes)) (runs: 274, μ: 1240090, ~: 1241340)
RecoveryManagerTest:test_GivenUnauthorizedSignatures(uint256,string[],(bytes,bytes)) (runs: 274, μ: 1209936, ~: 1210568)
RecoveryManagerTest:test_GivenValidSignatures(uint256,string[],(bytes,bytes)) (runs: 274, μ: 1258267, ~: 1255555)
RecoveryManagerTest:test_WhenCalling_swapRecoverer(address,address) (runs: 275, μ: 87242, ~: 87381)
SafeManagerTests:test_GivenACallFromOtherThanTheSafe(address) (runs: 275, μ: 16445, ~: 16445)
SafeManagerTests:test_GivenACallFromTheSafe() (gas: 27595)
SafeManagerTests:test_WhenInitialized() (gas: 12304)
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ optimizer_runs = 10_000
build_info = true
extra_output = ["storageLayout"]
ffi = true
evm_version = "cancun"

[profile.ci]
fuzz = { runs = 1_000 }
Expand Down
143 changes: 107 additions & 36 deletions src/Plumaa.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ import {Enum} from "@safe/contracts/common/Enum.sol";
import {Safe} from "@safe/contracts/Safe.sol";
import {RSAOwnerManager} from "./base/RSAOwnerManager.sol";
import {SafeManager} from "./base/SafeManager.sol";
import {RecoveryManager} from "./base/RecoveryManager.sol";
import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";

/// @title Plumaa - An RSA SHA256 PKCS1.5 enabler module for Safe{Wallet} Smart Accounts
///
/// It allows the Safe{Wallet} Smart Account to execute transactions signed with an RSA PKCS1.5 signature.
/// A notable example of RSA signatures in real-world applications are the government-issued digital certificates.
contract Plumaa is RSAOwnerManager, SafeManager, EIP712Upgradeable {
contract Plumaa is
RSAOwnerManager,
SafeManager,
EIP712Upgradeable,
RecoveryManager,
IERC1271
{
/// @notice A transaction signed with the Bytes32 owner's private key was executed
event ExecutedRSATransaction(
address indexed safe,
Expand All @@ -20,13 +28,8 @@ contract Plumaa is RSAOwnerManager, SafeManager, EIP712Upgradeable {
bool success
);

/// @notice The provided message SHA256 digest doesn't match signature for exponent and modulus
error InvalidRSASignature(
bytes32 digest,
bytes signature,
bytes exponent,
bytes modulus
);
/// @notice The provided message SHA256 digest doesn't match signature for owner's public key
error InvalidRSASignature(bytes32 digest, bytes signature);

/// @dev The request `deadline` has expired.
error ExpiredRSATransaction(uint48 deadline);
Expand All @@ -43,8 +46,6 @@ contract Plumaa is RSAOwnerManager, SafeManager, EIP712Upgradeable {
uint48 deadline;
bytes data;
bytes signature;
bytes exponent;
bytes modulus;
}

bytes32 internal constant _TRANSACTION_REQUEST_TYPEHASH =
Expand All @@ -58,19 +59,39 @@ contract Plumaa is RSAOwnerManager, SafeManager, EIP712Upgradeable {
}

/// @notice Initializes the contract with an RSA owner
/// @param exponent The exponent of the RSA public key
/// @param modulus The modulus of the RSA public key
/// @param initialOwner The RSA public key of the owner
/// @param safe_ The address of the Safe{Wallet} Smart Account
/// @param recoveryThreshold The threshold for the recovery manager
/// @param authorizedRecoverers The initial authorized recoverers
function setupPlumaa(
bytes memory exponent,
bytes memory modulus,
Safe safe_
RSAOwnerManager.RSAPublicKey calldata initialOwner,
Safe safe_,
uint256 recoveryThreshold,
address[] calldata authorizedRecoverers
) public initializer {
__EIP712_init("RSAOwnerManager", "1");
__RSAOwnerManager_init(exponent, modulus);
__RSAOwnerManager_init(initialOwner);
__SafeManager_init(safe_);
__RecoveryManager_init(recoveryThreshold, authorizedRecoverers);
}

/// @notice Checks if the provided signature is valid for the keccak256 hash.
function isValidSignature(
bytes32 keccak256Hash,
bytes memory signature
) external view returns (bytes4) {
// Most signers don't accept custom digests since it's not a good practice to deal with them directly.
// Therefore, this contract expects sha256 hashes of keccak256 hashes (most likely EVM-produced). This
// is secure assuming sha256 is a good cryptographic hash function.
bytes32 sha256Digest = sha256(abi.encodePacked(keccak256Hash));
return
_verifyRSAOwner(sha256Digest, signature, owner())
? this.isValidSignature.selector
: bytes4(0);
}

/// ===== Execution =====

/// @notice Executes a transaction from the associated Safe{Wallet} Smart Account sing an RSA PKCS1.5 signature
/// @param request The transaction request
function executeTransaction(
Expand All @@ -88,12 +109,7 @@ contract Plumaa is RSAOwnerManager, SafeManager, EIP712Upgradeable {
);

if (!valid) {
revert InvalidRSASignature(
sha256Digest,
request.signature,
request.exponent,
request.modulus
);
revert InvalidRSASignature(sha256Digest, request.signature);
}

Safe _safe = safe();
Expand All @@ -119,7 +135,7 @@ contract Plumaa is RSAOwnerManager, SafeManager, EIP712Upgradeable {
/// @param request The transaction request
/// @param currentNonce The nonce of the RSA owner
/// @return valid True if the transaction request is signed by the RSA owner
/// @return digest The transaction request digest
/// @return digest The transaction request sha256 digest
function verifyRSAOwnerTransactionRequest(
TransactionRequestData calldata request,
uint32 currentNonce
Expand All @@ -138,27 +154,82 @@ contract Plumaa is RSAOwnerManager, SafeManager, EIP712Upgradeable {
)
);

// Hashing again is required to be PKCS8 compliant
// EIP-712 defines typehash as a keccak256. However, PKCS1.5 requires a SHA256 digest.
// Assuming SHA256 is a good hash function, the user would sign the sha256(keccak256(typehash)).
bytes32 sha256Digest = sha256(abi.encodePacked(typehash));

return (
_verifyRSAOwner(
sha256Digest,
request.signature,
request.exponent,
request.modulus
),
_verifyRSAOwner(sha256Digest, request.signature, owner()),
sha256Digest
);
}

/// @notice Sets the RSA Owner
/// @param exponent The exponent of the RSA public key
/// @param modulus The modulus of the RSA public key
/// ===== Recovery =====

/// @notice Sets a new RSA Public Key Owner
function setOwner(
bytes memory exponent,
bytes memory modulus
RSAOwnerManager.RSAPublicKey calldata publicKey
) public onlySafe {
_setOwner(publicKey);
}

/// @notice Authorizes a recoverer to sign for the recovery of the Safe{Wallet} Smart Account
///
/// Requirements:
/// - The new threshold must be greater than 0
/// - The new threshold must be less than or equal to the total number of recoverers after the recoverer is added
function authorizeRecoverer(
address recoverer,
uint256 newThreshold
) public onlySafe {
_authorizeRecoverer(recoverer, newThreshold);
}

/// @notice Revokes the authorization of a recoverer to sign for the recovery of the Safe{Wallet} Smart Account.
///
/// Requirements:
/// - The new threshold must be greater than 0
/// - The new threshold must be less than or equal to the total number of recoverers after the recoverer is removed
function revokeRecoverer(
address recoverer,
uint256 newThreshold
) public onlySafe {
_revokeRecoverer(recoverer, newThreshold);
}

/// @notice Swaps an authorized recoverer address.
function swapRecoverer(
address oldRecoverer,
address newRecoverer
) public onlySafe {
_setOwner(exponent, modulus);
_swapRecoverer(oldRecoverer, newRecoverer);
}

/// @notice Changes the threshold of the recovery manager.
///
/// Requirements:
/// - The new threshold must be greater than 0
/// - The new threshold must be less than or equal to the total number of recoverers
function changeThreshold(uint256 newThreshold) public onlySafe {
_changeThreshold(newThreshold);
}

/// @notice Changes the owner of the Plumaa module controlling the Safe{Wallet} Smart Account
/// @param signers The authorized recovers that produced the signatures
/// @param signatures The signatures of the authorized recoverers
/// @param publicKey The new owner RSA public key
///
/// Requirements:
/// - The signatures must come from authorized recoverers.
/// - The number of signatures must be greater than or equal to the threshold.
/// - The signatures must be from different recoverers.
/// - Recoverers and signatures must have a one-to-one correspondence.
function recover(
address[] calldata signers,
bytes[] calldata signatures,
RSAOwnerManager.RSAPublicKey calldata publicKey
) public override {
_validateRecovery(signers, signatures, publicKey);
_setOwner(publicKey);
}
}
Loading

0 comments on commit 667b1f2

Please sign in to comment.