solana_secp256k1_recover/
lib.rs

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