diff --git a/.github/workflows/client-targets.yml b/.github/workflows/client-targets.yml index 2147abde891089..ab15b9786c2162 100644 --- a/.github/workflows/client-targets.yml +++ b/.github/workflows/client-targets.yml @@ -34,6 +34,11 @@ jobs: steps: - uses: actions/checkout@v4 + # This can be removed once cargo-ndk >= 3.5.4 is used. + - name: Setup environment for Android NDK + run: | + echo "RANLIB=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib" >> $GITHUB_ENV + - run: cargo install cargo-ndk@2.12.2 - name: Setup Rust diff --git a/Cargo.lock b/Cargo.lock index 04f2c5bb5c1466..69e7c92db8349a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8052,6 +8052,7 @@ dependencies = [ "solana-sdk", "solana-sdk-macro", "solana-secp256k1-recover", + "solana-secp256r1-program", "solana-serde-varint", "solana-short-vec", "solana-signature", @@ -8086,6 +8087,20 @@ dependencies = [ "thiserror", ] +[[package]] +name = "solana-secp256r1-program" +version = "2.1.1" +dependencies = [ + "bytemuck", + "openssl", + "solana-feature-set", + "solana-instruction", + "solana-logger", + "solana-precompile-error", + "solana-pubkey", + "solana-sdk", +] + [[package]] name = "solana-security-txt" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index 9c5b178ab71821..5b629d3431d665 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,6 +135,7 @@ members = [ "sdk/pubkey", "sdk/rent", "sdk/sanitize", + "sdk/secp256r1-program", "sdk/serde-varint", "sdk/serialize-utils", "sdk/sha256-hasher", @@ -475,6 +476,7 @@ solana-rayon-threadlimit = { path = "rayon-threadlimit", version = "=2.1.1" } solana-remote-wallet = { path = "remote-wallet", version = "=2.1.1", default-features = false } solana-rent = { path = "sdk/rent", version = "=2.1.1", default-features = false } solana-sanitize = { path = "sdk/sanitize", version = "=2.1.1" } +solana-secp256r1-program = { path = "sdk/secp256r1-program", version = "=2.1.1", default-features = false } solana-serde-varint = { path = "sdk/serde-varint", version = "=2.1.1" } solana-serialize-utils = { path = "sdk/serialize-utils", version = "=2.1.1" } solana-sha256-hasher = { path = "sdk/sha256-hasher", version = "=2.1.1" } diff --git a/client/Cargo.toml b/client/Cargo.toml index d68ca8c22e36bf..f138b2e7a10cf2 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -27,7 +27,7 @@ solana-quic-client = { workspace = true } solana-rpc-client = { workspace = true, features = ["default"] } solana-rpc-client-api = { workspace = true } solana-rpc-client-nonce-utils = { workspace = true } -solana-sdk = { workspace = true } +solana-sdk = { workspace = true, features = ["openssl-vendored"] } solana-streamer = { workspace = true } solana-thin-client = { workspace = true } solana-tpu-client = { workspace = true, features = ["default"] } diff --git a/docs/src/runtime/programs.md b/docs/src/runtime/programs.md index bcb43b1ad51070..f7df53bed530ac 100644 --- a/docs/src/runtime/programs.md +++ b/docs/src/runtime/programs.md @@ -168,6 +168,71 @@ also receive data from the transaction itself. Cost of the transaction will count the number of signatures to verify multiplied by the signature cost verify multiplier. +## Secp256r1 Program + +The program for verifying secp256r1 signatures. It takes a secp256r1 signature, +a public key, and a message. Up to 8 signatures can be verified. If any of the +signatures fail to verify, an error is returned. + +- Program id: `Secp256r1SigVerify1111111111111111111111111` +- Instructions: [secp256r1_instruction](https://docs.rs/solana-secp256r1) + +The secp256r1 program processes an instruction. The first `u8` is a count of the number of signatures to check, followed by a single byte padding. After that, the following struct is serialized, one for each signature to check: + +```rust +struct Secp256r1SignatureOffsets { + signature_offset: u16, // offset to compact secp256r1 signature of 64 bytes + signature_instruction_index: u16, // instruction index to find signature + public_key_offset: u16, // offset to compressed public key of 33 bytes + public_key_instruction_index: u16, // instruction index to find public key + message_data_offset: u16, // offset to start of message data + message_data_size: u16, // size of message data + message_instruction_index: u16, // index of instruction data to get message data +} + +``` + +The pseudo code of the signature verification: +``` +process_instruction() { + if data.len() < SIGNATURE_OFFSETS_START { + return Error + } + + num_signatures = data[0] as usize + if num_signatures == 0 || num_signatures > 8 { + return Error + } + + expected_data_size = num_signatures * SIGNATURE_OFFSETS_SERIALIZED_SIZE + SIGNATURE_OFFSETS_START + if data.len() < expected_data_size { + return Error + } + + for i in 0..num_signatures { + offsets = parse_signature_offsets(data, i) + + signature = get_data_slice(data, instruction_datas, offsets.signature_instruction_index, offsets.signature_offset, SIGNATURE_SERIALIZED_SIZE) + + if s > half_curve_order { + return Error + } + + pubkey = get_data_slice(data, instruction_datas, offsets.public_key_instruction_index, offsets.public_key_offset, COMPRESSED_PUBKEY_SERIALIZED_SIZE) + + message = get_data_slice(data, instruction_datas, offsets.message_instruction_index, offsets.message_data_offset, offsets.message_data_size) + + if !verify_signature(signature, pubkey, message) { + return Error + } + } + + return Success +} +``` +Note: Low S values are enforced for all signatures to avoid accidental signature +malleability. + ### Optimization notes The operation will have to take place after (at least partial) deserialization, diff --git a/ledger-tool/Cargo.toml b/ledger-tool/Cargo.toml index abfa07ade49d38..d5f18defbc15f9 100644 --- a/ledger-tool/Cargo.toml +++ b/ledger-tool/Cargo.toml @@ -45,7 +45,7 @@ solana-measure = { workspace = true } solana-program-runtime = { workspace = true } solana-rpc = { workspace = true } solana-runtime = { workspace = true, features = ["dev-context-only-utils"] } -solana-sdk = { workspace = true } +solana-sdk = { workspace = true, features = ["openssl-vendored"] } solana-stake-program = { workspace = true } solana-storage-bigtable = { workspace = true } solana-streamer = { workspace = true } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 8322f0949e2fd1..8235d7e407adfe 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6803,6 +6803,7 @@ dependencies = [ "solana-sanitize", "solana-sdk-macro", "solana-secp256k1-recover", + "solana-secp256r1-program", "solana-serde-varint", "solana-short-vec", "solana-signature", @@ -6831,6 +6832,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "solana-secp256r1-program" +version = "2.1.1" +dependencies = [ + "bytemuck", + "openssl", + "solana-feature-set", + "solana-instruction", + "solana-precompile-error", + "solana-pubkey", +] + [[package]] name = "solana-security-txt" version = "1.1.1" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 59e9cfabbbfe22..332fccab346d11 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -50,6 +50,8 @@ frozen-abi = [ "solana-signature/frozen-abi", "solana-transaction-error/frozen-abi" ] +# Enables the "vendored" feature of openssl inside of secp256r1-program +openssl-vendored = ["solana-secp256r1-program/openssl-vendored"] [dependencies] bincode = { workspace = true } @@ -111,6 +113,7 @@ solana-pubkey = { workspace = true, default-features = false, features = ["std"] solana-sanitize = { workspace = true } solana-sdk-macro = { workspace = true } solana-secp256k1-recover = { workspace = true } +solana-secp256r1-program = { workspace = true, default-features = false } solana-serde-varint = { workspace = true } solana-short-vec = { workspace = true } solana-signature = { workspace = true, features = [ diff --git a/sdk/feature-set/src/lib.rs b/sdk/feature-set/src/lib.rs index 91ed7ae78fe2ba..e7e41251e7bfd9 100644 --- a/sdk/feature-set/src/lib.rs +++ b/sdk/feature-set/src/lib.rs @@ -873,6 +873,10 @@ pub mod disable_account_loader_special_case { solana_pubkey::declare_id!("EQUMpNFr7Nacb1sva56xn1aLfBxppEoSBH8RRVdkcD1x"); } +pub mod enable_secp256r1_precompile { + solana_pubkey::declare_id!("sr11RdZWgbHTHxSroPALe6zgaT5A1K9LcE4nfsZS4gi"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -1086,6 +1090,7 @@ lazy_static! { (disable_sbpf_v1_execution::id(), "Disables execution of SBPFv1 programs"), (reenable_sbpf_v1_execution::id(), "Re-enables execution of SBPFv1 programs"), (disable_account_loader_special_case::id(), "Disable account loader special case #3513"), + (enable_secp256r1_precompile::id(), "Enable secp256r1 precompile SIMD-0075"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/sdk/secp256r1-program/Cargo.toml b/sdk/secp256r1-program/Cargo.toml new file mode 100644 index 00000000000000..941bda03285f6a --- /dev/null +++ b/sdk/secp256r1-program/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "solana-secp256r1-program" +description = "Precompile implementation for the secp256r1 elliptic curve." +documentation = "https://docs.rs/solana-secp256r1" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +bytemuck = { workspace = true, features = ["derive"] } +solana-feature-set = { workspace = true } +solana-precompile-error = { workspace = true } +solana-pubkey = { workspace = true } + +[target.'cfg(all(not(target_arch = "wasm32"), not(target_os = "solana")))'.dependencies] +solana-instruction = { workspace = true, features = ["std"] } +openssl = { workspace = true } + +[dev-dependencies] +solana-logger = { workspace = true } +solana-sdk = { path = "../" } + +[features] +default = [] +openssl-vendored = ["openssl/vendored"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + +[lints] +workspace = true diff --git a/sdk/secp256r1-program/src/lib.rs b/sdk/secp256r1-program/src/lib.rs new file mode 100644 index 00000000000000..334cbb78c4515c --- /dev/null +++ b/sdk/secp256r1-program/src/lib.rs @@ -0,0 +1,753 @@ +//! Instructions for the [secp256r1 native program][np]. +//! [np]: https://docs.solana.com/developing/runtime-facilities/programs#secp256r1-program +//! +//! Note on Signature Malleability: +//! This precompile requires low-S values in signatures (s <= half_curve_order) to prevent signature malleability. +//! Signature malleability means that for a valid signature (r,s), (r, order-s) is also valid for the +//! same message and public key. +//! +//! This property can be problematic for developers who assume each signature is unique. Without enforcing +//! low-S values, the same message and key can produce two different valid signatures, potentially breaking +//! replay protection schemes that rely on signature uniqueness. +solana_pubkey::declare_id!("Secp256r1SigVerify1111111111111111111111111"); + +use bytemuck::{Pod, Zeroable}; + +#[derive(Default, Debug, Copy, Clone, Zeroable, Pod, Eq, PartialEq)] +#[repr(C)] +pub struct Secp256r1SignatureOffsets { + /// Offset to compact secp256r1 signature of 64 bytes + pub signature_offset: u16, + + /// Instruction index where the signature can be found + pub signature_instruction_index: u16, + + /// Offset to compressed public key of 33 bytes + pub public_key_offset: u16, + + /// Instruction index where the public key can be found + pub public_key_instruction_index: u16, + + /// Offset to the start of message data + pub message_data_offset: u16, + + /// Size of message data in bytes + pub message_data_size: u16, + + /// Instruction index where the message data can be found + pub message_instruction_index: u16, +} + +#[cfg(all(not(target_arch = "wasm32"), not(target_os = "solana")))] +mod target_arch { + use { + crate::Secp256r1SignatureOffsets, + bytemuck::bytes_of, + openssl::{ + bn::{BigNum, BigNumContext}, + ec::{EcGroup, EcKey, EcPoint}, + ecdsa::EcdsaSig, + nid::Nid, + pkey::{PKey, Private}, + sign::{Signer, Verifier}, + }, + solana_feature_set::FeatureSet, + solana_instruction::Instruction, + solana_precompile_error::PrecompileError, + }; + + pub const COMPRESSED_PUBKEY_SERIALIZED_SIZE: usize = 33; + pub const SIGNATURE_SERIALIZED_SIZE: usize = 64; + pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 14; + pub const SIGNATURE_OFFSETS_START: usize = 2; + pub const DATA_START: usize = SIGNATURE_OFFSETS_SERIALIZED_SIZE + SIGNATURE_OFFSETS_START; + + // Order as defined in SEC2: 2.7.2 Recommended Parameters secp256r1 + pub const SECP256R1_ORDER: [u8; FIELD_SIZE] = [ + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, + 0x25, 0x51, + ]; + + // Computed SECP256R1_ORDER - 1 + pub const SECP256R1_ORDER_MINUS_ONE: [u8; FIELD_SIZE] = [ + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, + 0x25, 0x50, + ]; + + // Computed half order + const SECP256R1_HALF_ORDER: [u8; FIELD_SIZE] = [ + 0x7F, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xDE, 0x73, 0x7D, 0x56, 0xD3, 0x8B, 0xCF, 0x42, 0x79, 0xDC, 0xE5, 0x61, 0x7E, 0x31, + 0x92, 0xA8, + ]; + // Field size in bytes + const FIELD_SIZE: usize = 32; + + pub fn new_secp256r1_instruction( + message: &[u8], + signing_key: EcKey, + ) -> Result> { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?; + if signing_key.group().curve_name() != Some(Nid::X9_62_PRIME256V1) { + return Err(("Signing key must be on the secp256r1 curve".to_string()).into()); + } + + let mut ctx = BigNumContext::new()?; + let pubkey = signing_key.public_key().to_bytes( + &group, + openssl::ec::PointConversionForm::COMPRESSED, + &mut ctx, + )?; + + let signing_key_pkey = PKey::from_ec_key(signing_key)?; + + let mut signer = Signer::new(openssl::hash::MessageDigest::sha256(), &signing_key_pkey)?; + signer.update(message)?; + let signature = signer.sign_to_vec()?; + + let ecdsa_sig = EcdsaSig::from_der(&signature)?; + let r = ecdsa_sig.r().to_vec(); + let s = ecdsa_sig.s().to_vec(); + let mut signature = vec![0u8; SIGNATURE_SERIALIZED_SIZE]; + + // Incase of an r or s value of 31 bytes we need to pad it to 32 bytes + let mut padded_r = vec![0u8; FIELD_SIZE]; + let mut padded_s = vec![0u8; FIELD_SIZE]; + padded_r[FIELD_SIZE.saturating_sub(r.len())..].copy_from_slice(&r); + padded_s[FIELD_SIZE.saturating_sub(s.len())..].copy_from_slice(&s); + + signature[..FIELD_SIZE].copy_from_slice(&padded_r); + signature[FIELD_SIZE..].copy_from_slice(&padded_s); + + // Check if s > half_order, if so, compute s = order - s + let s_bignum = BigNum::from_slice(&s)?; + let half_order = BigNum::from_slice(&SECP256R1_HALF_ORDER)?; + let order = BigNum::from_slice(&SECP256R1_ORDER)?; + if s_bignum > half_order { + let mut new_s = BigNum::new()?; + new_s.checked_sub(&order, &s_bignum)?; + let new_s_bytes = new_s.to_vec(); + + // Incase the new s value is 31 bytes we need to pad it to 32 bytes + let mut new_padded_s = vec![0u8; FIELD_SIZE]; + new_padded_s[FIELD_SIZE.saturating_sub(new_s_bytes.len())..] + .copy_from_slice(&new_s_bytes); + + signature[FIELD_SIZE..].copy_from_slice(&new_padded_s); + } + + assert_eq!(pubkey.len(), COMPRESSED_PUBKEY_SERIALIZED_SIZE); + assert_eq!(signature.len(), SIGNATURE_SERIALIZED_SIZE); + + let mut instruction_data = Vec::with_capacity( + DATA_START + .saturating_add(SIGNATURE_SERIALIZED_SIZE) + .saturating_add(COMPRESSED_PUBKEY_SERIALIZED_SIZE) + .saturating_add(message.len()), + ); + + let num_signatures: u8 = 1; + let public_key_offset = DATA_START; + let signature_offset = public_key_offset.saturating_add(COMPRESSED_PUBKEY_SERIALIZED_SIZE); + let message_data_offset = signature_offset.saturating_add(SIGNATURE_SERIALIZED_SIZE); + + instruction_data.extend_from_slice(bytes_of(&[num_signatures, 0])); + + let offsets = Secp256r1SignatureOffsets { + signature_offset: signature_offset as u16, + signature_instruction_index: u16::MAX, + public_key_offset: public_key_offset as u16, + public_key_instruction_index: u16::MAX, + message_data_offset: message_data_offset as u16, + message_data_size: message.len() as u16, + message_instruction_index: u16::MAX, + }; + + instruction_data.extend_from_slice(bytes_of(&offsets)); + instruction_data.extend_from_slice(&pubkey); + instruction_data.extend_from_slice(&signature); + instruction_data.extend_from_slice(message); + + Ok(Instruction { + program_id: crate::id(), + accounts: vec![], + data: instruction_data, + }) + } + + pub fn verify( + data: &[u8], + instruction_datas: &[&[u8]], + _feature_set: &FeatureSet, + ) -> Result<(), PrecompileError> { + if data.len() < SIGNATURE_OFFSETS_START { + return Err(PrecompileError::InvalidInstructionDataSize); + } + let num_signatures = data[0] as usize; + if num_signatures == 0 { + return Err(PrecompileError::InvalidInstructionDataSize); + } + if num_signatures > 8 { + return Err(PrecompileError::InvalidInstructionDataSize); + } + + let expected_data_size = num_signatures + .saturating_mul(SIGNATURE_OFFSETS_SERIALIZED_SIZE) + .saturating_add(SIGNATURE_OFFSETS_START); + + // We do not check or use the byte at data[1] + if data.len() < expected_data_size { + return Err(PrecompileError::InvalidInstructionDataSize); + } + + // Parse half order from constant + let half_order: BigNum = BigNum::from_slice(&SECP256R1_HALF_ORDER) + .map_err(|_| PrecompileError::InvalidSignature)?; + + // Parse order - 1 from constant + let order_minus_one: BigNum = BigNum::from_slice(&SECP256R1_ORDER_MINUS_ONE) + .map_err(|_| PrecompileError::InvalidSignature)?; + + // Create a BigNum for 1 + let one = BigNum::from_u32(1).map_err(|_| PrecompileError::InvalidSignature)?; + + // Define curve group + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1) + .map_err(|_| PrecompileError::InvalidSignature)?; + let mut ctx = BigNumContext::new().map_err(|_| PrecompileError::InvalidSignature)?; + + for i in 0..num_signatures { + let start = i + .saturating_mul(SIGNATURE_OFFSETS_SERIALIZED_SIZE) + .saturating_add(SIGNATURE_OFFSETS_START); + let end = start.saturating_add(SIGNATURE_OFFSETS_SERIALIZED_SIZE); + + // bytemuck wants structures aligned + let offsets: &Secp256r1SignatureOffsets = + bytemuck::try_from_bytes(&data[start..end]) + .map_err(|_| PrecompileError::InvalidDataOffsets)?; + + // Parse out signature + let signature = get_data_slice( + data, + instruction_datas, + offsets.signature_instruction_index, + offsets.signature_offset, + SIGNATURE_SERIALIZED_SIZE, + )?; + + // Parse out pubkey + let pubkey = get_data_slice( + data, + instruction_datas, + offsets.public_key_instruction_index, + offsets.public_key_offset, + COMPRESSED_PUBKEY_SERIALIZED_SIZE, + )?; + + // Parse out message + let message = get_data_slice( + data, + instruction_datas, + offsets.message_instruction_index, + offsets.message_data_offset, + offsets.message_data_size as usize, + )?; + + let r_bignum = BigNum::from_slice(&signature[..FIELD_SIZE]) + .map_err(|_| PrecompileError::InvalidSignature)?; + let s_bignum = BigNum::from_slice(&signature[FIELD_SIZE..]) + .map_err(|_| PrecompileError::InvalidSignature)?; + + // Check that the signature is generally in range + let within_range = r_bignum >= one + && r_bignum <= order_minus_one + && s_bignum >= one + && s_bignum <= half_order; + + if !within_range { + return Err(PrecompileError::InvalidSignature); + } + + // Create an ECDSA signature object from the ASN.1 integers + let ecdsa_sig = openssl::ecdsa::EcdsaSig::from_private_components(r_bignum, s_bignum) + .and_then(|sig| sig.to_der()) + .map_err(|_| PrecompileError::InvalidSignature)?; + + let public_key_point = EcPoint::from_bytes(&group, pubkey, &mut ctx) + .map_err(|_| PrecompileError::InvalidPublicKey)?; + let public_key = EcKey::from_public_key(&group, &public_key_point) + .map_err(|_| PrecompileError::InvalidPublicKey)?; + let public_key_as_pkey = + PKey::from_ec_key(public_key).map_err(|_| PrecompileError::InvalidPublicKey)?; + + let mut verifier = + Verifier::new(openssl::hash::MessageDigest::sha256(), &public_key_as_pkey) + .map_err(|_| PrecompileError::InvalidSignature)?; + verifier + .update(message) + .map_err(|_| PrecompileError::InvalidSignature)?; + + if !verifier + .verify(&ecdsa_sig) + .map_err(|_| PrecompileError::InvalidSignature)? + { + return Err(PrecompileError::InvalidSignature); + } + } + Ok(()) + } + + fn get_data_slice<'a>( + data: &'a [u8], + instruction_datas: &'a [&[u8]], + instruction_index: u16, + offset_start: u16, + size: usize, + ) -> Result<&'a [u8], PrecompileError> { + let instruction = if instruction_index == u16::MAX { + data + } else { + let signature_index = instruction_index as usize; + if signature_index >= instruction_datas.len() { + return Err(PrecompileError::InvalidDataOffsets); + } + instruction_datas[signature_index] + }; + + let start = offset_start as usize; + let end = start.saturating_add(size); + if end > instruction.len() { + return Err(PrecompileError::InvalidDataOffsets); + } + + Ok(&instruction[start..end]) + } + + #[cfg(test)] + mod test { + use { + super::*, + solana_feature_set::FeatureSet, + solana_sdk::{ + hash::Hash, + signature::{Keypair, Signer}, + transaction::Transaction, + }, + }; + + fn test_case( + num_signatures: u16, + offsets: &Secp256r1SignatureOffsets, + ) -> Result<(), PrecompileError> { + assert_eq!( + bytemuck::bytes_of(offsets).len(), + SIGNATURE_OFFSETS_SERIALIZED_SIZE + ); + + let mut instruction_data = vec![0u8; DATA_START]; + instruction_data[0..SIGNATURE_OFFSETS_START].copy_from_slice(bytes_of(&num_signatures)); + instruction_data[SIGNATURE_OFFSETS_START..DATA_START] + .copy_from_slice(bytes_of(offsets)); + verify( + &instruction_data, + &[&[0u8; 100]], + &FeatureSet::all_enabled(), + ) + } + + #[test] + fn test_invalid_offsets() { + solana_logger::setup(); + + let mut instruction_data = vec![0u8; DATA_START]; + let offsets = Secp256r1SignatureOffsets::default(); + instruction_data[0..SIGNATURE_OFFSETS_START].copy_from_slice(bytes_of(&1u16)); + instruction_data[SIGNATURE_OFFSETS_START..DATA_START] + .copy_from_slice(bytes_of(&offsets)); + instruction_data.truncate(instruction_data.len() - 1); + + assert_eq!( + verify( + &instruction_data, + &[&[0u8; 100]], + &FeatureSet::all_enabled() + ), + Err(PrecompileError::InvalidInstructionDataSize) + ); + + let offsets = Secp256r1SignatureOffsets { + signature_instruction_index: 1, + ..Secp256r1SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(PrecompileError::InvalidDataOffsets) + ); + + let offsets = Secp256r1SignatureOffsets { + message_instruction_index: 1, + ..Secp256r1SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(PrecompileError::InvalidDataOffsets) + ); + + let offsets = Secp256r1SignatureOffsets { + public_key_instruction_index: 1, + ..Secp256r1SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(PrecompileError::InvalidDataOffsets) + ); + } + + #[test] + fn test_invalid_signature_data_size() { + solana_logger::setup(); + + // Test data.len() < SIGNATURE_OFFSETS_START + let small_data = vec![0u8; SIGNATURE_OFFSETS_START - 1]; + assert_eq!( + verify(&small_data, &[&[]], &FeatureSet::all_enabled()), + Err(PrecompileError::InvalidInstructionDataSize) + ); + + // Test num_signatures == 0 + let mut zero_sigs_data = vec![0u8; DATA_START]; + zero_sigs_data[0] = 0; // Set num_signatures to 0 + assert_eq!( + verify(&zero_sigs_data, &[&[]], &FeatureSet::all_enabled()), + Err(PrecompileError::InvalidInstructionDataSize) + ); + + // Test num_signatures > 8 + let mut too_many_sigs = vec![0u8; DATA_START]; + too_many_sigs[0] = 9; // Set num_signatures to 9 + assert_eq!( + verify(&too_many_sigs, &[&[]], &FeatureSet::all_enabled()), + Err(PrecompileError::InvalidInstructionDataSize) + ); + } + #[test] + fn test_message_data_offsets() { + let offsets = Secp256r1SignatureOffsets { + message_data_offset: 99, + message_data_size: 1, + ..Secp256r1SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(PrecompileError::InvalidSignature) + ); + + let offsets = Secp256r1SignatureOffsets { + message_data_offset: 100, + message_data_size: 1, + ..Secp256r1SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(PrecompileError::InvalidDataOffsets) + ); + + let offsets = Secp256r1SignatureOffsets { + message_data_offset: 100, + message_data_size: 1000, + ..Secp256r1SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(PrecompileError::InvalidDataOffsets) + ); + + let offsets = Secp256r1SignatureOffsets { + message_data_offset: u16::MAX, + message_data_size: u16::MAX, + ..Secp256r1SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(PrecompileError::InvalidDataOffsets) + ); + } + + #[test] + fn test_pubkey_offset() { + let offsets = Secp256r1SignatureOffsets { + public_key_offset: u16::MAX, + ..Secp256r1SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(PrecompileError::InvalidDataOffsets) + ); + + let offsets = Secp256r1SignatureOffsets { + public_key_offset: 100 - (COMPRESSED_PUBKEY_SERIALIZED_SIZE as u16) + 1, + ..Secp256r1SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(PrecompileError::InvalidDataOffsets) + ); + } + + #[test] + fn test_signature_offset() { + let offsets = Secp256r1SignatureOffsets { + signature_offset: u16::MAX, + ..Secp256r1SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(PrecompileError::InvalidDataOffsets) + ); + + let offsets = Secp256r1SignatureOffsets { + signature_offset: 100 - (SIGNATURE_SERIALIZED_SIZE as u16) + 1, + ..Secp256r1SignatureOffsets::default() + }; + assert_eq!( + test_case(1, &offsets), + Err(PrecompileError::InvalidDataOffsets) + ); + } + + #[test] + fn test_secp256r1() { + solana_logger::setup(); + let message_arr = b"hello"; + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let signing_key = EcKey::generate(&group).unwrap(); + let mut instruction = new_secp256r1_instruction(message_arr, signing_key).unwrap(); + let mint_keypair = Keypair::new(); + let feature_set = FeatureSet::all_enabled(); + + let tx = Transaction::new_signed_with_payer( + &[instruction.clone()], + Some(&mint_keypair.pubkey()), + &[&mint_keypair], + Hash::default(), + ); + + assert!(tx.verify_precompiles(&feature_set).is_ok()); + + // The message is the last field in the instruction data so + // changing its last byte will also change the signature validity + let message_byte_index = instruction.data.len() - 1; + instruction.data[message_byte_index] = + instruction.data[message_byte_index].wrapping_add(12); + let tx = Transaction::new_signed_with_payer( + &[instruction.clone()], + Some(&mint_keypair.pubkey()), + &[&mint_keypair], + Hash::default(), + ); + + assert!(tx.verify_precompiles(&feature_set).is_err()); + } + + #[test] + fn test_secp256r1_high_s() { + solana_logger::setup(); + let message_arr = b"hello"; + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let signing_key = EcKey::generate(&group).unwrap(); + let mut instruction = new_secp256r1_instruction(message_arr, signing_key).unwrap(); + + // To double check that the untampered low-S value signature passes + let feature_set = FeatureSet::all_enabled(); + let tx_pass = verify( + instruction.data.as_slice(), + &[instruction.data.as_slice()], + &feature_set, + ); + assert!(tx_pass.is_ok()); + + // Determine offsets at which to perform the S-value manipulation + let public_key_offset = DATA_START; + let signature_offset = public_key_offset + COMPRESSED_PUBKEY_SERIALIZED_SIZE; + let s_offset = signature_offset + FIELD_SIZE; + + // Create a high S value by doing order - s + let order = BigNum::from_slice(&SECP256R1_ORDER).unwrap(); + let current_s = + BigNum::from_slice(&instruction.data[s_offset..s_offset + FIELD_SIZE]).unwrap(); + let mut high_s = BigNum::new().unwrap(); + high_s.checked_sub(&order, ¤t_s).unwrap(); + + // Replace the S value in the signature with our high S + instruction.data[s_offset..s_offset + FIELD_SIZE].copy_from_slice(&high_s.to_vec()); + + // Since Transaction::verify_precompiles only returns a vague + // `InvalidAccountIndex` error on precompile failure, we use verify() + // here direclty to check for the specific + // InvalidSignatureValueRange error + let tx_fail = verify( + instruction.data.as_slice(), + &[instruction.data.as_slice()], + &feature_set, + ); + assert!(tx_fail.unwrap_err() == PrecompileError::InvalidSignature); + } + #[test] + fn test_new_secp256r1_instruction_31byte_components() { + solana_logger::setup(); + let message_arr = b"hello"; + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let signing_key = EcKey::generate(&group).unwrap(); + + // Keep generating signatures until we get one with a 31-byte component + loop { + let instruction = + new_secp256r1_instruction(message_arr, signing_key.clone()).unwrap(); + + // Extract r and s from the signature + let signature_offset = DATA_START + COMPRESSED_PUBKEY_SERIALIZED_SIZE; + let r = &instruction.data[signature_offset..signature_offset + FIELD_SIZE]; + let s = &instruction.data + [signature_offset + FIELD_SIZE..signature_offset + 2 * FIELD_SIZE]; + + // Convert to BigNum and back to get byte representation + let r_bn = BigNum::from_slice(r).unwrap(); + let s_bn = BigNum::from_slice(s).unwrap(); + let r_bytes = r_bn.to_vec(); + let s_bytes = s_bn.to_vec(); + + if r_bytes.len() == 31 || s_bytes.len() == 31 { + // Once found, verify the signature and break out of the loop + let mint_keypair = Keypair::new(); + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&mint_keypair.pubkey()), + &[&mint_keypair], + Hash::default(), + ); + + let feature_set = FeatureSet::all_enabled(); + assert!(tx.verify_precompiles(&feature_set).is_ok()); + break; + } + } + } + + #[test] + fn test_new_secp256r1_instruction_signing_key() { + solana_logger::setup(); + let message_arr = b"hello"; + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let signing_key = EcKey::generate(&group).unwrap(); + assert!(new_secp256r1_instruction(message_arr, signing_key).is_ok()); + + let incorrect_group = EcGroup::from_curve_name(Nid::X9_62_PRIME192V1).unwrap(); + let incorrect_key = EcKey::generate(&incorrect_group).unwrap(); + assert!(new_secp256r1_instruction(message_arr, incorrect_key).is_err()); + } + #[test] + fn test_secp256r1_order() { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let mut ctx = BigNumContext::new().unwrap(); + let mut openssl_order = BigNum::new().unwrap(); + group.order(&mut openssl_order, &mut ctx).unwrap(); + + let our_order = BigNum::from_slice(&SECP256R1_ORDER).unwrap(); + assert_eq!(our_order, openssl_order); + } + + #[test] + fn test_secp256r1_order_minus_one() { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let mut ctx = BigNumContext::new().unwrap(); + let mut openssl_order = BigNum::new().unwrap(); + group.order(&mut openssl_order, &mut ctx).unwrap(); + + let mut expected_order_minus_one = BigNum::new().unwrap(); + expected_order_minus_one + .checked_sub(&openssl_order, &BigNum::from_u32(1).unwrap()) + .unwrap(); + + let our_order_minus_one = BigNum::from_slice(&SECP256R1_ORDER_MINUS_ONE).unwrap(); + assert_eq!(our_order_minus_one, expected_order_minus_one); + } + + #[test] + fn test_secp256r1_half_order() { + // Get the secp256r1 curve group + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + + // Get the order from OpenSSL + let mut ctx = BigNumContext::new().unwrap(); + let mut openssl_order = BigNum::new().unwrap(); + group.order(&mut openssl_order, &mut ctx).unwrap(); + + // Calculate half order + let mut calculated_half_order = BigNum::new().unwrap(); + let two = BigNum::from_u32(2).unwrap(); + calculated_half_order + .checked_div(&openssl_order, &two, &mut ctx) + .unwrap(); + + // Get our constant half order + let our_half_order = BigNum::from_slice(&SECP256R1_HALF_ORDER).unwrap(); + + // Compare the calculated half order with our constant + assert_eq!(calculated_half_order, our_half_order); + } + + #[test] + fn test_secp256r1_order_relationships() { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let mut ctx = BigNumContext::new().unwrap(); + let mut openssl_order = BigNum::new().unwrap(); + group.order(&mut openssl_order, &mut ctx).unwrap(); + + let our_order = BigNum::from_slice(&SECP256R1_ORDER).unwrap(); + let our_order_minus_one = BigNum::from_slice(&SECP256R1_ORDER_MINUS_ONE).unwrap(); + let our_half_order = BigNum::from_slice(&SECP256R1_HALF_ORDER).unwrap(); + + // Verify our order matches OpenSSL's order + assert_eq!(our_order, openssl_order); + + // Verify order - 1 + let mut expected_order_minus_one = BigNum::new().unwrap(); + expected_order_minus_one + .checked_sub(&openssl_order, &BigNum::from_u32(1).unwrap()) + .unwrap(); + assert_eq!(our_order_minus_one, expected_order_minus_one); + + // Verify half order + let mut expected_half_order = BigNum::new().unwrap(); + expected_half_order + .checked_div(&openssl_order, &BigNum::from_u32(2).unwrap(), &mut ctx) + .unwrap(); + assert_eq!(our_half_order, expected_half_order); + + // Verify half order * 2 = order - 1 + let mut double_half_order = BigNum::new().unwrap(); + double_half_order + .checked_mul(&our_half_order, &BigNum::from_u32(2).unwrap(), &mut ctx) + .unwrap(); + assert_eq!(double_half_order, expected_order_minus_one); + } + } +} + +#[cfg(any(target_arch = "wasm32", target_os = "solana"))] +mod target_arch { + use {solana_feature_set::FeatureSet, solana_precompile_error::PrecompileError}; + + pub fn verify( + _data: &[u8], + _instruction_datas: &[&[u8]], + _feature_set: &FeatureSet, + ) -> Result<(), PrecompileError> { + Err(PrecompileError::InvalidSignature) + } +} + +pub use self::target_arch::*; diff --git a/sdk/src/precompiles.rs b/sdk/src/precompiles.rs index 4812b81e347717..5b40397bb75301 100644 --- a/sdk/src/precompiles.rs +++ b/sdk/src/precompiles.rs @@ -7,6 +7,7 @@ pub use solana_precompile_error::PrecompileError; use { lazy_static::lazy_static, solana_feature_set::FeatureSet, solana_program::instruction::CompiledInstruction, solana_pubkey::Pubkey, + solana_secp256r1_program as secp256r1_program, }; /// All precompiled programs must implement the `Verify` function @@ -63,6 +64,11 @@ lazy_static! { None, // always enabled crate::ed25519_instruction::verify, ), + Precompile::new( + secp256r1_program::id(), + Some(solana_feature_set::enable_secp256r1_precompile::id()), + secp256r1_program::verify, + ) ]; } diff --git a/sdk/src/reserved_account_keys.rs b/sdk/src/reserved_account_keys.rs index 49cde7fd9bcd79..d985f76f16b8d3 100644 --- a/sdk/src/reserved_account_keys.rs +++ b/sdk/src/reserved_account_keys.rs @@ -12,6 +12,7 @@ use { }, lazy_static::lazy_static, solana_feature_set::{self as feature_set, FeatureSet}, + solana_secp256r1_program as secp256r1_program, std::collections::{HashMap, HashSet}, }; @@ -160,6 +161,7 @@ lazy_static! { ReservedAccount::new_active(feature::id()), ReservedAccount::new_pending(loader_v4::id(), feature_set::add_new_reserved_account_keys::id()), ReservedAccount::new_pending(secp256k1_program::id(), feature_set::add_new_reserved_account_keys::id()), + ReservedAccount::new_pending(secp256r1_program::id(), feature_set::enable_secp256r1_precompile::id()), #[allow(deprecated)] ReservedAccount::new_active(stake::config::id()), ReservedAccount::new_active(stake::program::id()), diff --git a/test-validator/Cargo.toml b/test-validator/Cargo.toml index 4fa4330f90be00..33e542540058a6 100644 --- a/test-validator/Cargo.toml +++ b/test-validator/Cargo.toml @@ -31,7 +31,7 @@ solana-rpc = { workspace = true } solana-rpc-client = { workspace = true } solana-rpc-client-api = { workspace = true } solana-runtime = { workspace = true } -solana-sdk = { workspace = true } +solana-sdk = { workspace = true, features = ["openssl-vendored"] } solana-streamer = { workspace = true } solana-tpu-client = { workspace = true } tokio = { workspace = true, features = ["full"] } diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 39e5c4d1b19d0e..2adfd48845b45d 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -55,7 +55,7 @@ solana-rpc = { workspace = true } solana-rpc-client = { workspace = true } solana-rpc-client-api = { workspace = true } solana-runtime = { workspace = true } -solana-sdk = { workspace = true } +solana-sdk = { workspace = true, features = ["openssl-vendored"] } solana-send-transaction-service = { workspace = true } solana-storage-bigtable = { workspace = true } solana-streamer = { workspace = true }