//! Public key recovery from [secp256k1] ECDSA signatures. //! //! [secp256k1]: https://en.bitcoin.it/wiki/Secp256k1 //! //! _This module provides low-level cryptographic building blocks that must be //! used carefully to ensure proper security. Read this documentation and //! accompanying links thoroughly._ //! //! The [`secp256k1_recover`] syscall allows a secp256k1 public key that has //! previously signed a message to be recovered from the combination of the //! message, the signature, and a recovery ID. The recovery ID is generated //! during signing. //! //! Use cases for `secp256k1_recover` include: //! //! - Implementing the Ethereum [`ecrecover`] builtin contract. //! - Performing secp256k1 public key recovery generally. //! - Verifying a single secp256k1 signature. //! //! While `secp256k1_recover` can be used to verify secp256k1 signatures, Solana //! also provides the [secp256k1 program][sp], which is more flexible, has lower CPU //! cost, and can validate many signatures at once. //! //! [sp]: crate::secp256k1_program //! [`ecrecover`]: https://docs.soliditylang.org/en/v0.8.14/units-and-global-variables.html?highlight=ecrecover#mathematical-and-cryptographic-functions use { borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, core::convert::TryFrom, thiserror::Error, }; #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum Secp256k1RecoverError { #[error("The hash provided to a secp256k1_recover is invalid")] InvalidHash, #[error("The recovery_id provided to a secp256k1_recover is invalid")] InvalidRecoveryId, #[error("The signature provided to a secp256k1_recover is invalid")] InvalidSignature, } impl From for Secp256k1RecoverError { fn from(v: u64) -> Secp256k1RecoverError { match v { 1 => Secp256k1RecoverError::InvalidHash, 2 => Secp256k1RecoverError::InvalidRecoveryId, 3 => Secp256k1RecoverError::InvalidSignature, _ => panic!("Unsupported Secp256k1RecoverError"), } } } impl From for u64 { fn from(v: Secp256k1RecoverError) -> u64 { match v { Secp256k1RecoverError::InvalidHash => 1, Secp256k1RecoverError::InvalidRecoveryId => 2, Secp256k1RecoverError::InvalidSignature => 3, } } } pub const SECP256K1_SIGNATURE_LENGTH: usize = 64; pub const SECP256K1_PUBLIC_KEY_LENGTH: usize = 64; #[repr(transparent)] #[derive( BorshSerialize, BorshDeserialize, BorshSchema, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, AbiExample, )] pub struct Secp256k1Pubkey(pub [u8; SECP256K1_PUBLIC_KEY_LENGTH]); impl Secp256k1Pubkey { pub fn new(pubkey_vec: &[u8]) -> Self { Self( <[u8; SECP256K1_PUBLIC_KEY_LENGTH]>::try_from(<&[u8]>::clone(&pubkey_vec)) .expect("Slice must be the same length as a Pubkey"), ) } pub fn to_bytes(self) -> [u8; 64] { self.0 } } /// Recover the public key from a [secp256k1] ECDSA signature and /// cryptographically-hashed message. /// /// [secp256k1]: https://en.bitcoin.it/wiki/Secp256k1 /// /// 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. /// /// [`ecrecover`]: https://docs.soliditylang.org/en/v0.8.14/units-and-global-variables.html?highlight=ecrecover#mathematical-and-cryptographic-functions /// /// `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 efficent. 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. /// /// [`keccak`]: crate::keccak /// [`wrapping_sub`]: https://doc.rust-lang.org/std/primitive.u8.html#method.wrapping_sub /// /// 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][sp], which is more flexible, has lower CPU cost, and /// can validate many signatures at once. /// /// [sp]: crate::secp256k1_program /// /// The `secp256k1_recover` syscall is implemented with the [`libsecp256k1`] /// crate, which clients may also want to use. /// /// [`libsecp256k1`]: https://docs.rs/libsecp256k1/latest/libsecp256k1 /// /// # 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: /// /// ```rust /// # use solana_program::program_error::ProgramError; /// # let signature_bytes = [ /// # 0x83, 0x55, 0x81, 0xDF, 0xB1, 0x02, 0xA7, 0xD2, /// # 0x2D, 0x33, 0xA4, 0x07, 0xDD, 0x7E, 0xFA, 0x9A, /// # 0xE8, 0x5F, 0x42, 0x6B, 0x2A, 0x05, 0xBB, 0xFB, /// # 0xA1, 0xAE, 0x93, 0x84, 0x46, 0x48, 0xE3, 0x35, /// # 0x74, 0xE1, 0x6D, 0xB4, 0xD0, 0x2D, 0xB2, 0x0B, /// # 0x3C, 0x89, 0x8D, 0x0A, 0x44, 0xDF, 0x73, 0x9C, /// # 0x1E, 0xBF, 0x06, 0x8E, 0x8A, 0x9F, 0xA9, 0xC3, /// # 0xA5, 0xEA, 0x21, 0xAC, 0xED, 0x5B, 0x22, 0x13, /// # ]; /// let signature = libsecp256k1::Signature::parse_standard_slice(&signature_bytes) /// .map_err(|_| ProgramError::InvalidArgument)?; /// /// if signature.s.is_high() { /// return Err(ProgramError::InvalidArgument); /// } /// # Ok::<_, ProgramError>(()) /// ``` /// /// 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 BPF target. /// /// [`libsecp256k1`]: https://docs.rs/libsecp256k1/latest/libsecp256k1 /// /// 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][ozr], and [this description of the problem on /// StackExchange][sxr]. /// /// [`secp256k1.h`]: https://github.com/bitcoin-core/secp256k1/blob/44c2452fd387f7ca604ab42d73746e7d3a44d8a2/include/secp256k1.h /// [ozr]: https://docs.openzeppelin.com/contracts/2.x/api/cryptography#ECDSA-recover-bytes32-bytes- /// [sxr]: https://bitcoin.stackexchange.com/questions/81115/if-someone-wanted-to-pretend-to-be-satoshi-by-posting-a-fake-signature-to-defrau/81116#81116 /// /// # Errors /// /// If `hash` is not 32 bytes in length this function returns /// [`Secp256k1RecoverError::InvalidHash`], though see notes /// on BPF-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 BPF-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`]. /// /// # BPF-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, /// 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. /// /// ```no_run /// 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 struct DemoSecp256k1RecoverInstruction { /// # pub message: Vec, /// # pub signature: [u8; 64], /// # pub recovery_id: u8, /// # } /// /// 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: /// /// ```no_run /// # use solana_program::example_mocks::solana_client; /// # use solana_program::example_mocks::solana_sdk; /// use anyhow::Result; /// use solana_client::rpc_client::RpcClient; /// use solana_sdk::{ /// instruction::Instruction, /// keccak, /// pubkey::Pubkey, /// signature::{Keypair, Signer}, /// transaction::Transaction, /// }; /// # use borsh::{BorshDeserialize, BorshSerialize}; /// # #[derive(BorshSerialize, BorshDeserialize, Debug)] /// # pub struct DemoSecp256k1RecoverInstruction { /// # pub message: Vec, /// # pub signature: [u8; 64], /// # pub recovery_id: u8, /// # } /// /// 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(()) /// } /// ``` pub fn secp256k1_recover( hash: &[u8], recovery_id: u8, signature: &[u8], ) -> Result { #[cfg(target_os = "solana")] { let mut pubkey_buffer = [0u8; SECP256K1_PUBLIC_KEY_LENGTH]; let result = unsafe { crate::syscalls::sol_secp256k1_recover( hash.as_ptr(), recovery_id as u64, signature.as_ptr(), pubkey_buffer.as_mut_ptr(), ) }; match result { 0 => Ok(Secp256k1Pubkey::new(&pubkey_buffer)), error => Err(Secp256k1RecoverError::from(error)), } } #[cfg(not(target_os = "solana"))] { let message = libsecp256k1::Message::parse_slice(hash) .map_err(|_| Secp256k1RecoverError::InvalidHash)?; let recovery_id = libsecp256k1::RecoveryId::parse(recovery_id) .map_err(|_| Secp256k1RecoverError::InvalidRecoveryId)?; let signature = libsecp256k1::Signature::parse_standard_slice(signature) .map_err(|_| Secp256k1RecoverError::InvalidSignature)?; let secp256k1_key = libsecp256k1::recover(&message, &signature, &recovery_id) .map_err(|_| Secp256k1RecoverError::InvalidSignature)?; Ok(Secp256k1Pubkey::new(&secp256k1_key.serialize()[1..65])) } }