Skip to content

Commit

Permalink
Merge pull request #6 from klkvr/klkvr/examples
Browse files Browse the repository at this point in the history
feat: EIP-7702 + Secp256r1 example
  • Loading branch information
mattsse authored Aug 16, 2024
2 parents c8e6804 + f7ff660 commit df04fc0
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ docs/

# Dotenv file
.env

# Key files
private.pem
public.pem
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)' '<public key X>' '<public key Y>' --auth "<address of P256Delegation>" --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' '<r value>' '<s value>' --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
Expand All @@ -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
53 changes: 53 additions & 0 deletions examples/p256.py
Original file line number Diff line number Diff line change
@@ -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}")
32 changes: 32 additions & 0 deletions src/P256Delegation.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
14 changes: 11 additions & 3 deletions src/sign/Secp256r1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ pragma solidity ^0.8.23;
/// <https://eips.ethereum.org/EIPS/eip-7212>.
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 <https://github.com/paradigmxyz/alphanet/blob/main/crates/precompile/src/addresses.rs>
(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;
Expand Down

0 comments on commit df04fc0

Please sign in to comment.