Skip to content

Commit 3bbf4cf

Browse files
authored
fix: ecdsa recovery failure may cause executor to panic (#1799)
1 parent 41532ee commit 3bbf4cf

File tree

5 files changed

+104
-45
lines changed

5 files changed

+104
-45
lines changed

Cargo.lock

+24-24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+21-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace.package]
2-
version = "3.2.1"
2+
version = "3.3.0"
33
edition = "2021"
44
license = "MIT OR Apache-2.0"
55
repository = "https://github.com/succinctlabs/sp1"
@@ -47,26 +47,26 @@ debug-assertions = true
4747

4848
[workspace.dependencies]
4949
# sp1
50-
sp1-build = { path = "crates/build", version = "3.2.1" }
51-
sp1-cli = { path = "crates/cli", version = "3.2.1", default-features = false }
52-
sp1-core-machine = { path = "crates/core/machine", version = "3.2.1" }
53-
sp1-core-executor = { path = "crates/core/executor", version = "3.2.1" }
54-
sp1-curves = { path = "crates/curves", version = "3.2.1" }
55-
sp1-derive = { path = "crates/derive", version = "3.2.1" }
56-
sp1-eval = { path = "crates/eval", version = "3.2.1" }
57-
sp1-helper = { path = "crates/helper", version = "3.2.1", default-features = false }
58-
sp1-primitives = { path = "crates/primitives", version = "3.2.1" }
59-
sp1-prover = { path = "crates/prover", version = "3.2.1" }
60-
sp1-recursion-compiler = { path = "crates/recursion/compiler", version = "3.2.1" }
61-
sp1-recursion-core = { path = "crates/recursion/core", version = "3.2.1", default-features = false }
62-
sp1-recursion-derive = { path = "crates/recursion/derive", version = "3.2.1", default-features = false }
63-
sp1-recursion-gnark-ffi = { path = "crates/recursion/gnark-ffi", version = "3.2.1", default-features = false }
64-
sp1-recursion-circuit = { path = "crates/recursion/circuit", version = "3.2.1", default-features = false }
65-
sp1-sdk = { path = "crates/sdk", version = "3.2.1" }
66-
sp1-cuda = { path = "crates/cuda", version = "3.2.1" }
67-
sp1-stark = { path = "crates/stark", version = "3.2.1" }
68-
sp1-lib = { path = "crates/zkvm/lib", version = "3.2.1", default-features = false }
69-
sp1-zkvm = { path = "crates/zkvm/entrypoint", version = "3.2.1", default-features = false }
50+
sp1-build = { path = "crates/build", version = "3.3.0" }
51+
sp1-cli = { path = "crates/cli", version = "3.3.0", default-features = false }
52+
sp1-core-machine = { path = "crates/core/machine", version = "3.3.0" }
53+
sp1-core-executor = { path = "crates/core/executor", version = "3.3.0" }
54+
sp1-curves = { path = "crates/curves", version = "3.3.0" }
55+
sp1-derive = { path = "crates/derive", version = "3.3.0" }
56+
sp1-eval = { path = "crates/eval", version = "3.3.0" }
57+
sp1-helper = { path = "crates/helper", version = "3.3.0", default-features = false }
58+
sp1-primitives = { path = "crates/primitives", version = "3.3.0" }
59+
sp1-prover = { path = "crates/prover", version = "3.3.0" }
60+
sp1-recursion-compiler = { path = "crates/recursion/compiler", version = "3.3.0" }
61+
sp1-recursion-core = { path = "crates/recursion/core", version = "3.3.0", default-features = false }
62+
sp1-recursion-derive = { path = "crates/recursion/derive", version = "3.3.0", default-features = false }
63+
sp1-recursion-gnark-ffi = { path = "crates/recursion/gnark-ffi", version = "3.3.0", default-features = false }
64+
sp1-recursion-circuit = { path = "crates/recursion/circuit", version = "3.3.0", default-features = false }
65+
sp1-sdk = { path = "crates/sdk", version = "3.3.0" }
66+
sp1-cuda = { path = "crates/cuda", version = "3.3.0" }
67+
sp1-stark = { path = "crates/stark", version = "3.3.0" }
68+
sp1-lib = { path = "crates/zkvm/lib", version = "3.3.0", default-features = false }
69+
sp1-zkvm = { path = "crates/zkvm/entrypoint", version = "3.3.0", default-features = false }
7070

7171
# p3
7272
p3-air = "0.1.4-succinct"

book/writing-programs/patched-crates.md

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ tiny-keccak = { git = "https://github.com/sp1-patches/tiny-keccak", tag = "tiny_
3939
curve25519-dalek = { git = "https://github.com/sp1-patches/curve25519-dalek", tag = "curve25519_dalek-v4.1.3-patch-v1" }
4040
curve25519-dalek-ng = { git = "https://github.com/sp1-patches/curve25519-dalek-ng", tag = "curve25519_dalek_ng-v4.1.1-patch-v1" }
4141
ed25519-consensus = { git = "https://github.com/sp1-patches/ed25519-consensus", tag = "ed25519_consensus-v2.1.0-patch-v1" }
42+
# For sp1 versions >= 3.3.0
43+
ecdsa-core = { git = "https://github.com/sp1-patches/signatures", package = "ecdsa", tag = "ecdsa-v0.16.9-patch-v3.3.0" }
44+
# For sp1 versions < 3.3.0
4245
ecdsa-core = { git = "https://github.com/sp1-patches/signatures", package = "ecdsa", tag = "ecdsa-v0.16.9-patch-v1" }
4346
secp256k1 = { git = "https://github.com/sp1-patches/rust-secp256k1", tag = "secp256k1-v0.29.0-patch-v1" }
4447
substrate-bn = { git = "https://github.com/sp1-patches/bn", tag = "substrate_bn-v0.6.0-patch-v1" }

crates/core/executor/src/hook.rs

+53
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ pub type BoxedHook<'a> = Arc<RwLock<dyn Hook + Send + Sync + 'a>>;
1313
/// The file descriptor through which to access `hook_ecrecover`.
1414
pub const FD_ECRECOVER_HOOK: u32 = 5;
1515

16+
// Note: we skip 6 because we have an eddsa hook in dev.
17+
18+
/// The file descriptor through which to access `hook_ecrecover_2`.
19+
pub const FD_ECRECOVER_HOOK_2: u32 = 7;
20+
1621
/// A runtime hook. May be called during execution by writing to a specified file descriptor,
1722
/// accepting and returning arbitrary data.
1823
pub trait Hook {
@@ -76,6 +81,7 @@ impl<'a> Default for HookRegistry<'a> {
7681
// Note: To ensure any `fd` value is synced with `zkvm/precompiles/src/io.rs`,
7782
// add an assertion to the test `hook_fds_match` below.
7883
(FD_ECRECOVER_HOOK, hookify(hook_ecrecover)),
84+
(FD_ECRECOVER_HOOK_2, hookify(hook_ecrecover_v2)),
7985
]);
8086

8187
Self { table }
@@ -117,6 +123,7 @@ pub struct HookEnv<'a, 'b: 'a> {
117123
/// WARNING: This function is used to recover the public key outside of the zkVM context. These
118124
/// values must be constrained by the zkVM for correctness.
119125
#[must_use]
126+
#[deprecated = "Use `hook_ecrecover_v2` instead."]
120127
pub fn hook_ecrecover(_: HookEnv, buf: &[u8]) -> Vec<Vec<u8>> {
121128
assert_eq!(buf.len(), 65 + 32, "ecrecover input should have length 65 + 32");
122129
let (sig, msg_hash) = buf.split_at(65);
@@ -141,6 +148,52 @@ pub fn hook_ecrecover(_: HookEnv, buf: &[u8]) -> Vec<Vec<u8>> {
141148
vec![bytes.to_vec(), s_inverse.to_bytes().to_vec()]
142149
}
143150

151+
/// Recovers the public key from the signature and message hash using the k256 crate.
152+
///
153+
/// # Arguments
154+
///
155+
/// * `env` - The environment in which the hook is invoked.
156+
/// * `buf` - The buffer containing the signature and message hash.
157+
/// - The signature is 65 bytes, the first 64 bytes are the signature and the last byte is the
158+
/// recovery ID.
159+
/// - The message hash is 32 bytes.
160+
///
161+
/// The result is returned as a status and a pair of bytes, where the first 32 bytes are the X coordinate
162+
/// and the second 32 bytes are the Y coordinate of the decompressed point.
163+
///
164+
/// A status of 0 indicates that the public key could not be recovered.
165+
///
166+
/// WARNING: This function is used to recover the public key outside of the zkVM context. These
167+
/// values must be constrained by the zkVM for correctness.
168+
#[must_use]
169+
pub fn hook_ecrecover_v2(_: HookEnv, buf: &[u8]) -> Vec<Vec<u8>> {
170+
assert_eq!(buf.len(), 65 + 32, "ecrecover input should have length 65 + 32, this is a bug.");
171+
let (sig, msg_hash) = buf.split_at(65);
172+
let sig: &[u8; 65] = sig.try_into().unwrap();
173+
let msg_hash: &[u8; 32] = msg_hash.try_into().unwrap();
174+
175+
let mut recovery_id = sig[64];
176+
let mut sig = Signature::from_slice(&sig[..64]).unwrap();
177+
178+
if let Some(sig_normalized) = sig.normalize_s() {
179+
sig = sig_normalized;
180+
recovery_id ^= 1;
181+
};
182+
let recid = RecoveryId::from_byte(recovery_id).expect("Computed recovery ID is invalid, this is a bug.");
183+
184+
// Attempting to recvover the public key has failed, write a 0 to indicate to the caller.
185+
let Ok(recovered_key) = VerifyingKey::recover_from_prehash(&msg_hash[..], &sig, recid) else {
186+
return vec![vec![0]];
187+
};
188+
189+
let bytes = recovered_key.to_sec1_bytes();
190+
191+
let (_, s) = sig.split_scalars();
192+
let s_inverse = s.invert();
193+
194+
vec![vec![1], bytes.to_vec(), s_inverse.to_bytes().to_vec()]
195+
}
196+
144197
#[cfg(test)]
145198
pub mod tests {
146199
use super::*;

crates/zkvm/lib/src/io.rs

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ pub const FD_HINT: u32 = 4;
1515
/// The file descriptor for the `ecreover` hook.
1616
pub const FD_ECRECOVER_HOOK: u32 = 5;
1717

18+
/// The file descriptor through which to access `hook_ecrecover_2`.
19+
pub const FD_ECRECOVER_HOOK_2: u32 = 7;
20+
1821
/// A writer that writes to a file descriptor inside the zkVM.
1922
struct SyscallWriter {
2023
fd: u32,

0 commit comments

Comments
 (0)