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}