solana_program::secp256k1_recover

Function secp256k1_recover

source
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(())
}