P256 Precompile
Address: 0x0000000000000000000000000000000000001011
This is the implementation of RIP-7212 , which provides a precompiled contract for verifying signatures in the secp256r1
or P-256
elliptic curve.
Overview
The P256 precompile enables efficient signature verification for the secp256r1
curve, which is widely used in modern security systems including:
- Apple’s Secure Enclave
- WebAuthn/FIDO2
- Android Keychain
- Various hardware security modules (HSMs)
- PassKeys
This precompile implementation is significantly more gas efficient (up to 60x) compared to Solidity-based implementations.
Interface
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
address constant P256VERIFY_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001011;
IP256VERIFY constant P256VERIFY_CONTRACT = IP256VERIFY(P256VERIFY_PRECOMPILE_ADDRESS);
interface IP256VERIFY {
function verify(
bytes memory signature
) external view returns (bytes memory response);
}
Implementation Library
Here’s a complete implementation of the P256 library that provides a convenient wrapper around the precompile:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import "./ECDSA.sol";
import "./P256Verify.sol";
/// @title P256
/// @author klkvr <https://github.com/klkvr>
/// @author jxom <https://github.com/jxom>
/// @notice Wrapper function to abstract low level details of call to the P256
/// signature verification precompile as defined in EIP-7212, see
/// <https://eips.ethereum.org/EIPS/eip-7212>.
library P256 {
/// @notice P256VERIFY operation
/// @param digest 32 bytes of the signed data hash
/// @param signature Signature of the signer
/// @param publicKey Public key of the signer
/// @return success Represents if the operation was successful
function verify(bytes32 digest, ECDSA.Signature memory signature, ECDSA.PublicKey memory publicKey)
internal
view
returns (bool)
{
bytes memory input = abi.encode(digest, signature.r, signature.s, publicKey.x, publicKey.y);
bytes memory output = P256VERIFY_CONTRACT.verify(input);
bool success = output.length == 32 && output[31] == 0x01;
return success;
}
}
Basic Usage
Using the P256 Library
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import "./P256.sol";
import "./ECDSA.sol";
contract P256Example {
using P256 for bytes32;
function verifySignature(
bytes32 messageHash,
ECDSA.Signature memory signature,
ECDSA.PublicKey memory publicKey
) public view returns (bool) {
return P256.verify(messageHash, signature, publicKey);
}
}
Direct Precompile Usage
For more control, you can call the precompile directly:
pragma solidity ^0.8.0;
interface IP256Verify {
function verify(bytes calldata input) external view returns (bytes32);
}
contract P256Verifier {
IP256Verify constant P256_PRECOMPILE = IP256Verify(0x0000000000000000000000000000000000001011);
function verifySignature(
bytes32 messageHash,
bytes32 r,
bytes32 s,
bytes32 publicKeyX,
bytes32 publicKeyY
) external view returns (bool) {
bytes memory input = abi.encodePacked(
messageHash,
r,
s,
publicKeyX,
publicKeyY
);
bytes32 result = P256_PRECOMPILE.verify(input);
return result != bytes32(0);
}
}
Signature Format
The signature verification requires the following components:
digest
: 32 bytes of the signed data hashsignature
: Contains the r and s components of the signaturepublicKey
: Contains the x and y coordinates of the public key
The precompile expects these components to be encoded in a specific format:
- First 32 bytes: message hash
- Next 32 bytes:
r
component of the signature - Next 32 bytes:
s
component of the signature - Next 32 bytes:
x
coordinate of the public key - Next 32 bytes:
y
coordinate of the public key
Total length: 160 bytes
Gas Costs
The precompile is highly gas efficient compared to Solidity implementations. The exact gas cost per byte of verified data is set to GasCostPerByte = 300
, which gives us:
- Total Cost: 300 × 160 = 48,000 gas per verification
- Efficiency: Up to 60x more efficient than pure Solidity implementations
Real-World Use Cases
WebAuthn/PassKeys Authentication
pragma solidity ^0.8.0;
contract WebAuthnAuth {
IP256Verify constant P256_PRECOMPILE = IP256Verify(0x0000000000000000000000000000000000001011);
struct PublicKey {
bytes32 x;
bytes32 y;
}
mapping(address => PublicKey) public userKeys;
function registerPasskey(
bytes32 keyX,
bytes32 keyY
) external {
userKeys[msg.sender] = PublicKey(keyX, keyY);
}
function authenticateWithPasskey(
bytes32 challengeHash,
bytes32 r,
bytes32 s
) external view returns (bool) {
PublicKey memory userKey = userKeys[msg.sender];
require(userKey.x != 0 && userKey.y != 0, "No passkey registered");
bytes memory input = abi.encodePacked(
challengeHash,
r,
s,
userKey.x,
userKey.y
);
bytes32 result = P256_PRECOMPILE.verify(input);
return result != bytes32(0);
}
}
Apple Secure Enclave Integration
contract SecureEnclaveAuth {
event SecureOperation(address indexed user, bytes32 indexed operationHash);
function executeSecureOperation(
bytes32 operationHash,
bytes32 r,
bytes32 s,
bytes32 enclaveKeyX,
bytes32 enclaveKeyY
) external {
require(
verifyEnclaveSignature(operationHash, r, s, enclaveKeyX, enclaveKeyY),
"Invalid Secure Enclave signature"
);
emit SecureOperation(msg.sender, operationHash);
}
function verifyEnclaveSignature(
bytes32 hash,
bytes32 r,
bytes32 s,
bytes32 x,
bytes32 y
) internal view returns (bool) {
bytes memory input = abi.encodePacked(hash, r, s, x, y);
bytes32 result = P256_PRECOMPILE.verify(input);
return result != bytes32(0);
}
}
Multi-Signature with Hardware Keys
contract HardwareMultiSig {
IP256Verify constant P256_PRECOMPILE = IP256Verify(0x0000000000000000000000000000000000001011);
struct Signature {
bytes32 r;
bytes32 s;
}
struct PublicKey {
bytes32 x;
bytes32 y;
}
function verifyMultipleHardwareKeys(
bytes32 messageHash,
Signature[] calldata signatures,
PublicKey[] calldata publicKeys,
uint256 threshold
) external view returns (bool) {
require(signatures.length == publicKeys.length, "Mismatched arrays");
require(threshold <= signatures.length, "Invalid threshold");
uint256 validSignatures = 0;
for (uint i = 0; i < signatures.length; i++) {
bytes memory input = abi.encodePacked(
messageHash,
signatures[i].r,
signatures[i].s,
publicKeys[i].x,
publicKeys[i].y
);
bytes32 result = P256_PRECOMPILE.verify(input);
if (result != bytes32(0)) {
validSignatures++;
}
}
return validSignatures >= threshold;
}
}
Security Considerations
Important: Always validate public keys and signature components before verification to prevent invalid curve point attacks.
Public Key Validation
function isValidPublicKey(bytes32 x, bytes32 y) internal pure returns (bool) {
// Ensure point is not at infinity
if (x == 0 && y == 0) return false;
// Additional validation should be performed for production use
// The precompile will reject invalid curve points
return true;
}
Signature Malleability
P256 signatures can be malleable. If your application requires unique signatures, implement additional checks:
function isLowS(bytes32 s) internal pure returns (bool) {
// Check if s is in the lower half of the curve order
// This prevents signature malleability
return uint256(s) <= 0x7FFFFFFF80000000FFFFFFFFFFFFFFFF;
}
JavaScript Integration
Preparing Input Data
// Example: Preparing P256 verification input
function prepareP256Input(messageHash, signature, publicKey) {
// Ensure all components are 32 bytes
const hash = ethers.utils.hexZeroPad(messageHash, 32);
const r = ethers.utils.hexZeroPad(signature.r, 32);
const s = ethers.utils.hexZeroPad(signature.s, 32);
const x = ethers.utils.hexZeroPad(publicKey.x, 32);
const y = ethers.utils.hexZeroPad(publicKey.y, 32);
// Concatenate all components
return ethers.utils.concat([hash, r, s, x, y]);
}
// Usage with ethers.js
async function verifyP256Signature(contract, messageHash, signature, publicKey) {
const input = prepareP256Input(messageHash, signature, publicKey);
const result = await contract.P256_PRECOMPILE.verify(input);
return result !== '0x0000000000000000000000000000000000000000000000000000000000000000';
}
Error Handling
The P256 precompile returns zero on failure. Common failure cases include:
- Invalid Input Length: Input must be exactly 160 bytes
- Invalid Public Key: Point not on the P256 curve
- Invalid Signature: r or s values out of valid range
- Verification Failure: Signature doesn’t match message and public key
function safeVerifyP256(
bytes32 messageHash,
bytes32 r,
bytes32 s,
bytes32 publicKeyX,
bytes32 publicKeyY
) internal view returns (bool success, string memory error) {
// Validate inputs
if (!isValidPublicKey(publicKeyX, publicKeyY)) {
return (false, "Invalid public key");
}
if (r == 0 || s == 0) {
return (false, "Invalid signature components");
}
// Prepare input and verify
bytes memory input = abi.encodePacked(
messageHash, r, s, publicKeyX, publicKeyY
);
bytes32 result = P256_PRECOMPILE.verify(input);
return (result != bytes32(0), result == bytes32(0) ? "Verification failed" : "");
}
Testing
Unit Tests
contract P256Test {
IP256Verify constant P256_PRECOMPILE = IP256Verify(0x0000000000000000000000000000000000001011);
function testValidSignature() public {
// Test with known valid P256 signature
bytes32 messageHash = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;
// Use actual test vectors for production tests
bytes32 r = 0x...; // Valid r component
bytes32 s = 0x...; // Valid s component
bytes32 x = 0x...; // Valid public key x
bytes32 y = 0x...; // Valid public key y
bytes memory input = abi.encodePacked(messageHash, r, s, x, y);
bytes32 result = P256_PRECOMPILE.verify(input);
assert(result != bytes32(0));
}
}
Performance Considerations
- Gas Efficiency: 48,000 gas per verification vs 2M+ gas for Solidity implementations
- Batch Operations: Consider batching multiple verifications in a single transaction
- Caching: Cache public keys on-chain to reduce calldata for repeated verifications
- Hardware Integration: Particularly efficient for applications using hardware-backed keys