diff --git a/Cargo.lock b/Cargo.lock index eefbd10a787751..fbf34e6ed8f44a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5928,6 +5928,7 @@ dependencies = [ "ed25519-dalek", "ed25519-dalek-bip32", "generic-array 0.14.5", + "hex", "hmac 0.12.1", "itertools", "js-sys", diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index 6337f429c0802e..e0eac93250ea5d 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -2053,15 +2053,31 @@ dependencies = [ "base64 0.12.3", "digest 0.9.0", "hmac-drbg", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", + "libsecp256k1-core 0.2.2", + "libsecp256k1-gen-ecmult 0.2.1", + "libsecp256k1-gen-genmult 0.2.1", "rand 0.7.3", "serde", "sha2 0.9.9", "typenum", ] +[[package]] +name = "libsecp256k1" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0452aac8bab02242429380e9b2f94ea20cea2b37e2c1777a1358799bbe97f37" +dependencies = [ + "arrayref", + "base64 0.13.0", + "digest 0.9.0", + "libsecp256k1-core 0.3.0", + "libsecp256k1-gen-ecmult 0.3.0", + "libsecp256k1-gen-genmult 0.3.0", + "rand 0.8.5", + "serde", +] + [[package]] name = "libsecp256k1-core" version = "0.2.2" @@ -2073,13 +2089,33 @@ dependencies = [ "subtle", ] +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + [[package]] name = "libsecp256k1-gen-ecmult" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" dependencies = [ - "libsecp256k1-core", + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core 0.3.0", ] [[package]] @@ -2088,7 +2124,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" dependencies = [ - "libsecp256k1-core", + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core 0.3.0", ] [[package]] @@ -3963,7 +4008,7 @@ version = "1.11.2" dependencies = [ "bincode", "byteorder 1.4.3", - "libsecp256k1", + "libsecp256k1 0.6.0", "log", "solana-measure", "solana-metrics", @@ -4284,6 +4329,7 @@ dependencies = [ name = "solana-bpf-rust-secp256k1-recover" version = "1.11.2" dependencies = [ + "libsecp256k1 0.7.0", "solana-program 1.11.2", ] @@ -4951,7 +4997,7 @@ dependencies = [ "itertools", "js-sys", "lazy_static", - "libsecp256k1", + "libsecp256k1 0.6.0", "log", "num-derive", "num-traits", @@ -4991,7 +5037,7 @@ dependencies = [ "itertools", "js-sys", "lazy_static", - "libsecp256k1", + "libsecp256k1 0.6.0", "log", "memoffset", "num-derive", @@ -5213,7 +5259,7 @@ dependencies = [ "itertools", "js-sys", "lazy_static", - "libsecp256k1", + "libsecp256k1 0.6.0", "log", "memmap2", "num-derive", @@ -5262,7 +5308,7 @@ dependencies = [ "itertools", "js-sys", "lazy_static", - "libsecp256k1", + "libsecp256k1 0.6.0", "log", "memmap2", "num-derive", diff --git a/programs/bpf/rust/secp256k1_recover/Cargo.toml b/programs/bpf/rust/secp256k1_recover/Cargo.toml index d11d8fd0b4c410..fcaa7441e78a4b 100644 --- a/programs/bpf/rust/secp256k1_recover/Cargo.toml +++ b/programs/bpf/rust/secp256k1_recover/Cargo.toml @@ -10,6 +10,8 @@ documentation = "https://docs.rs/solana-bpf-rust-secp256k1-recover" edition = "2021" [dependencies] +# nb version 7 of this crate is required for running on bpf, 6 does not build +libsecp256k1 = { version = "0.7.0", default-features = false } solana-program = { path = "../../../../sdk/program", version = "=1.11.2" } [lib] diff --git a/programs/bpf/rust/secp256k1_recover/src/lib.rs b/programs/bpf/rust/secp256k1_recover/src/lib.rs index 58fe2c7b96ba0f..ac4a5d4668c42a 100644 --- a/programs/bpf/rust/secp256k1_recover/src/lib.rs +++ b/programs/bpf/rust/secp256k1_recover/src/lib.rs @@ -1,11 +1,11 @@ //! Secp256k1Recover Syscall test extern crate solana_program; -use solana_program::{custom_heap_default, custom_panic_default, msg}; +use solana_program::{ + custom_heap_default, custom_panic_default, msg, secp256k1_recover::secp256k1_recover, +}; fn test_secp256k1_recover() { - use solana_program::secp256k1_recover::secp256k1_recover; - let expected: [u8; 64] = [ 0x42, 0xcd, 0x27, 0xe4, 0x0f, 0xdf, 0x7c, 0x97, 0x0a, 0xa2, 0xca, 0x0b, 0x88, 0x5b, 0x96, 0x0f, 0x8b, 0x62, 0x8a, 0x41, 0xa1, 0x81, 0xe7, 0xe6, 0x8e, 0x03, 0xea, 0x0b, 0x84, 0x20, @@ -32,11 +32,57 @@ fn test_secp256k1_recover() { assert_eq!(public_key.to_bytes(), expected); } +/// secp256k1_recover allows malleable signatures +fn test_secp256k1_recover_malleability() { + let message = b"hello world"; + let message_hash = { + let mut hasher = solana_program::keccak::Hasher::default(); + hasher.hash(message); + hasher.result() + }; + + let pubkey_bytes: [u8; 64] = [ + 0x9B, 0xEE, 0x7C, 0x18, 0x34, 0xE0, 0x18, 0x21, 0x7B, 0x40, 0x14, 0x9B, 0x84, 0x2E, 0xFA, + 0x80, 0x96, 0x00, 0x1A, 0x9B, 0x17, 0x88, 0x01, 0x80, 0xA8, 0x46, 0x99, 0x09, 0xE9, 0xC4, + 0x73, 0x6E, 0x39, 0x0B, 0x94, 0x00, 0x97, 0x68, 0xC2, 0x28, 0xB5, 0x55, 0xD3, 0x0C, 0x0C, + 0x42, 0x43, 0xC1, 0xEE, 0xA5, 0x0D, 0xC0, 0x48, 0x62, 0xD3, 0xAE, 0xB0, 0x3D, 0xA2, 0x20, + 0xAC, 0x11, 0x85, 0xEE, + ]; + let signature_bytes: [u8; 64] = [ + 0x93, 0x92, 0xC4, 0x6C, 0x42, 0xF6, 0x31, 0x73, 0x81, 0xD4, 0xB2, 0x44, 0xE9, 0x2F, 0xFC, + 0xE3, 0xF4, 0x57, 0xDD, 0x50, 0xB3, 0xA5, 0x20, 0x26, 0x3B, 0xE7, 0xEF, 0x8A, 0xB0, 0x69, + 0xBB, 0xDE, 0x2F, 0x90, 0x12, 0x93, 0xD7, 0x3F, 0xA0, 0x29, 0x0C, 0x46, 0x4B, 0x97, 0xC5, + 0x00, 0xAD, 0xEA, 0x6A, 0x64, 0x4D, 0xC3, 0x8D, 0x25, 0x24, 0xEF, 0x97, 0x6D, 0xC6, 0xD7, + 0x1D, 0x9F, 0x5A, 0x26, + ]; + let recovery_id: u8 = 0; + + let pubkey = libsecp256k1::PublicKey::parse_slice(&pubkey_bytes, None).unwrap(); + let signature = libsecp256k1::Signature::parse_standard_slice(&signature_bytes).unwrap(); + + // Flip the S value in the signature to make a different but valid signature. + let mut alt_signature = signature.clone(); + alt_signature.s = -alt_signature.s; + let alt_recovery_id = libsecp256k1::RecoveryId::parse(recovery_id ^ 1).unwrap(); + + let alt_signature_bytes = alt_signature.serialize(); + let alt_recovery_id = alt_recovery_id.serialize(); + + let recovered_pubkey = + secp256k1_recover(&message_hash.0, recovery_id, &signature_bytes[..]).unwrap(); + assert_eq!(recovered_pubkey.to_bytes(), pubkey_bytes); + + let alt_recovered_pubkey = + secp256k1_recover(&message_hash.0, alt_recovery_id, &alt_signature_bytes[..]).unwrap(); + assert_eq!(alt_recovered_pubkey.to_bytes(), pubkey_bytes); +} + #[no_mangle] pub extern "C" fn entrypoint(_input: *mut u8) -> u64 { msg!("secp256k1_recover"); test_secp256k1_recover(); + test_secp256k1_recover_malleability(); 0 } diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 7815aaf0d9f2c1..a7db3ebc080c80 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -86,6 +86,7 @@ js-sys = "0.3.57" [dev-dependencies] anyhow = "1.0.57" curve25519-dalek = "3.2.1" +hex = "0.4.3" tiny-bip39 = "0.8.2" [build-dependencies] diff --git a/sdk/program/src/example_mocks.rs b/sdk/program/src/example_mocks.rs index cacc800ef0c640..97b5f8b5c6f873 100644 --- a/sdk/program/src/example_mocks.rs +++ b/sdk/program/src/example_mocks.rs @@ -113,7 +113,7 @@ pub mod solana_client { /// programs. pub mod solana_sdk { pub use crate::{ - address_lookup_table_account, hash, instruction, message, nonce, + address_lookup_table_account, hash, instruction, keccak, message, nonce, pubkey::{self, Pubkey}, system_instruction, system_program, }; @@ -178,6 +178,7 @@ pub mod solana_sdk { pub trait Signers {} + impl Signers for [&T] {} impl Signers for [&T; 1] {} impl Signers for [&T; 2] {} } diff --git a/sdk/program/src/secp256k1_program.rs b/sdk/program/src/secp256k1_program.rs index 5517fcb2e8f491..4bc3de2c71596e 100644 --- a/sdk/program/src/secp256k1_program.rs +++ b/sdk/program/src/secp256k1_program.rs @@ -1,5 +1,10 @@ //! The [secp256k1 native program][np]. //! //! [np]: https://docs.solana.com/developing/runtime-facilities/programs#secp256k1-program +//! +//! Constructors for secp256k1 program instructions, and documentation on the +//! program's usage can be found in [`solana_sdk::secp256k1_instruction`]. +//! +//! [`solana_sdk::secp256k1_instruction`]: https://docs.rs/solana-sdk/latest/solana_sdk/secp256k1_instruction/index.html crate::declare_id!("KeccakSecp256k11111111111111111111111111111"); diff --git a/sdk/program/src/secp256k1_recover.rs b/sdk/program/src/secp256k1_recover.rs index 16451b149da850..150f4b020c65c0 100644 --- a/sdk/program/src/secp256k1_recover.rs +++ b/sdk/program/src/secp256k1_recover.rs @@ -1,4 +1,28 @@ -//! Key recovery from secp256k1 signatures. +//! 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}, @@ -69,6 +93,309 @@ impl Secp256k1Pubkey { } } +/// 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 fuction 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 interpet 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, diff --git a/sdk/src/secp256k1_instruction.rs b/sdk/src/secp256k1_instruction.rs index f284e1f2dab5ab..31c8e3f9337958 100644 --- a/sdk/src/secp256k1_instruction.rs +++ b/sdk/src/secp256k1_instruction.rs @@ -1,3 +1,790 @@ +//! Instructions for the [secp256k1 native program][np]. +//! +//! [np]: https://docs.solana.com/developing/runtime-facilities/programs#secp256k1-program +//! +//! _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 secp26k1 native program peforms flexible verification of [secp256k1] +//! ECDSA signatures, as used by Ethereum. It can verify up to 255 signatures on +//! up to 255 messages, with those signatures, messages, and their public keys +//! arbitrarily distributed across the instruction data of any instructions in +//! the same transaction as the secp256k1 instruction. +//! +//! The secp256k1 native program ID is located in the [`secp256k1_program`] module. +//! +//! The instruction is designed for Ethereum interoperability, but may be useful +//! for other purposes. It operates on Ethereum addresses, which are [`keccak`] +//! hashes of secp256k1 public keys, and internally is implemented using the +//! secp256k1 key recovery algorithm. Ethereum address can be created for +//! secp256k1 public keys with the [`construct_eth_pubkey`] function. +//! +//! [`keccak`]: crate::keccak +//! +//! This instruction does not directly allow for key recovery as in Ethereum's +//! [`ecrecover`] precompile. For that Solana provides the [`secp256k1_recover`] +//! syscall. +//! +//! [secp256k1]: https://en.bitcoin.it/wiki/Secp256k1 +//! [`secp256k1_program`]: solana_program::secp256k1_program +//! [`secp256k1_recover`]: solana_program::secp256k1_recover +//! [`ecrecover`]: https://docs.soliditylang.org/en/v0.8.14/units-and-global-variables.html?highlight=ecrecover#mathematical-and-cryptographic-functions +//! +//! Use cases for the secp256k1 instruction include: +//! +//! - Verifying Ethereum transaction signatures. +//! - Verifying Ethereum [EIP-712] signatures. +//! - Verifying arbitrary secp256k1 signatures. +//! - Signing a single message with multiple signatures. +//! +//! [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 +//! +//! The [`new_secp256k1_instruction`] function is suitable for building a +//! secp256k1 program instruction for basic use cases were a single message must +//! be signed by a known secret key. For other uses cases, including many +//! Ethereum-integration use cases, construction of the secp256k1 instruction +//! must be done manually. +//! +//! # How to use this program +//! +//! Transactions that uses the secp256k1 native program will typically include +//! at least two instructions: one for the secp256k1 program to verify the +//! signatures, and one for a custom program that will check that the secp256k1 +//! instruction data matches what the program expects (using +//! [`load_instruction_at_checked`] or [`get_instruction_relative`]). The +//! signatures, messages, and Ethereum addresses being verified may reside in the +//! instruction data of either of these instructions, or in the instruction data +//! of one or more additional instructions, as long as those instructions are in +//! the same transaction. +//! +//! [`load_instruction_at_checked`]: crate::sysvar::instructions::load_instruction_at_checked +//! [`get_instruction_relative`]: crate::sysvar::instructions::get_instruction_relative +//! +//! Correct use of this program involves multiple steps, in client code and +//! program code: +//! +//! - In the client: +//! - Sign the [`keccak`]-hashed messages with a secp256k1 ECDSA library, +//! like the [`libsecp256k1`] crate. +//! - Build any custom instruction data that contain signature, message, or +//! Ethereum address data that will be used by the secp256k1 instruction. +//! - Build the secp256k1 program instruction data, specifying the number of +//! signatures to verify, the instruction indexes within the transaction, +//! and offsets within those instruction's data, where the signatures, +//! messages, and Ethereum addresses are located. +//! - Build the custom instruction for the program that will check the results +//! of the secp256k1 native program. +//! - Package all instructions into a single transaction and submit them. +//! - In the program: +//! - Load the secp256k1 instruction data with +//! [`load_instruction_at_checked`]. or [`get_instruction_relative`]. +//! - Check that the secp256k1 program ID is equal to +//! [`secp256k1_program::ID`], so that the signature verification cannot be +//! faked with a malicious program. +//! - Check that the public keys and messages are the expected values per +//! the program's requirements. +//! +//! [`secp256k1_program::ID`]: crate::secp256k1_program::ID +//! +//! The signature, message, or Ethereum addresses may reside in the secp256k1 +//! instruction data itself as additional data, their bytes following the bytes +//! of the protocol required by the secp256k1 instruction to locate the +//! signature, message, and Ethereum address data. This is the technique used by +//! `new_secp256k1_instruction` for simple signature verification. +//! +//! The `solana_sdk` crate provides few APIs for building the instructions and +//! transactions necessary for properly using the secp256k1 native program. +//! Many steps must be done manually. +//! +//! The `solana_program` crate provides no APIs to assist in interpreting +//! the the secp256k1 instruction data. It must be done manually. +//! +//! The secp256k1 program is implemented with the [`libsecp256k1`] crate, +//! which clients may also want to use. +//! +//! [`libsecp256k1`]: https://docs.rs/libsecp256k1/latest/libsecp256k1 +//! +//! # Layout and interpretation of the secp256k1 instruction data +//! +//! The secp256k1 instruction data contains: +//! +//! - 1 byte indicating the number of signatures to verify, 0 - 255, +//! - A number of _signature offset_ structures that indicate where in the +//! transaction to locate each signature, message, and Ethereum address. +//! - 0 or more bytes of arbitrary data, which may contain signatures, +//! messages or Ethereum addresses. +//! +//! The signature offset structure is defined by [`SecpSignatureOffsets`], +//! and can be serialized to the correct format with [`bincode::serialize_into`]. +//! Note that the bincode format may not be stable, +//! and callers should ensure they use the same version of `bincode` as the Solana SDK. +//! This data structure is not provided to Solana programs, +//! which are expected to interpret the signature offsets manually. +//! +//! [`bincode::serialize_into`]: https://docs.rs/bincode/1.3.3/bincode/fn.serialize_into.html +//! +//! The serialized signature offset structure has the following 11-byte layout, +//! with data types in little-endian encoding. +//! +//! | index | bytes | type | description | +//! |--------|-------|-------|-------------| +//! | 0 | 2 | `u16` | `signature_offset` - offset to 64-byte signature plus 1-byte recovery ID. | +//! | 2 | 1 | `u8` | `signature_offset_instruction_index` - within the transaction, the index of the transaction whose instruction data contains the signature. | +//! | 3 | 2 | `u16` | `eth_address_offset` - offset to 20-byte Ethereum address. | +//! | 5 | 1 | `u8` | `eth_address_instruction_index` - within the transaction, the index of the instruction whose instruction data contains the Ethereum address. | +//! | 6 | 2 | `u16` | `message_data_offset` - Offset to start of message data. | +//! | 8 | 2 | `u16` | `message_data_size` - Size of message data in bytes. | +//! | 10 | 1 | `u8` | `message_instruction_index` - Within the transaction, the index of the instruction whose instruction data contains the message data. | +//! +//! # 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. +//! +//! For more complete documentation of the subject, and techniques to prevent +//! malleability, see the documentation for the [`secp256k1_recover`] syscall. +//! +//! # Additional security considerations +//! +//! Most programs will want to be conservative about the layout of the secp256k1 instruction +//! to prevent unforseen bugs. The following checks may be desirable: +//! +//! - That there are exactly the expected number of signatures. +//! - That the three indexes, `signature_offset_instruction_index`, +//! `eth_address_instruction_index`, and `message_instruction_index` are as +//! expected, placing the signature, message and Ethereum address in the +//! expected instruction. +//! +//! Loading the secp256k1 instruction data within a program requires access to +//! the [instructions sysvar][is], which must be passed to the program by its +//! caller. Programs must verify the ID of this program to avoid calling an +//! imposter program. This does not need to be done manually though, as long as +//! it is only used through the [`load_instruction_at_checked`] or +//! [`get_instruction_relative`] functions. Both of these functions check their +//! sysvar argument to ensure it is the known instruction sysvar. +//! +//! [is]: crate::sysvar::instructions +//! +//! Programs should _always_ verify that the secp256k1 program ID loaded through +//! the instructions sysvar has the same value as in the [`secp256k1_program`] +//! module. Again this prevents imposter programs. +//! +//! [`secp256k1_program`]: crate::secp256k1_program +//! +//! # Errors +//! +//! The transaction will fail if any of the following are true: +//! +//! - Any signature was not created by the secret key corresponding to the +//! specified public key. +//! - Any signature is invalid. +//! - Any signature is "overflowing", a non-standard condition. +//! - The instruction data is not empty. +//! - The first byte of instruction data is equal to 0 (indicating no signatures), +//! but the instruction data's length is greater than 1. +//! - The instruction data is not long enough to hold the number of signature +//! offsets specified in the first byte. +//! - Any instruction indexes specified in the signature offsets are greater or +//! equal to the number of instructions in the transaction. +//! - Any bounds specified in the signature offsets exceed the bounds of the +//! instruction data to which they are indexed. +//! +//! # Examples +//! +//! Both of the following examples make use of the following module definition +//! to parse the secp256k1 instruction data from within a Solana program. +//! +//! ```no_run +//! mod secp256k1_defs { +//! use solana_program::program_error::ProgramError; +//! use std::iter::Iterator; +//! +//! pub const HASHED_PUBKEY_SERIALIZED_SIZE: usize = 20; +//! pub const SIGNATURE_SERIALIZED_SIZE: usize = 64; +//! pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 11; +//! +//! /// The structure encoded in the secp2256k1 instruction data. +//! pub struct SecpSignatureOffsets { +//! pub signature_offset: u16, +//! pub signature_instruction_index: u8, +//! pub eth_address_offset: u16, +//! pub eth_address_instruction_index: u8, +//! pub message_data_offset: u16, +//! pub message_data_size: u16, +//! pub message_instruction_index: u8, +//! } +//! +//! pub fn iter_signature_offsets( +//! secp256k1_instr_data: &[u8], +//! ) -> Result + '_, ProgramError> { +//! // First element is the number of `SecpSignatureOffsets`. +//! let num_structs = *secp256k1_instr_data +//! .get(0) +//! .ok_or(ProgramError::InvalidArgument)?; +//! +//! let all_structs_size = SIGNATURE_OFFSETS_SERIALIZED_SIZE * num_structs as usize; +//! let all_structs_slice = secp256k1_instr_data +//! .get(1..all_structs_size + 1) +//! .ok_or(ProgramError::InvalidArgument)?; +//! +//! fn decode_u16(chunk: &[u8], index: usize) -> u16 { +//! u16::from_le_bytes(<[u8; 2]>::try_from(&chunk[index..index + 2]).unwrap()) +//! } +//! +//! Ok(all_structs_slice +//! .chunks(SIGNATURE_OFFSETS_SERIALIZED_SIZE) +//! .map(|chunk| SecpSignatureOffsets { +//! signature_offset: decode_u16(chunk, 0), +//! signature_instruction_index: chunk[2], +//! eth_address_offset: decode_u16(chunk, 3), +//! eth_address_instruction_index: chunk[5], +//! message_data_offset: decode_u16(chunk, 6), +//! message_data_size: decode_u16(chunk, 8), +//! message_instruction_index: chunk[10], +//! })) +//! } +//! } +//! ``` +//! +//! ## Example: Signing and verifying with `new_secp256k1_instruction` +//! +//! This example demonstrates the simplest way to use the secp256k1 program, by +//! calling [`new_secp256k1_instruction`] to sign a single message and build the +//! corresponding secp256k1 instruction. +//! +//! This example has two components: a Solana program, and an RPC client that +//! sends a transaction to call it. The RPC client will sign a single message, +//! and the Solana program will introspect the secp256k1 instruction to verify +//! that the signer matches a known authorized public key. +//! +//! The Solana program. Note that it uses `libsecp256k1` version 0.7.0 to parse +//! the secp256k1 signature to prevent malleability. +//! +//! ```no_run +//! # mod secp256k1_defs { +//! # use solana_program::program_error::ProgramError; +//! # use std::iter::Iterator; +//! # +//! # pub const HASHED_PUBKEY_SERIALIZED_SIZE: usize = 20; +//! # pub const SIGNATURE_SERIALIZED_SIZE: usize = 64; +//! # pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 11; +//! # +//! # /// The structure encoded in the secp2256k1 instruction data. +//! # pub struct SecpSignatureOffsets { +//! # pub signature_offset: u16, +//! # pub signature_instruction_index: u8, +//! # pub eth_address_offset: u16, +//! # pub eth_address_instruction_index: u8, +//! # pub message_data_offset: u16, +//! # pub message_data_size: u16, +//! # pub message_instruction_index: u8, +//! # } +//! # +//! # pub fn iter_signature_offsets( +//! # secp256k1_instr_data: &[u8], +//! # ) -> Result + '_, ProgramError> { +//! # // First element is the number of `SecpSignatureOffsets`. +//! # let num_structs = *secp256k1_instr_data +//! # .get(0) +//! # .ok_or(ProgramError::InvalidArgument)?; +//! # +//! # let all_structs_size = SIGNATURE_OFFSETS_SERIALIZED_SIZE * num_structs as usize; +//! # let all_structs_slice = secp256k1_instr_data +//! # .get(1..all_structs_size + 1) +//! # .ok_or(ProgramError::InvalidArgument)?; +//! # +//! # fn decode_u16(chunk: &[u8], index: usize) -> u16 { +//! # u16::from_le_bytes(<[u8; 2]>::try_from(&chunk[index..index + 2]).unwrap()) +//! # } +//! # +//! # Ok(all_structs_slice +//! # .chunks(SIGNATURE_OFFSETS_SERIALIZED_SIZE) +//! # .map(|chunk| SecpSignatureOffsets { +//! # signature_offset: decode_u16(chunk, 0), +//! # signature_instruction_index: chunk[2], +//! # eth_address_offset: decode_u16(chunk, 3), +//! # eth_address_instruction_index: chunk[5], +//! # message_data_offset: decode_u16(chunk, 6), +//! # message_data_size: decode_u16(chunk, 8), +//! # message_instruction_index: chunk[10], +//! # })) +//! # } +//! # } +//! use solana_program::{ +//! account_info::{next_account_info, AccountInfo}, +//! entrypoint::ProgramResult, +//! msg, +//! program_error::ProgramError, +//! secp256k1_program, +//! sysvar, +//! }; +//! +//! /// An Ethereum address corresponding to a secp256k1 secret key that is +//! /// authorized to sign our messages. +//! const AUTHORIZED_ETH_ADDRESS: [u8; 20] = [ +//! 0x18, 0x8a, 0x5c, 0xf2, 0x3b, 0x0e, 0xff, 0xe9, 0xa8, 0xe1, 0x42, 0x64, 0x5b, 0x82, 0x2f, 0x3a, +//! 0x6b, 0x8b, 0x52, 0x35, +//! ]; +//! +//! /// Check the secp256k1 instruction to ensure it was signed by +//! /// `AUTHORIZED_ETH_ADDRESS`s key. +//! /// +//! /// `accounts` is the slice of all accounts passed to the program +//! /// entrypoint. The only account it should contain is the instructions sysvar. +//! fn demo_secp256k1_verify_basic( +//! accounts: &[AccountInfo], +//! ) -> ProgramResult { +//! let account_info_iter = &mut accounts.iter(); +//! +//! // The instructions sysvar gives access to the instructions in the transaction. +//! let instructions_sysvar_account = next_account_info(account_info_iter)?; +//! assert!(sysvar::instructions::check_id( +//! instructions_sysvar_account.key +//! )); +//! +//! // Load the secp256k1 instruction. +//! // `new_secp256k1_instruction` generates an instruction that must be at index 0. +//! let secp256k1_instr = +//! sysvar::instructions::load_instruction_at_checked(0, instructions_sysvar_account)?; +//! +//! // Verify it is a secp256k1 instruction. +//! // This is security-critical - what if the transaction uses an imposter secp256k1 program? +//! assert!(secp256k1_program::check_id(&secp256k1_instr.program_id)); +//! +//! // There must be at least one byte. This is also verified by the runtime, +//! // and doesn't strictly need to be checked. +//! assert!(secp256k1_instr.data.len() > 1); +//! +//! let num_signatures = secp256k1_instr.data[0]; +//! // `new_secp256k1_instruction` generates an instruction that contains one signature. +//! assert_eq!(1, num_signatures); +//! +//! // Load the first and only set of signature offsets. +//! let offsets: secp256k1_defs::SecpSignatureOffsets = +//! secp256k1_defs::iter_signature_offsets(&secp256k1_instr.data)? +//! .next() +//! .ok_or(ProgramError::InvalidArgument)?; +//! +//! // `new_secp256k1_instruction` generates an instruction that only uses instruction index 0. +//! assert_eq!(0, offsets.signature_instruction_index); +//! assert_eq!(0, offsets.eth_address_instruction_index); +//! assert_eq!(0, offsets.message_instruction_index); +//! +//! // 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 = &secp256k1_instr.data[offsets.signature_offset as usize +//! ..offsets.signature_offset as usize + secp256k1_defs::SIGNATURE_SERIALIZED_SIZE]; +//! let signature = libsecp256k1::Signature::parse_standard_slice(signature) +//! .map_err(|_| ProgramError::InvalidArgument)?; +//! +//! if signature.s.is_high() { +//! msg!("signature with high-s value"); +//! return Err(ProgramError::InvalidArgument); +//! } +//! } +//! +//! // There is likely at least one more verification step a real program needs +//! // to do here to ensure it trusts the secp256k1 instruction, e.g.: +//! // +//! // - verify the tx signer is authorized +//! // - verify the secp256k1 signer is authorized +//! +//! // Here we are checking the secp256k1 pubkey against a known authorized pubkey. +//! let eth_address = &secp256k1_instr.data[offsets.eth_address_offset as usize +//! ..offsets.eth_address_offset as usize + secp256k1_defs::HASHED_PUBKEY_SERIALIZED_SIZE]; +//! +//! if eth_address != AUTHORIZED_ETH_ADDRESS { +//! return Err(ProgramError::InvalidArgument); +//! } +//! +//! Ok(()) +//! } +//! ``` +//! +//! The client program: +//! +//! ```no_run +//! # use solana_sdk::example_mocks::solana_client; +//! use anyhow::Result; +//! use solana_client::rpc_client::RpcClient; +//! use solana_sdk::{ +//! instruction::{AccountMeta, Instruction}, +//! secp256k1_instruction, +//! signature::{Keypair, Signer}, +//! sysvar, +//! transaction::Transaction, +//! }; +//! +//! fn demo_secp256k1_verify_basic( +//! payer_keypair: &Keypair, +//! secp256k1_secret_key: &libsecp256k1::SecretKey, +//! client: &RpcClient, +//! program_keypair: &Keypair, +//! ) -> Result<()> { +//! // Internally to `new_secp256k1_instruction` and +//! // `secp256k_instruction::verify` (the secp256k1 program), this message is +//! // keccak-hashed before signing. +//! let msg = b"hello world"; +//! let secp256k1_instr = secp256k1_instruction::new_secp256k1_instruction(&secp256k1_secret_key, msg); +//! +//! let program_instr = Instruction::new_with_bytes( +//! program_keypair.pubkey(), +//! &[], +//! vec![ +//! AccountMeta::new_readonly(sysvar::instructions::ID, false) +//! ], +//! ); +//! +//! let blockhash = client.get_latest_blockhash()?; +//! let tx = Transaction::new_signed_with_payer( +//! &[secp256k1_instr, program_instr], +//! Some(&payer_keypair.pubkey()), +//! &[payer_keypair], +//! blockhash, +//! ); +//! +//! client.send_and_confirm_transaction(&tx)?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! ## Example: Verifying multiple signatures in one instruction +//! +//! This examples demonstrates manually creating a secp256k1 instruction +//! containing many signatures, and a Solana program that parses them all. This +//! example on its own has no practical purpose. It simply demonstrates advanced +//! use of the secp256k1 program. +//! +//! Recall that the secp256k1 program will accept signatures, messages, and +//! Ethereum addresses that reside in any instruction contained in the same +//! transaction. In the _previous_ example, the Solana program asserted that all +//! signatures, messages, and addresses were stored in the instruction at 0. In +//! this next example the Solana program supports signatures, messages, and +//! addresses stored in any instruction. For simplicity the client still only +//! stores signatures, messages, and addresses in a single instruction, the +//! secp256k1 instruction. The code for storing this data across multiple +//! instructions would be complex, and may not be necessary in practice. +//! +//! This example has two components: a Solana program, and an RPC client that +//! sends a transaction to call it. +//! +//! The Solana program: +//! +//! ```no_run +//! # mod secp256k1_defs { +//! # use solana_program::program_error::ProgramError; +//! # use std::iter::Iterator; +//! # +//! # pub const HASHED_PUBKEY_SERIALIZED_SIZE: usize = 20; +//! # pub const SIGNATURE_SERIALIZED_SIZE: usize = 64; +//! # pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 11; +//! # +//! # /// The structure encoded in the secp2256k1 instruction data. +//! # pub struct SecpSignatureOffsets { +//! # pub signature_offset: u16, +//! # pub signature_instruction_index: u8, +//! # pub eth_address_offset: u16, +//! # pub eth_address_instruction_index: u8, +//! # pub message_data_offset: u16, +//! # pub message_data_size: u16, +//! # pub message_instruction_index: u8, +//! # } +//! # +//! # pub fn iter_signature_offsets( +//! # secp256k1_instr_data: &[u8], +//! # ) -> Result + '_, ProgramError> { +//! # // First element is the number of `SecpSignatureOffsets`. +//! # let num_structs = *secp256k1_instr_data +//! # .get(0) +//! # .ok_or(ProgramError::InvalidArgument)?; +//! # +//! # let all_structs_size = SIGNATURE_OFFSETS_SERIALIZED_SIZE * num_structs as usize; +//! # let all_structs_slice = secp256k1_instr_data +//! # .get(1..all_structs_size + 1) +//! # .ok_or(ProgramError::InvalidArgument)?; +//! # +//! # fn decode_u16(chunk: &[u8], index: usize) -> u16 { +//! # u16::from_le_bytes(<[u8; 2]>::try_from(&chunk[index..index + 2]).unwrap()) +//! # } +//! # +//! # Ok(all_structs_slice +//! # .chunks(SIGNATURE_OFFSETS_SERIALIZED_SIZE) +//! # .map(|chunk| SecpSignatureOffsets { +//! # signature_offset: decode_u16(chunk, 0), +//! # signature_instruction_index: chunk[2], +//! # eth_address_offset: decode_u16(chunk, 3), +//! # eth_address_instruction_index: chunk[5], +//! # message_data_offset: decode_u16(chunk, 6), +//! # message_data_size: decode_u16(chunk, 8), +//! # message_instruction_index: chunk[10], +//! # })) +//! # } +//! # } +//! use solana_program::{ +//! account_info::{next_account_info, AccountInfo}, +//! entrypoint::ProgramResult, +//! msg, +//! program_error::ProgramError, +//! secp256k1_program, +//! sysvar, +//! }; +//! +//! /// A struct to hold the values specified in the `SecpSignatureOffsets` struct. +//! struct SecpSignature { +//! signature: [u8; secp256k1_defs::SIGNATURE_SERIALIZED_SIZE], +//! recovery_id: u8, +//! eth_address: [u8; secp256k1_defs::HASHED_PUBKEY_SERIALIZED_SIZE], +//! message: Vec, +//! } +//! +//! /// Load all signatures indicated in the secp256k1 instruction. +//! /// +//! /// This function is quite inefficient for reloading the same instructions +//! /// repeatedly and making copies and allocations. +//! fn load_signatures( +//! secp256k1_instr_data: &[u8], +//! instructions_sysvar_account: &AccountInfo, +//! ) -> Result, ProgramError> { +//! let mut sigs = vec![]; +//! for offsets in secp256k1_defs::iter_signature_offsets(secp256k1_instr_data)? { +//! let signature_instr = sysvar::instructions::load_instruction_at_checked( +//! offsets.signature_instruction_index as usize, +//! instructions_sysvar_account, +//! )?; +//! let eth_address_instr = sysvar::instructions::load_instruction_at_checked( +//! offsets.eth_address_instruction_index as usize, +//! instructions_sysvar_account, +//! )?; +//! let message_instr = sysvar::instructions::load_instruction_at_checked( +//! offsets.message_instruction_index as usize, +//! instructions_sysvar_account, +//! )?; +//! +//! // These indexes must all be valid because the runtime already verified them. +//! let signature = &signature_instr.data[offsets.signature_offset as usize +//! ..offsets.signature_offset as usize + secp256k1_defs::SIGNATURE_SERIALIZED_SIZE]; +//! let recovery_id = signature_instr.data +//! [offsets.signature_offset as usize + secp256k1_defs::SIGNATURE_SERIALIZED_SIZE]; +//! let eth_address = ð_address_instr.data[offsets.eth_address_offset as usize +//! ..offsets.eth_address_offset as usize + secp256k1_defs::HASHED_PUBKEY_SERIALIZED_SIZE]; +//! let message = &message_instr.data[offsets.message_data_offset as usize +//! ..offsets.message_data_offset as usize + offsets.message_data_size as usize]; +//! +//! let signature = +//! <[u8; secp256k1_defs::SIGNATURE_SERIALIZED_SIZE]>::try_from(signature).unwrap(); +//! let eth_address = +//! <[u8; secp256k1_defs::HASHED_PUBKEY_SERIALIZED_SIZE]>::try_from(eth_address).unwrap(); +//! let message = Vec::from(message); +//! +//! sigs.push(SecpSignature { +//! signature, +//! recovery_id, +//! eth_address, +//! message, +//! }) +//! } +//! Ok(sigs) +//! } +//! +//! fn demo_secp256k1_custom_many( +//! accounts: &[AccountInfo], +//! ) -> ProgramResult { +//! let account_info_iter = &mut accounts.iter(); +//! +//! let instructions_sysvar_account = next_account_info(account_info_iter)?; +//! assert!(sysvar::instructions::check_id( +//! instructions_sysvar_account.key +//! )); +//! +//! let secp256k1_instr = +//! sysvar::instructions::get_instruction_relative(-1, instructions_sysvar_account)?; +//! +//! assert!(secp256k1_program::check_id(&secp256k1_instr.program_id)); +//! +//! let signatures = load_signatures(&secp256k1_instr.data, instructions_sysvar_account)?; +//! for (idx, signature_bundle) in signatures.iter().enumerate() { +//! let signature = hex::encode(&signature_bundle.signature); +//! let eth_address = hex::encode(&signature_bundle.eth_address); +//! let message = hex::encode(&signature_bundle.message); +//! msg!("sig {}: {:?}", idx, signature); +//! msg!("recid: {}: {}", idx, signature_bundle.recovery_id); +//! msg!("eth address {}: {}", idx, eth_address); +//! msg!("message {}: {}", idx, message); +//! } +//! +//! Ok(()) +//! } +//! ``` +//! +//! The client program: +//! +//! ```no_run +//! # use solana_sdk::example_mocks::solana_client; +//! use anyhow::Result; +//! use solana_client::rpc_client::RpcClient; +//! use solana_sdk::{ +//! instruction::{AccountMeta, Instruction}, +//! keccak, +//! secp256k1_instruction::{ +//! self, SecpSignatureOffsets, HASHED_PUBKEY_SERIALIZED_SIZE, +//! SIGNATURE_OFFSETS_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE, +//! }, +//! signature::{Keypair, Signer}, +//! sysvar, +//! transaction::Transaction, +//! }; +//! +//! /// A struct to hold the values specified in the `SecpSignatureOffsets` struct. +//! struct SecpSignature { +//! signature: [u8; SIGNATURE_SERIALIZED_SIZE], +//! recovery_id: u8, +//! eth_address: [u8; HASHED_PUBKEY_SERIALIZED_SIZE], +//! message: Vec, +//! } +//! +//! /// Create the instruction data for a secp256k1 instruction. +//! /// +//! /// `instruction_index` is the index the secp256k1 instruction will appear +//! /// within the transaction. For simplicity, this function only supports packing +//! /// the signatures into the secp256k1 instruction data, and not into any other +//! /// instructions within the transaction. +//! fn make_secp256k1_instruction_data( +//! signatures: &[SecpSignature], +//! instruction_index: u8, +//! ) -> Result> { +//! assert!(signatures.len() <= u8::max_value().into()); +//! +//! // We're going to pack all the signatures into the secp256k1 instruction data. +//! // Before our signatures though is the signature offset structures +//! // the secp256k1 program parses to find those signatures. +//! // This value represents the byte offset where the signatures begin. +//! let data_start = 1 + signatures.len() * SIGNATURE_OFFSETS_SERIALIZED_SIZE; +//! +//! let mut signature_offsets = vec![]; +//! let mut signature_buffer = vec![]; +//! +//! for signature_bundle in signatures { +//! let data_start = data_start +//! .checked_add(signature_buffer.len()) +//! .expect("overflow"); +//! +//! let signature_offset = data_start; +//! let eth_address_offset = data_start +//! .checked_add(SIGNATURE_SERIALIZED_SIZE + 1) +//! .expect("overflow"); +//! let message_data_offset = eth_address_offset +//! .checked_add(HASHED_PUBKEY_SERIALIZED_SIZE) +//! .expect("overflow"); +//! let message_data_size = signature_bundle.message.len(); +//! +//! let signature_offset = u16::try_from(signature_offset)?; +//! let eth_address_offset = u16::try_from(eth_address_offset)?; +//! let message_data_offset = u16::try_from(message_data_offset)?; +//! let message_data_size = u16::try_from(message_data_size)?; +//! +//! signature_offsets.push(SecpSignatureOffsets { +//! signature_offset, +//! signature_instruction_index: instruction_index, +//! eth_address_offset, +//! eth_address_instruction_index: instruction_index, +//! message_data_offset, +//! message_data_size, +//! message_instruction_index: instruction_index, +//! }); +//! +//! signature_buffer.extend(signature_bundle.signature); +//! signature_buffer.push(signature_bundle.recovery_id); +//! signature_buffer.extend(&signature_bundle.eth_address); +//! signature_buffer.extend(&signature_bundle.message); +//! } +//! +//! let mut instr_data = vec![]; +//! instr_data.push(signatures.len() as u8); +//! +//! for offsets in signature_offsets { +//! let offsets = bincode::serialize(&offsets)?; +//! instr_data.extend(offsets); +//! } +//! +//! instr_data.extend(signature_buffer); +//! +//! Ok(instr_data) +//! } +//! +//! fn demo_secp256k1_custom_many( +//! payer_keypair: &Keypair, +//! client: &RpcClient, +//! program_keypair: &Keypair, +//! ) -> Result<()> { +//! // Sign some messages. +//! let mut signatures = vec![]; +//! for idx in 0..2 { +//! let secret_key = libsecp256k1::SecretKey::random(&mut rand::thread_rng()); +//! let message = format!("hello world {}", idx).into_bytes(); +//! 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, &secret_key); +//! let signature = signature.serialize(); +//! let recovery_id = recovery_id.serialize(); +//! +//! let public_key = libsecp256k1::PublicKey::from_secret_key(&secret_key); +//! let eth_address = secp256k1_instruction::construct_eth_pubkey(&public_key); +//! +//! signatures.push(SecpSignature { +//! signature, +//! recovery_id, +//! eth_address, +//! message, +//! }); +//! } +//! +//! let secp256k1_instr_data = make_secp256k1_instruction_data(&signatures, 0)?; +//! let secp256k1_instr = Instruction::new_with_bytes( +//! solana_sdk::secp256k1_program::ID, +//! &secp256k1_instr_data, +//! vec![], +//! ); +//! +//! let program_instr = Instruction::new_with_bytes( +//! program_keypair.pubkey(), +//! &[], +//! vec![ +//! AccountMeta::new_readonly(sysvar::instructions::ID, false) +//! ], +//! ); +//! +//! let blockhash = client.get_latest_blockhash()?; +//! let tx = Transaction::new_signed_with_payer( +//! &[secp256k1_instr, program_instr], +//! Some(&payer_keypair.pubkey()), +//! &[payer_keypair], +//! blockhash, +//! ); +//! +//! client.send_and_confirm_transaction(&tx)?; +//! +//! Ok(()) +//! } +//! ``` + #![cfg(feature = "full")] use { @@ -18,17 +805,45 @@ pub const SIGNATURE_SERIALIZED_SIZE: usize = 64; pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 11; pub const DATA_START: usize = SIGNATURE_OFFSETS_SERIALIZED_SIZE + 1; +/// Offsets of signature data within a secp256k1 instruction. +/// +/// See the [module documentation][md] for a complete description. +/// +/// [md]: self #[derive(Default, Serialize, Deserialize, Debug)] pub struct SecpSignatureOffsets { - pub signature_offset: u16, // offset to [signature,recovery_id] of 64+1 bytes + /// Offset to 64-byte signature plus 1-byte recovery ID. + pub signature_offset: u16, + /// Within the transaction, the index of the instruction whose instruction data contains the signature. pub signature_instruction_index: u8, - pub eth_address_offset: u16, // offset to eth_address of 20 bytes + /// Offset to 20-byte Ethereum address. + pub eth_address_offset: u16, + /// Within the transaction, the index of the instruction whose instruction data contains the address. pub eth_address_instruction_index: u8, - pub message_data_offset: u16, // offset to start of message data - pub message_data_size: u16, // size of message data + /// Offset to start of message data. + pub message_data_offset: u16, + /// Size of message data in bytes. + pub message_data_size: u16, + /// Within the transaction, the index of the instruction whose instruction data contains the message. pub message_instruction_index: u8, } +/// Sign a message and create a secp256k1 program instruction to verify the signature. +/// +/// This function is suitable for simple uses of the secp256k1 program. +/// More complex uses must encode the secp256k1 instruction data manually. +/// See the [module documentation][md] for examples. +/// +/// [md]: self +/// +/// The instruction generated by this function must be the first instruction +/// included in a transaction or it will not verify. The +/// [`SecpSignatureOffsets`] structure encoded in the instruction data specify +/// the instruction indexes as 0. +/// +/// `message_arr` is hashed with the [`keccak`] hash function prior to signing. +/// +/// [`keccak`]: crate::keccak pub fn new_secp256k1_instruction( priv_key: &libsecp256k1::SecretKey, message_arr: &[u8], @@ -91,6 +906,7 @@ pub fn new_secp256k1_instruction( } } +/// Creates an Ethereum address from a secp256k1 public key. pub fn construct_eth_pubkey( pubkey: &libsecp256k1::PublicKey, ) -> [u8; HASHED_PUBKEY_SERIALIZED_SIZE] { @@ -100,6 +916,19 @@ pub fn construct_eth_pubkey( addr } +/// Verifies the signatures specified in the secp256k1 instruction data. +/// +/// This is same the verification routine executed by the runtime's secp256k1 native program, +/// and is primarily of use to the runtime. +/// +/// `data` is the secp256k1 program's instruction data. `instruction_datas` is +/// the full slice of instruction datas for all instructions in the transaction, +/// including the secp256k1 program's instruction data. +/// +/// `feature_set` is the set of active Solana features. It is used to enable or +/// disable a few minor additional checks that were activated on chain +/// subsequent to the addition of the secp256k1 native program. For many +/// purposes passing `Arc::new` is reasonable. pub fn verify( data: &[u8], instruction_datas: &[&[u8]], @@ -220,6 +1049,7 @@ pub mod test { crate::{ feature_set, hash::Hash, + keccak, secp256k1_instruction::{ new_secp256k1_instruction, SecpSignatureOffsets, SIGNATURE_OFFSETS_SERIALIZED_SIZE, }, @@ -449,4 +1279,74 @@ pub mod test { ); assert!(tx.verify_precompiles(&feature_set).is_err()); } + + // Signatures are malleable. + #[test] + fn test_malleability() { + solana_logger::setup(); + + let secret_key = libsecp256k1::SecretKey::random(&mut thread_rng()); + let public_key = libsecp256k1::PublicKey::from_secret_key(&secret_key); + let eth_address = construct_eth_pubkey(&public_key); + + let message = b"hello"; + 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, &secret_key); + + // Flip the S value in the signature to make a different but valid signature. + let mut alt_signature = signature; + alt_signature.s = -alt_signature.s; + let alt_recovery_id = libsecp256k1::RecoveryId::parse(recovery_id.serialize() ^ 1).unwrap(); + + let mut data: Vec = vec![]; + let mut both_offsets = vec![]; + + // Verify both signatures of the same message. + let sigs = [(signature, recovery_id), (alt_signature, alt_recovery_id)]; + for (signature, recovery_id) in sigs.iter() { + let signature_offset = data.len(); + data.extend(signature.serialize()); + data.push(recovery_id.serialize()); + let eth_address_offset = data.len(); + data.extend(ð_address); + let message_data_offset = data.len(); + data.extend(message); + + let data_start = 1 + SIGNATURE_OFFSETS_SERIALIZED_SIZE * 2; + + let offsets = SecpSignatureOffsets { + signature_offset: (signature_offset + data_start) as u16, + signature_instruction_index: 0, + eth_address_offset: (eth_address_offset + data_start) as u16, + eth_address_instruction_index: 0, + message_data_offset: (message_data_offset + data_start) as u16, + message_data_size: message.len() as u16, + message_instruction_index: 0, + }; + + both_offsets.push(offsets); + } + + let mut instruction_data: Vec = vec![2]; + + for offsets in both_offsets { + let offsets = bincode::serialize(&offsets).unwrap(); + instruction_data.extend(offsets); + } + + instruction_data.extend(data); + + verify( + &instruction_data, + &[&instruction_data], + &Arc::new(FeatureSet::all_enabled()), + ) + .unwrap(); + } }