Skip to content

context: introduce new global context API with rerandomization #806

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ unexpected_cfgs = { level = "deny", check-cfg = ['cfg(bench)', 'cfg(secp256k1_fu

[[example]]
name = "sign_verify_recovery"
required-features = ["recovery", "hashes", "std"]
required-features = ["recovery", "hashes"]

[[example]]
name = "sign_verify"
Expand Down
27 changes: 8 additions & 19 deletions examples/sign_verify_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,25 @@ extern crate hashes;
extern crate secp256k1;

use hashes::{sha256, Hash};
use secp256k1::{ecdsa, Error, Message, PublicKey, Secp256k1, SecretKey, Signing, Verification};

fn recover<C: Verification>(
secp: &Secp256k1<C>,
msg: &[u8],
sig: [u8; 64],
recovery_id: u8,
) -> Result<PublicKey, Error> {
use secp256k1::{ecdsa, Error, Message, PublicKey, SecretKey};

fn recover(msg: &[u8], sig: [u8; 64], recovery_id: u8) -> Result<PublicKey, Error> {
let msg = sha256::Hash::hash(msg);
let msg = Message::from_digest(msg.to_byte_array());
let id = ecdsa::RecoveryId::try_from(i32::from(recovery_id))?;
let sig = ecdsa::RecoverableSignature::from_compact(&sig, id)?;

secp.recover_ecdsa(msg, &sig)
sig.recover_ecdsa(msg)
}

fn sign_recovery<C: Signing>(
secp: &Secp256k1<C>,
msg: &[u8],
seckey: [u8; 32],
) -> Result<ecdsa::RecoverableSignature, Error> {
fn sign_recovery(msg: &[u8], seckey: [u8; 32]) -> Result<ecdsa::RecoverableSignature, Error> {
let msg = sha256::Hash::hash(msg);
let msg = Message::from_digest(msg.to_byte_array());
let seckey = SecretKey::from_byte_array(seckey)?;
Ok(secp.sign_ecdsa_recoverable(msg, &seckey))
Ok(ecdsa::RecoverableSignature::sign_ecdsa_recoverable(msg, &seckey))
}

fn main() {
let secp = Secp256k1::new();

let seckey = [
59, 148, 11, 85, 134, 130, 61, 253, 2, 174, 59, 70, 27, 180, 51, 107, 94, 203, 174, 253,
102, 39, 170, 146, 46, 252, 4, 143, 236, 12, 136, 28,
Expand All @@ -43,9 +32,9 @@ fn main() {
.unwrap();
let msg = b"This is some message";

let signature = sign_recovery(&secp, msg, seckey).unwrap();
let signature = sign_recovery(msg, seckey).unwrap();

let (recovery_id, serialize_sig) = signature.serialize_compact();

assert_eq!(recover(&secp, msg, serialize_sig, recovery_id.to_u8()), Ok(pubkey));
assert_eq!(recover(msg, serialize_sig, recovery_id.to_u8()), Ok(pubkey));
}
6 changes: 3 additions & 3 deletions no_std_test/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ use core::panic::PanicInfo;

use secp256k1::ecdh::{self, SharedSecret};
use secp256k1::ffi::types::AlignedType;
use secp256k1::rand::{self, RngCore};
use secp256k1::rand::RngCore;
use secp256k1::serde::Serialize;
use secp256k1::*;

Expand Down Expand Up @@ -93,9 +93,9 @@ fn start(_argc: isize, _argv: *const *const u8) -> isize {
let sig = secp.sign_ecdsa(message, &secret_key);
assert!(secp.verify_ecdsa(&sig, message, &public_key).is_ok());

let rec_sig = secp.sign_ecdsa_recoverable(message, &secret_key);
let rec_sig = ecdsa::RecoverableSignature::sign_ecdsa_recoverable(message, &secret_key);
assert!(secp.verify_ecdsa(&rec_sig.to_standard(), message, &public_key).is_ok());
assert_eq!(public_key, secp.recover_ecdsa(message, &rec_sig).unwrap());
assert_eq!(public_key, rec_sig.recover_ecdsa(message).unwrap());
let (rec_id, data) = rec_sig.serialize_compact();
let new_rec_sig = ecdsa::RecoverableSignature::from_compact(&data, rec_id).unwrap();
assert_eq!(rec_sig, new_rec_sig);
Expand Down
159 changes: 159 additions & 0 deletions src/context/internal_nostd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: CC0-1.0

use core::marker::PhantomData;
use core::mem::ManuallyDrop;
use core::ptr::NonNull;

use crate::context::spinlock::SpinLock;
use crate::{ffi, Context, Secp256k1};

mod self_contained_context {
use core::mem::MaybeUninit;
use core::ptr::NonNull;

use crate::ffi::types::{c_void, AlignedType};
use crate::{ffi, AllPreallocated, Context as _};

const MAX_PREALLOC_SIZE: usize = 16; // measured at 208 bytes on Andrew's 64-bit system

/// A secp256k1 context object which can be allocated on the stack or in static storage.
pub struct SelfContainedContext(
[MaybeUninit<AlignedType>; MAX_PREALLOC_SIZE],
Option<NonNull<ffi::Context>>,
);

// SAFETY: the context object owns all its own data.
unsafe impl Send for SelfContainedContext {}

impl SelfContainedContext {
/// Creates a new uninitialized self-contained context.
pub const fn new_uninitialized() -> Self {
Self([MaybeUninit::uninit(); MAX_PREALLOC_SIZE], None)
}

/// Accessor for the underlying raw context pointer
fn buf(&mut self) -> NonNull<c_void> {
NonNull::new(self.0.as_mut_ptr() as *mut c_void).unwrap()
}

pub fn clone_into(&mut self, other: &mut SelfContainedContext) {
// SAFETY: just FFI calls
unsafe {
let other = other.raw_ctx().as_ptr();
assert!(
ffi::secp256k1_context_preallocated_clone_size(other)
<= core::mem::size_of::<[AlignedType; MAX_PREALLOC_SIZE]>(),
"prealloc size exceeds our guessed compile-time upper bound",
);
ffi::secp256k1_context_preallocated_clone(other, self.buf());
}
}

/// Accessor for the context as a raw context pointer.
///
/// On the first call, this will create the context.
pub fn raw_ctx(&mut self) -> NonNull<ffi::Context> {
let buf = self.buf();
*self.1.get_or_insert_with(|| {
// SAFETY: just FFI calls
unsafe {
assert!(
ffi::secp256k1_context_preallocated_size(AllPreallocated::FLAGS)
<= core::mem::size_of::<[AlignedType; MAX_PREALLOC_SIZE]>(),
"prealloc size exceeds our guessed compile-time upper bound",
);
ffi::secp256k1_context_preallocated_create(buf, AllPreallocated::FLAGS)
}
})
}
}
}
// Needs to be pub(super) so that we can define a constructor for
// SpinLock<SelfContainedContext> in the spinlock module. (We cannot do so generically
// because we need a const constructor.)
pub(super) use self_contained_context::SelfContainedContext;

static SECP256K1: SpinLock<SelfContainedContext> = SpinLock::<SelfContainedContext>::new();

/// Borrows the global context and do some operation on it.
///
/// If `randomize_seed` is provided, it is used to rerandomize the context after the
/// operation is complete. If it is not provided, randomization is skipped.
///
/// Only a bit or two per signing operation is needed; if you have any entropy at all,
/// you should provide it, even if you can't provide 32 random bytes.
pub fn with_global_context<T, Ctx: Context, F: FnOnce(&Secp256k1<Ctx>) -> T>(
f: F,
rerandomize_seed: Option<&[u8; 32]>,
) -> T {
with_raw_global_context(
|ctx| {
let secp = ManuallyDrop::new(Secp256k1 { ctx, phantom: PhantomData });
f(&*secp)
},
rerandomize_seed,
)
}

/// Borrows the global context as a raw pointer and do some operation on it.
///
/// If `randomize_seed` is provided, it is used to rerandomize the context after the
/// operation is complete. If it is not provided, randomization is skipped.
///
/// Only a bit or two per signing operation is needed; if you have any entropy at all,
/// you should provide it, even if you can't provide 32 random bytes.
pub fn with_raw_global_context<T, F: FnOnce(NonNull<ffi::Context>) -> T>(
f: F,
rerandomize_seed: Option<&[u8; 32]>,
) -> T {
// Our function may be expensive, so before calling it, we copy the global
// context into this local buffer on the stack. Then we can release it,
// allowing other callers to use it simultaneously.
let mut ctx = SelfContainedContext::new_uninitialized();
let mut have_global_ctx = false;
if let Some(mut guard) = SECP256K1.try_lock() {
let global_ctx = &mut *guard;
ctx.clone_into(global_ctx);
have_global_ctx = true;
// (the lock is now dropped)
}

// Obtain a raw pointer to the context, creating one if it has not been already,
// and call the function.
let ctx_ptr = ctx.raw_ctx();
let ret = f(ctx_ptr);

// ...then rerandomize the local copy, and try to replace the global one
// with this. Note that even if we got the lock above, we may fail to get
// it now; in that case, we don't rerandomize and leave the contexct in
// the state that we found it in.
//
// We do this, rather than holding the lock continuously through the call
// to `f`, to minimize the likelihood of contention. If we fail to randomize,
// that really isn't a big deal since this is a "defense in depth" measure
// whose value is likely to obtain even if it only succeeds a small fraction
// of the time.
//
// Contention, meanwhile, will lead to users using a stack-local copy of
// the context rather than the global one, which aside from being inefficient,
// means that the context they use won't be rerandomized at all. So there
// isn't even any benefit.
if have_global_ctx {
if let Some(seed) = rerandomize_seed {
// SAFETY: just a FFI call
unsafe {
assert_eq!(ffi::secp256k1_context_randomize(ctx_ptr, seed.as_ptr()), 1);
}
if let Some(ref mut guard) = SECP256K1.try_lock() {
guard.clone_into(&mut ctx);
}
}
}
ret
}

/// Rerandomize the global context, using the given data as a seed.
///
/// The provided data will be mixed with the entropy from previous calls in a timing
/// analysis resistant way. It is safe to directly pass secret data to this function.
pub fn rerandomize_global_context(seed: &[u8; 32]) { with_raw_global_context(|_| {}, Some(seed)) }
75 changes: 75 additions & 0 deletions src/context/internal_std.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: CC0-1.0

use std::cell::RefCell;
use std::marker::PhantomData;
use std::mem::ManuallyDrop;
use std::ptr::NonNull;

use secp256k1_sys as ffi;

use crate::{All, Context, Secp256k1};

thread_local! {
static SECP256K1: RefCell<Secp256k1<All>> = RefCell::new(Secp256k1::new());
}

/// Borrows the global context and do some operation on it.
///
/// If provided, after the operation is complete, [`rerandomize_global_context`]
/// is called on the context. If you have some random data available,
pub fn with_global_context<T, Ctx: Context, F: FnOnce(&Secp256k1<Ctx>) -> T>(
f: F,
rerandomize_seed: Option<&[u8; 32]>,
) -> T {
with_raw_global_context(
|ctx| {
let secp = ManuallyDrop::new(Secp256k1 { ctx, phantom: PhantomData });
f(&*secp)
},
rerandomize_seed,
)
}

/// Borrows the global context as a raw pointer and do some operation on it.
///
/// If provided, after the operation is complete, [`rerandomize_global_context`]
/// is called on the context. If you have some random data available,
pub fn with_raw_global_context<T, F: FnOnce(NonNull<ffi::Context>) -> T>(
f: F,
rerandomize_seed: Option<&[u8; 32]>,
) -> T {
SECP256K1.with(|secp| {
let borrow = secp.borrow();
let ret = f(borrow.ctx);
drop(borrow);

if let Some(seed) = rerandomize_seed {
rerandomize_global_context(seed);
}
ret
})
}

/// Rerandomize the global context, using the given data as a seed.
///
/// The provided data will be mixed with the entropy from previous calls in a timing
/// analysis resistant way. It is safe to directly pass secret data to this function.
pub fn rerandomize_global_context(seed: &[u8; 32]) {
SECP256K1.with(|secp| {
let mut borrow = secp.borrow_mut();

// If we have access to the thread rng then use it as well.
#[cfg(feature = "rand")]
{
let mut new_seed: [u8; 32] = rand::random();
for (new, byte) in new_seed.iter_mut().zip(seed.iter()) {
*new ^= *byte;
}
borrow.seeded_randomize(&new_seed);
}
#[cfg(not(feature = "rand"))]
{
borrow.seeded_randomize(seed);
}
});
}
12 changes: 11 additions & 1 deletion src/context.rs → src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ use crate::ffi::types::{c_uint, c_void, AlignedType};
use crate::ffi::{self, CPtr};
use crate::{Error, Secp256k1};

#[cfg_attr(feature = "std", path = "internal_std.rs")]
#[cfg_attr(not(feature = "std"), path = "internal_nostd.rs")]
mod internal;

#[cfg(not(feature = "std"))]
mod spinlock;

pub use internal::{rerandomize_global_context, with_global_context, with_raw_global_context};

#[cfg(all(feature = "global-context", feature = "std"))]
/// Module implementing a singleton pattern for a global `Secp256k1` context.
pub mod global {
Expand Down Expand Up @@ -369,7 +378,8 @@ impl<'buf> Secp256k1<AllPreallocated<'buf>> {
/// * The version of `libsecp256k1` used to create `raw_ctx` must be **exactly the one linked
/// into this library**.
/// * The lifetime of the `raw_ctx` pointer must outlive `'buf`.
/// * `raw_ctx` must point to writable memory (cannot be `ffi::secp256k1_context_no_precomp`).
/// * `raw_ctx` must point to writable memory (cannot be `ffi::secp256k1_context_no_precomp`),
/// **or** the user must never attempt to rerandomize the context.
pub unsafe fn from_raw_all(
raw_ctx: NonNull<ffi::Context>,
) -> ManuallyDrop<Secp256k1<AllPreallocated<'buf>>> {
Expand Down
Loading
Loading