diff --git a/.gitignore b/.gitignore index 85198aa..55bdebd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ docs/ # Dotenv file .env + +# Key files +private.pem +public.pem \ No newline at end of file diff --git a/README.md b/README.md index abc7c17..845593a 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,63 @@ contract Secp256r1Example { } ``` +## Account controlled by a P256 key + +With EIP-7702 and EIP-7212 it is possible to delegate control over an EOA to a P256 key. This has large potential for UX improvement as P256 keys are adopted by commonly used protocols like [Apple Secure Enclave] and [WebAuthn]. + +We are demonstrating a simple implementation of an account that can be controlled by a P256 key. EOAs can delegate to this contract and configure an authorized P256 key, which can then be used to perform actions on behalf of the EOA. + +To run the commands below, you will need to have [Python] and `openssl` CLI tool installed. + +1. Run anvil in Alphanet mode to enable support for EIP-7702 and P256 precompile: +```shell +anvil --alphanet +``` +We will be using dev account with address `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` and private key `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`. + +2. Generate a P256 private and public key pair: +```shell +python examples/p256.py gen +``` + +This command will generate a private and public key pair, save them to `private.pem` and `public.pem` respectively, and print the public key in hex format. + +3. Deploy [P256Delegation](src/P256Delegation.sol) contract which we will be delegating to: +```shell +forge create P256Delegation --private-key "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" --rpc-url "http://127.0.0.1:8545" +``` + +4. Configure delegation contract: +Send EIP-7702 transaction, delegating to our newly deployed contract. +This transaction will both authorize the delegation and set it to use our P256 public key: +```shell +cast send 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 'authorize(uint256,uint256)' '' '' --auth "
" --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` +Note that we are transacting with our EOA account which already includes the updated code. + +Verify that new code at our EOA account contains the [delegation designation]: +```shell +$ cast code 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +0xef0100... +``` + +5. After that, you should be able to transact on behalf of the EOA account by using the `transact` function of the delegation contract. +Let's generate a signature for sending 1 ether to zero address by using our P256 private key: +```shell +python examples/p256.py sign $(cast abi-encode 'f(uint256,address,bytes,uint256)' $(cast call 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 'nonce()(uint256)') '0x0000000000000000000000000000000000000000' '0x' '1000000000000000000') +``` + +Note that it uses `cast call` to get internal nonce of our EOA used to protect against replay attacks. +It also abi-encodes the payload expected by the `P256Delegation` contract, and passes it to our Python script to sign with openssl. + +Command output will contain the signature r and s values, which we then should pass to the `transact` function of the delegation contract: +```shell +cast send 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 'transact(address to,bytes data,uint256 value,bytes32 r,bytes32 s)' '0x0000000000000000000000000000000000000000' '0x' '1000000000000000000' '' '' --private-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d +``` + +Note that we are using a different private key here, this transaction can be sent by anyone as it was authorized by our P256 key. + + [AlphaNet]: https://github.com/paradigmxyz/alphanet [EOF]: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3540.md [forge-eof]: https://github.com/paradigmxyz/forge-eof @@ -74,3 +131,7 @@ contract Secp256r1Example { [EIP-7212]: https://eips.ethereum.org/EIPS/eip-7212 [EIP-3074]: https://eips.ethereum.org/EIPS/eip-3074 [foundry-alphanet]: https://github.com/paradigmxyz/foundry-alphanet +[Apple Secure Enclave]: https://support.apple.com/guide/security/secure-enclave-sec59b0b31ff/web +[WebAuthn]: https://webauthn.io/ +[Python]: https://www.python.org/ +[delegation designation]: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md#delegation-designation \ No newline at end of file diff --git a/examples/p256.py b/examples/p256.py new file mode 100644 index 0000000..b7a106d --- /dev/null +++ b/examples/p256.py @@ -0,0 +1,53 @@ +import sys +import subprocess + +args = sys.argv[1:] +if args[0] == "gen": + subprocess.run( + "openssl ecparam -name prime256v1 -genkey -noout -out private.pem", + shell=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + subprocess.run( + "openssl ec -in private.pem -pubout -out public.pem", + shell=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + data = ( + subprocess.check_output( + "openssl ec -in private.pem -text", shell=True, stderr=subprocess.DEVNULL + ) + .decode() + .rstrip() + .replace("\n", "") + .replace(" ", "") + ) + priv = data.split("priv:")[1].split("pub:")[0].replace(":", "") + pub = data.split("pub:")[1].split("ASN1")[0].replace(":", "")[2:] + pub_x, pub_y = pub[:64], pub[64:] + + print(f"Private key: 0x{priv}") + print(f"Public key: 0x{pub_x}, 0x{pub_y}") +elif args[0] == "sign": + payload = bytearray.fromhex(args[1].replace("0x", "")) + + proc = subprocess.Popen( + ["openssl", "dgst", "-keccak-256", "-sign", "private.pem"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + output = proc.communicate(payload)[0] + proc = subprocess.Popen( + ["openssl", "asn1parse", "-inform", "DER"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + output = proc.communicate(output)[0].decode().replace(" ", "").replace("\n", "") + + sig_r = output.split("INTEGER:")[1][:64] + sig_s = output.split("INTEGER:")[2][:64] + + print(f"Signature r: 0x{sig_r}") + print(f"Signature s: 0x{sig_s}") diff --git a/src/P256Delegation.sol b/src/P256Delegation.sol new file mode 100644 index 0000000..fdc2d56 --- /dev/null +++ b/src/P256Delegation.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Secp256r1} from "./sign/Secp256r1.sol"; + +/// @notice Contract designed for being delegated to by EOAs to authorize a secp256r1 key to transact on their behalf. +contract P256Delegation { + /// @notice The x coordinate of the authorized public key + uint256 authorizedPublicKeyX; + /// @notice The y coordinate of the authorized public key + uint256 authorizedPublicKeyY; + + /// @notice Internal nonce used for replay protection, must be tracked and included into prehashed message. + uint256 public nonce; + + /// @notice Authorizes provided public key to transact on behalf of this account. Only callable by EOA itself. + function authorize(uint256 publicKeyX, uint256 publicKeyY) public { + require(msg.sender == address(this)); + + authorizedPublicKeyX = publicKeyX; + authorizedPublicKeyY = publicKeyY; + } + + /// @notice Main entrypoint for authorized transactions. Accepts transaction parameters (to, data, value) and a secp256r1 signature. + function transact(address to, bytes memory data, uint256 value, bytes32 r, bytes32 s) public { + bytes32 digest = keccak256(abi.encode(nonce++, to, data, value)); + require(Secp256r1.verify(digest, r, s, authorizedPublicKeyX, authorizedPublicKeyY), "Invalid signature"); + + (bool success,) = to.call{value: value}(data); + require(success); + } +} diff --git a/src/sign/Secp256r1.sol b/src/sign/Secp256r1.sol index cc57a66..83f9449 100644 --- a/src/sign/Secp256r1.sol +++ b/src/sign/Secp256r1.sol @@ -7,11 +7,19 @@ pragma solidity ^0.8.23; /// . library Secp256r1 { /// @notice P256VERIFY operation - /// @param input Slice of bytes representing the input for the precompile operation + /// @param digest 32 bytes of the signed data hash + /// @param r 32 bytes of the r component of the signature + /// @param s 32 bytes of the s component of the signature + /// @param publicKeyX 32 bytes of the x coordinate of the public key + /// @param publicKeyY 32 bytes of the y coordinate of the public key /// @return success Represents if the operation was successful - function verify(bytes memory input) internal view returns (bool) { + function verify(bytes32 digest, bytes32 r, bytes32 s, uint256 publicKeyX, uint256 publicKeyY) + internal + view + returns (bool) + { // P256VERIFY address is 0x14 from - (bool success, bytes memory output) = address(0x14).staticcall(input); + (bool success, bytes memory output) = address(0x14).staticcall(abi.encode(digest, r, s, publicKeyX, publicKeyY)); success = success && output.length == 32 && output[31] == 0x01; return success;