solana_secp256k1_recover/lib.rs
1#![cfg_attr(feature = "frozen-abi", feature(min_specialization))]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3//! Public key recovery from [secp256k1] ECDSA signatures.
4//!
5//! [secp256k1]: https://en.bitcoin.it/wiki/Secp256k1
6//!
7//! _This module provides low-level cryptographic building blocks that must be
8//! used carefully to ensure proper security. Read this documentation and
9//! accompanying links thoroughly._
10//!
11//! The [`secp256k1_recover`] syscall allows a secp256k1 public key that has
12//! previously signed a message to be recovered from the combination of the
13//! message, the signature, and a recovery ID. The recovery ID is generated
14//! during signing.
15//!
16//! Use cases for `secp256k1_recover` include:
17//!
18//! - Implementing the Ethereum [`ecrecover`] builtin contract.
19//! - Performing secp256k1 public key recovery generally.
20//! - Verifying a single secp256k1 signature.
21//!
22//! While `secp256k1_recover` can be used to verify secp256k1 signatures, Solana
23//! also provides the [secp256k1 program][sp], which is more flexible, has lower CPU
24//! cost, and can validate many signatures at once.
25//!
26//! [sp]: https://docs.rs/solana-program/latest/solana_program/secp256k1_program/
27//! [`ecrecover`]: https://docs.soliditylang.org/en/v0.8.14/units-and-global-variables.html?highlight=ecrecover#mathematical-and-cryptographic-functions
28
29#[cfg(feature = "borsh")]
30use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
31use {core::convert::TryFrom, thiserror::Error};
32
33#[derive(Debug, Clone, PartialEq, Eq, Error)]
34pub enum Secp256k1RecoverError {
35 #[error("The hash provided to a secp256k1_recover is invalid")]
36 InvalidHash,
37 #[error("The recovery_id provided to a secp256k1_recover is invalid")]
38 InvalidRecoveryId,
39 #[error("The signature provided to a secp256k1_recover is invalid")]
40 InvalidSignature,
41}
42
43impl From<u64> for Secp256k1RecoverError {
44 fn from(v: u64) -> Secp256k1RecoverError {
45 match v {
46 1 => Secp256k1RecoverError::InvalidHash,
47 2 => Secp256k1RecoverError::InvalidRecoveryId,
48 3 => Secp256k1RecoverError::InvalidSignature,
49 _ => panic!("Unsupported Secp256k1RecoverError"),
50 }
51 }
52}
53
54impl From<Secp256k1RecoverError> for u64 {
55 fn from(v: Secp256k1RecoverError) -> u64 {
56 match v {
57 Secp256k1RecoverError::InvalidHash => 1,
58 Secp256k1RecoverError::InvalidRecoveryId => 2,
59 Secp256k1RecoverError::InvalidSignature => 3,
60 }
61 }
62}
63
64pub const SECP256K1_SIGNATURE_LENGTH: usize = 64;
65pub const SECP256K1_PUBLIC_KEY_LENGTH: usize = 64;
66
67#[repr(transparent)]
68#[cfg_attr(feature = "frozen-abi", derive(solana_frozen_abi_macro::AbiExample))]
69#[cfg_attr(
70 feature = "borsh",
71 derive(BorshSerialize, BorshDeserialize, BorshSchema),
72 borsh(crate = "borsh")
73)]
74#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
75pub struct Secp256k1Pubkey(pub [u8; SECP256K1_PUBLIC_KEY_LENGTH]);
76
77impl Secp256k1Pubkey {
78 pub fn new(pubkey_vec: &[u8]) -> Self {
79 Self(
80 <[u8; SECP256K1_PUBLIC_KEY_LENGTH]>::try_from(<&[u8]>::clone(&pubkey_vec))
81 .expect("Slice must be the same length as a Pubkey"),
82 )
83 }
84
85 pub fn to_bytes(self) -> [u8; 64] {
86 self.0
87 }
88}
89
90#[cfg(any(target_os = "solana", target_arch = "bpf"))]
91pub use solana_define_syscall::definitions::sol_secp256k1_recover;
92
93/// Recover the public key from a [secp256k1] ECDSA signature and
94/// cryptographically-hashed message.
95///
96/// [secp256k1]: https://en.bitcoin.it/wiki/Secp256k1
97///
98/// This function is specifically intended for efficiently implementing
99/// Ethereum's [`ecrecover`] builtin contract, for use by Ethereum integrators.
100/// It may be useful for other purposes.
101///
102/// [`ecrecover`]: https://docs.soliditylang.org/en/v0.8.14/units-and-global-variables.html?highlight=ecrecover#mathematical-and-cryptographic-functions
103///
104/// `hash` is the 32-byte cryptographic hash (typically [`keccak`]) of an
105/// arbitrary message, signed by some public key.
106///
107/// The recovery ID is a value in the range [0, 3] that is generated during
108/// signing, and allows the recovery process to be more efficient. Note that the
109/// `recovery_id` here does not directly correspond to an Ethereum recovery ID
110/// as used in `ecrecover`. This function accepts recovery IDs in the range of
111/// [0, 3], while Ethereum's recovery IDs have a value of 27 or 28. To convert
112/// an Ethereum recovery ID to a value this function will accept subtract 27
113/// from it, checking for underflow. In practice this function will not succeed
114/// if given a recovery ID of 2 or 3, as these values represent an
115/// "overflowing" signature, and this function returns an error when parsing
116/// overflowing signatures.
117///
118/// [`keccak`]: https://docs.rs/solana-program/latest/solana_program/keccak/
119/// [`wrapping_sub`]: https://doc.rust-lang.org/std/primitive.u8.html#method.wrapping_sub
120///
121/// On success this function returns a [`Secp256k1Pubkey`], a wrapper around a
122/// 64-byte secp256k1 public key. This public key corresponds to the secret key
123/// that previously signed the message `hash` to produce the provided
124/// `signature`.
125///
126/// While `secp256k1_recover` can be used to verify secp256k1 signatures by
127/// comparing the recovered key against an expected key, Solana also provides
128/// the [secp256k1 program][sp], which is more flexible, has lower CPU cost, and
129/// can validate many signatures at once.
130///
131/// [sp]: https://docs.rs/solana-program/latest/solana_program/secp256k1_program/
132///
133/// The `secp256k1_recover` syscall is implemented with the [`libsecp256k1`]
134/// crate, but clients may want to use [`k256`] for an up-to-date pure Rust
135/// implementation.
136///
137/// [`libsecp256k1`]: https://docs.rs/libsecp256k1/latest/libsecp256k1
138/// [`k256`]: https://docs.rs/k256/latest/k256
139///
140/// # Hashing messages
141///
142/// In ECDSA signing and key recovery the signed "message" is always a
143/// cryptographic hash, not the original message itself. If not a cryptographic
144/// hash, then an adversary can craft signatures that recover to arbitrary
145/// public keys. This means the caller of this function generally must hash the
146/// original message themselves and not rely on another party to provide the
147/// hash.
148///
149/// Ethereum uses the [`keccak`] hash.
150///
151/// # Signature malleability
152///
153/// With the ECDSA signature algorithm it is possible for any party, given a
154/// valid signature of some message, to create a second signature that is
155/// equally valid. This is known as _signature malleability_. In many cases this
156/// is not a concern, but in cases where applications rely on signatures to have
157/// a unique representation this can be the source of bugs, potentially with
158/// security implications.
159///
160/// **The solana `secp256k1_recover` function does not prevent signature
161/// malleability**. This is in contrast to the Bitcoin secp256k1 library, which
162/// does prevent malleability by default. Solana accepts signatures with `S`
163/// values that are either in the _high order_ or in the _low order_, and it
164/// is trivial to produce one from the other.
165///
166/// To prevent signature malleability, it is common for secp256k1 signature
167/// validators to only accept signatures with low-order `S` values, and reject
168/// signatures with high-order `S` values. The following code will accomplish
169/// this:
170///
171/// ```rust
172/// # use k256::elliptic_curve::scalar::IsHigh;
173/// # use solana_program::program_error::ProgramError;
174/// # let signature_bytes = [
175/// # 0x83, 0x55, 0x81, 0xDF, 0xB1, 0x02, 0xA7, 0xD2,
176/// # 0x2D, 0x33, 0xA4, 0x07, 0xDD, 0x7E, 0xFA, 0x9A,
177/// # 0xE8, 0x5F, 0x42, 0x6B, 0x2A, 0x05, 0xBB, 0xFB,
178/// # 0xA1, 0xAE, 0x93, 0x84, 0x46, 0x48, 0xE3, 0x35,
179/// # 0x74, 0xE1, 0x6D, 0xB4, 0xD0, 0x2D, 0xB2, 0x0B,
180/// # 0x3C, 0x89, 0x8D, 0x0A, 0x44, 0xDF, 0x73, 0x9C,
181/// # 0x1E, 0xBF, 0x06, 0x8E, 0x8A, 0x9F, 0xA9, 0xC3,
182/// # 0xA5, 0xEA, 0x21, 0xAC, 0xED, 0x5B, 0x22, 0x13,
183/// # ];
184/// let signature = k256::ecdsa::Signature::from_slice(&signature_bytes)
185/// .map_err(|_| ProgramError::InvalidArgument)?;
186///
187/// if bool::from(signature.s().is_high()) {
188/// return Err(ProgramError::InvalidArgument);
189/// }
190/// # Ok::<_, ProgramError>(())
191/// ```
192///
193/// For the most accurate description of signature malleability, and its
194/// prevention in secp256k1, refer to comments in [`secp256k1.h`] in the Bitcoin
195/// Core secp256k1 library, the documentation of the [OpenZeppelin `recover`
196/// method for Solidity][ozr], and [this description of the problem on
197/// StackExchange][sxr].
198///
199/// [`secp256k1.h`]: https://github.com/bitcoin-core/secp256k1/blob/44c2452fd387f7ca604ab42d73746e7d3a44d8a2/include/secp256k1.h
200/// [ozr]: https://docs.openzeppelin.com/contracts/2.x/api/cryptography#ECDSA-recover-bytes32-bytes-
201/// [sxr]: https://bitcoin.stackexchange.com/questions/81115/if-someone-wanted-to-pretend-to-be-satoshi-by-posting-a-fake-signature-to-defrau/81116#81116
202///
203/// # Errors
204///
205/// If `hash` is not 32 bytes in length this function returns
206/// [`Secp256k1RecoverError::InvalidHash`], though see notes
207/// on SBF-specific behavior below.
208///
209/// If `recovery_id` is not in the range [0, 3] this function returns
210/// [`Secp256k1RecoverError::InvalidRecoveryId`].
211///
212/// If `signature` is not 64 bytes in length this function returns
213/// [`Secp256k1RecoverError::InvalidSignature`], though see notes
214/// on SBF-specific behavior below.
215///
216/// If `signature` represents an "overflowing" signature this function returns
217/// [`Secp256k1RecoverError::InvalidSignature`]. Overflowing signatures are
218/// non-standard and should not be encountered in practice.
219///
220/// If `signature` is otherwise invalid this function returns
221/// [`Secp256k1RecoverError::InvalidSignature`].
222///
223/// # SBF-specific behavior
224///
225/// When calling this function on-chain the caller must verify the correct
226/// lengths of `hash` and `signature` beforehand.
227///
228/// When run on-chain this function will not directly validate the lengths of
229/// `hash` and `signature`. It will assume they are the correct lengths and
230/// pass their pointers to the runtime, which will interpret them as 32-byte and
231/// 64-byte buffers. If the provided slices are too short, the runtime will read
232/// invalid data and attempt to interpret it, most likely returning an error,
233/// though in some scenarios it may be possible to incorrectly return
234/// successfully, or the transaction will abort if the syscall reads data
235/// outside of the program's memory space. If the provided slices are too long
236/// then they may be used to "smuggle" uninterpreted data.
237///
238/// # Examples
239///
240/// This example demonstrates recovering a public key and using it to verify a
241/// signature with the `secp256k1_recover` syscall. It has three parts: a Solana
242/// program, an RPC client to call the program, and common definitions shared
243/// between the two.
244///
245/// Common definitions:
246///
247/// ```
248/// use borsh::{BorshDeserialize, BorshSerialize};
249///
250/// #[derive(BorshSerialize, BorshDeserialize, Debug)]
251/// # #[borsh(crate = "borsh")]
252/// pub struct DemoSecp256k1RecoverInstruction {
253/// pub message: Vec<u8>,
254/// pub signature: [u8; 64],
255/// pub recovery_id: u8,
256/// }
257/// ```
258///
259/// The Solana program. Note that it uses `k256` version 0.13.0 to parse
260/// the secp256k1 signature to prevent malleability.
261///
262/// ```rust
263/// use k256::elliptic_curve::scalar::IsHigh;
264/// use solana_program::{
265/// entrypoint::ProgramResult,
266/// keccak, msg,
267/// program_error::ProgramError,
268/// };
269/// use solana_secp256k1_recover::secp256k1_recover;
270///
271/// # pub struct DemoSecp256k1RecoverInstruction {
272/// # pub message: Vec<u8>,
273/// # pub signature: [u8; 64],
274/// # pub recovery_id: u8,
275/// # }
276///
277/// pub fn process_secp256k1_recover(
278/// instruction: DemoSecp256k1RecoverInstruction,
279/// expected_public_key: [u8; 64],
280/// ) -> ProgramResult {
281/// // The secp256k1 recovery operation accepts a cryptographically-hashed
282/// // message only. Passing it anything else is insecure and allows signatures
283/// // to be forged.
284/// //
285/// // This means that the code calling `secp256k1_recover` must perform the hash
286/// // itself, and not assume that data passed to it has been properly hashed.
287/// let message_hash = {
288/// let mut hasher = keccak::Hasher::default();
289/// hasher.hash(&instruction.message);
290/// hasher.result()
291/// };
292///
293/// // Reject high-s value signatures to prevent malleability.
294/// // Solana does not do this itself.
295/// // This may or may not be necessary depending on use case.
296/// {
297/// let signature = k256::ecdsa::Signature::from_slice(&instruction.signature)
298/// .map_err(|_| ProgramError::InvalidArgument)?;
299///
300/// if bool::from(signature.s().is_high()) {
301/// msg!("signature with high-s value");
302/// return Err(ProgramError::InvalidArgument);
303/// }
304/// }
305///
306/// let recovered_pubkey = secp256k1_recover(
307/// message_hash.as_bytes(),
308/// instruction.recovery_id,
309/// &instruction.signature,
310/// )
311/// .map_err(|_| ProgramError::InvalidArgument)?;
312///
313/// // If we're using this function for signature verification then we
314/// // need to check the pubkey is an expected value.
315/// // Here we are checking the secp256k1 pubkey against a known authorized pubkey.
316/// if recovered_pubkey.0 != expected_public_key {
317/// return Err(ProgramError::InvalidArgument);
318/// }
319///
320/// Ok(())
321/// }
322///
323/// let secret_key = k256::ecdsa::SigningKey::random(&mut rand::thread_rng());
324/// let public_key = &secret_key.verifying_key().to_encoded_point(false);
325/// let message = b"hello world!";
326/// let message_hash = {
327/// let mut hasher = keccak::Hasher::default();
328/// hasher.hash(message);
329/// hasher.result()
330/// };
331/// let (signature, recovery_id) = secret_key.sign_prehash_recoverable(message_hash.as_bytes()).unwrap();
332/// let signature = signature.to_bytes().into();
333/// let instruction = DemoSecp256k1RecoverInstruction {
334/// message: message.to_vec(),
335/// signature,
336/// recovery_id: recovery_id.to_byte(),
337/// };
338/// process_secp256k1_recover(instruction, public_key.as_bytes()[1..65].try_into().unwrap()).unwrap();
339/// ```
340///
341/// The RPC client program:
342///
343/// ```rust
344/// # use solana_program::example_mocks::solana_rpc_client;
345/// # use solana_program::example_mocks::solana_sdk;
346/// use anyhow::Result;
347/// use solana_rpc_client::rpc_client::RpcClient;
348/// use solana_sdk::{
349/// instruction::Instruction,
350/// keccak,
351/// pubkey::Pubkey,
352/// signature::{Keypair, Signer},
353/// transaction::Transaction,
354/// };
355/// # use borsh::{BorshDeserialize, BorshSerialize};
356/// # #[derive(BorshSerialize, BorshDeserialize, Debug)]
357/// # #[borsh(crate = "borsh")]
358/// # pub struct DemoSecp256k1RecoverInstruction {
359/// # pub message: Vec<u8>,
360/// # pub signature: [u8; 64],
361/// # pub recovery_id: u8,
362/// # }
363///
364/// pub fn demo_secp256k1_recover(
365/// payer_keypair: &Keypair,
366/// secp256k1_secret_key: &k256::ecdsa::SigningKey,
367/// client: &RpcClient,
368/// program_keypair: &Keypair,
369/// ) -> Result<()> {
370/// let message = b"hello world";
371/// let message_hash = {
372/// let mut hasher = keccak::Hasher::default();
373/// hasher.hash(message);
374/// hasher.result()
375/// };
376///
377/// let (signature, recovery_id) = secp256k1_secret_key.sign_prehash_recoverable(message_hash.as_bytes()).unwrap();
378///
379/// let signature = signature.to_bytes().into();
380///
381/// let instr = DemoSecp256k1RecoverInstruction {
382/// message: message.to_vec(),
383/// signature,
384/// recovery_id: recovery_id.to_byte(),
385/// };
386/// let instr = Instruction::new_with_borsh(
387/// program_keypair.pubkey(),
388/// &instr,
389/// vec![],
390/// );
391///
392/// let blockhash = client.get_latest_blockhash()?;
393/// let tx = Transaction::new_signed_with_payer(
394/// &[instr],
395/// Some(&payer_keypair.pubkey()),
396/// &[payer_keypair],
397/// blockhash,
398/// );
399///
400/// client.send_and_confirm_transaction(&tx)?;
401///
402/// Ok(())
403/// }
404/// ```
405#[cfg_attr(any(target_os = "solana", target_arch = "bpf"), inline(always))]
406pub fn secp256k1_recover(
407 hash: &[u8],
408 recovery_id: u8,
409 signature: &[u8],
410) -> Result<Secp256k1Pubkey, Secp256k1RecoverError> {
411 #[cfg(any(target_os = "solana", target_arch = "bpf"))]
412 {
413 let mut pubkey_buffer =
414 core::mem::MaybeUninit::<[u8; SECP256K1_PUBLIC_KEY_LENGTH]>::uninit();
415 let result = unsafe {
416 sol_secp256k1_recover(
417 hash.as_ptr(),
418 recovery_id as u64,
419 signature.as_ptr(),
420 pubkey_buffer.as_mut_ptr() as *mut u8,
421 )
422 };
423
424 // SAFETY: This is sound as in our pass case, all 64 bytes of the pubkey are
425 // always initialized by sol_secp256k1_recover
426 match result {
427 0 => Ok(Secp256k1Pubkey(unsafe { pubkey_buffer.assume_init() })),
428 error => Err(Secp256k1RecoverError::from(error)),
429 }
430 }
431
432 #[cfg(not(any(target_os = "solana", target_arch = "bpf")))]
433 {
434 const HASH_SIZE: usize = 32;
435 if hash.len() != HASH_SIZE {
436 return Err(Secp256k1RecoverError::InvalidHash);
437 }
438 let recovery_id = k256::ecdsa::RecoveryId::try_from(recovery_id)
439 .map_err(|_| Secp256k1RecoverError::InvalidRecoveryId)?;
440 let signature = k256::ecdsa::Signature::from_slice(signature)
441 .map_err(|_| Secp256k1RecoverError::InvalidSignature)?;
442 let secp256k1_key =
443 k256::ecdsa::VerifyingKey::recover_from_prehash(hash, &signature, recovery_id)
444 .map_err(|_| Secp256k1RecoverError::InvalidSignature)?;
445 Ok(Secp256k1Pubkey::new(
446 &secp256k1_key.to_encoded_point(false).as_bytes()[1..65],
447 ))
448 }
449}