pub fn secp256k1_recover(
hash: &[u8],
recovery_id: u8,
signature: &[u8],
) -> Result<Secp256k1Pubkey, Secp256k1RecoverError>
Expand description
Recover the public key from a secp256k1 ECDSA signature and cryptographically-hashed message.
This function is specifically intended for efficiently implementing
Ethereum’s ecrecover
builtin contract, for use by Ethereum integrators.
It may be useful for other purposes.
hash
is the 32-byte cryptographic hash (typically keccak
) of an
arbitrary message, signed by some public key.
The recovery ID is a value in the range [0, 3] that is generated during
signing, and allows the recovery process to be more efficient. Note that the
recovery_id
here does not directly correspond to an Ethereum recovery ID
as used in ecrecover
. This function accepts recovery IDs in the range of
[0, 3], while Ethereum’s recovery IDs have a value of 27 or 28. To convert
an Ethereum recovery ID to a value this function will accept subtract 27
from it, checking for underflow. In practice this function will not succeed
if given a recovery ID of 2 or 3, as these values represent an
“overflowing” signature, and this function returns an error when parsing
overflowing signatures.
On success this function returns a Secp256k1Pubkey
, a wrapper around a
64-byte secp256k1 public key. This public key corresponds to the secret key
that previously signed the message hash
to produce the provided
signature
.
While secp256k1_recover
can be used to verify secp256k1 signatures by
comparing the recovered key against an expected key, Solana also provides
the secp256k1 program, which is more flexible, has lower CPU cost, and
can validate many signatures at once.
The secp256k1_recover
syscall is implemented with the libsecp256k1
crate, which clients may also want to use.
§Hashing messages
In ECDSA signing and key recovery the signed “message” is always a crytographic hash, not the original message itself. If not a cryptographic hash, then an adversary can craft signatures that recover to arbitrary public keys. This means the caller of this function generally must hash the original message themselves and not rely on another party to provide the hash.
Ethereum uses the keccak
hash.
§Signature malleability
With the ECDSA signature algorithm it is possible for any party, given a valid signature of some message, to create a second signature that is equally valid. This is known as signature malleability. In many cases this is not a concern, but in cases where applications rely on signatures to have a unique representation this can be the source of bugs, potentially with security implications.
The solana secp256k1_recover
function does not prevent signature
malleability. This is in contrast to the Bitcoin secp256k1 library, which
does prevent malleability by default. Solana accepts signatures with S
values that are either in the high order or in the low order, and it
is trivial to produce one from the other.
To prevent signature malleability, it is common for secp256k1 signature
validators to only accept signatures with low-order S
values, and reject
signatures with high-order S
values. The following code will accomplish
this:
let signature = libsecp256k1::Signature::parse_standard_slice(&signature_bytes)
.map_err(|_| ProgramError::InvalidArgument)?;
if signature.s.is_high() {
return Err(ProgramError::InvalidArgument);
}
This has the downside that the program must link to the libsecp256k1
crate and parse the signature just for this check. Note that libsecp256k1
version 0.7.0 or greater is required for running on the Solana SBF target.
For the most accurate description of signature malleability, and its
prevention in secp256k1, refer to comments in secp256k1.h
in the Bitcoin
Core secp256k1 library, the documentation of the OpenZeppelin recover
method for Solidity, and this description of the problem on
StackExchange.
§Errors
If hash
is not 32 bytes in length this function returns
Secp256k1RecoverError::InvalidHash
, though see notes
on SBF-specific behavior below.
If recovery_id
is not in the range [0, 3] this function returns
Secp256k1RecoverError::InvalidRecoveryId
.
If signature
is not 64 bytes in length this function returns
Secp256k1RecoverError::InvalidSignature
, though see notes
on SBF-specific behavior below.
If signature
represents an “overflowing” signature this function returns
Secp256k1RecoverError::InvalidSignature
. Overflowing signatures are
non-standard and should not be encountered in practice.
If signature
is otherwise invalid this function returns
Secp256k1RecoverError::InvalidSignature
.
§SBF-specific behavior
When calling this function on-chain the caller must verify the correct
lengths of hash
and signature
beforehand.
When run on-chain this function will not directly validate the lengths of
hash
and signature
. It will assume they are the the correct lengths and
pass their pointers to the runtime, which will interpret them as 32-byte and
64-byte buffers. If the provided slices are too short, the runtime will read
invalid data and attempt to interpret it, most likely returning an error,
though in some scenarios it may be possible to incorrectly return
successfully, or the transaction will abort if the syscall reads data
outside of the program’s memory space. If the provided slices are too long
then they may be used to “smuggle” uninterpreted data.
§Examples
This example demonstrates recovering a public key and using it to very a
signature with the secp256k1_recover
syscall. It has three parts: a Solana
program, an RPC client to call the program, and common definitions shared
between the two.
Common definitions:
use borsh::{BorshDeserialize, BorshSerialize};
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct DemoSecp256k1RecoverInstruction {
pub message: Vec<u8>,
pub signature: [u8; 64],
pub recovery_id: u8,
}
The Solana program. Note that it uses libsecp256k1
version 0.7.0 to parse
the secp256k1 signature to prevent malleability.
use solana_program::{
entrypoint::ProgramResult,
keccak, msg,
program_error::ProgramError,
secp256k1_recover::secp256k1_recover,
};
/// The key we expect to sign secp256k1 messages,
/// as serialized by `libsecp256k1::PublicKey::serialize`.
const AUTHORIZED_PUBLIC_KEY: [u8; 64] = [
0x8C, 0xD6, 0x47, 0xF8, 0xA5, 0xBF, 0x59, 0xA0, 0x4F, 0x77, 0xFA, 0xFA, 0x6C, 0xA0, 0xE6, 0x4D,
0x94, 0x5B, 0x46, 0x55, 0xA6, 0x2B, 0xB0, 0x6F, 0x10, 0x4C, 0x9E, 0x2C, 0x6F, 0x42, 0x0A, 0xBE,
0x18, 0xDF, 0x0B, 0xF0, 0x87, 0x42, 0xBA, 0x88, 0xB4, 0xCF, 0x87, 0x5A, 0x35, 0x27, 0xBE, 0x0F,
0x45, 0xAE, 0xFC, 0x66, 0x9C, 0x2C, 0x6B, 0xF3, 0xEF, 0xCA, 0x5C, 0x32, 0x11, 0xF7, 0x2A, 0xC7,
];
pub fn process_secp256k1_recover(
instruction: DemoSecp256k1RecoverInstruction,
) -> ProgramResult {
// The secp256k1 recovery operation accepts a cryptographically-hashed
// message only. Passing it anything else is insecure and allows signatures
// to be forged.
//
// This means that the code calling `secp256k1_recover` must perform the hash
// itself, and not assume that data passed to it has been properly hashed.
let message_hash = {
let mut hasher = keccak::Hasher::default();
hasher.hash(&instruction.message);
hasher.result()
};
// Reject high-s value signatures to prevent malleability.
// Solana does not do this itself.
// This may or may not be necessary depending on use case.
{
let signature = libsecp256k1::Signature::parse_standard_slice(&instruction.signature)
.map_err(|_| ProgramError::InvalidArgument)?;
if signature.s.is_high() {
msg!("signature with high-s value");
return Err(ProgramError::InvalidArgument);
}
}
let recovered_pubkey = secp256k1_recover(
&message_hash.0,
instruction.recovery_id,
&instruction.signature,
)
.map_err(|_| ProgramError::InvalidArgument)?;
// If we're using this function for signature verification then we
// need to check the pubkey is an expected value.
// Here we are checking the secp256k1 pubkey against a known authorized pubkey.
if recovered_pubkey.0 != AUTHORIZED_PUBLIC_KEY {
return Err(ProgramError::InvalidArgument);
}
Ok(())
}
The RPC client program:
use anyhow::Result;
use solana_rpc_client::rpc_client::RpcClient;
use solana_sdk::{
instruction::Instruction,
keccak,
pubkey::Pubkey,
signature::{Keypair, Signer},
transaction::Transaction,
};
pub fn demo_secp256k1_recover(
payer_keypair: &Keypair,
secp256k1_secret_key: &libsecp256k1::SecretKey,
client: &RpcClient,
program_keypair: &Keypair,
) -> Result<()> {
let message = b"hello world";
let message_hash = {
let mut hasher = keccak::Hasher::default();
hasher.hash(message);
hasher.result()
};
let secp_message = libsecp256k1::Message::parse(&message_hash.0);
let (signature, recovery_id) = libsecp256k1::sign(&secp_message, &secp256k1_secret_key);
let signature = signature.serialize();
let instr = DemoSecp256k1RecoverInstruction {
message: message.to_vec(),
signature,
recovery_id: recovery_id.serialize(),
};
let instr = Instruction::new_with_borsh(
program_keypair.pubkey(),
&instr,
vec![],
);
let blockhash = client.get_latest_blockhash()?;
let tx = Transaction::new_signed_with_payer(
&[instr],
Some(&payer_keypair.pubkey()),
&[payer_keypair],
blockhash,
);
client.send_and_confirm_transaction(&tx)?;
Ok(())
}