Skip to content

Commit 69901b3

Browse files
committed
context: add nostd version of global context
See the previous commit description for a high-level overview of the spinlocking logic used in this commit. Next steps are: 1. Update the API to use this logic everywhere; on validation functions we don't need to rerandomize and on signing/keygen functions we should rerandomize using our secret key material. 2. Remove the existing "no context" API, along with the global-context and global-context-less-secure features. 3. Improve our entropy story on nostd by scraping system time or CPU jitter or something and hashing that into our rerandomization. We don't need to do a great job here -- if we can get even a bit or two per signature, that will completely BTFO a timing attacker.
1 parent cd0fdd1 commit 69901b3

File tree

4 files changed

+173
-10
lines changed

4 files changed

+173
-10
lines changed

src/context/internal_nostd.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
use core::marker::PhantomData;
4+
use core::mem::ManuallyDrop;
5+
use core::ptr::NonNull;
6+
7+
use crate::context::spinlock::SpinLock;
8+
use crate::{ffi, Context, Secp256k1};
9+
10+
mod self_contained_context {
11+
use core::mem::MaybeUninit;
12+
use core::ptr::NonNull;
13+
14+
use crate::ffi::types::{c_void, AlignedType};
15+
use crate::{ffi, AllPreallocated, Context as _};
16+
17+
const MAX_PREALLOC_SIZE: usize = 16; // measured at 208 bytes on Andrew's 64-bit system
18+
19+
/// A secp256k1 context object which can be allocated on the stack or in static storage.
20+
pub struct SelfContainedContext(
21+
[MaybeUninit<AlignedType>; MAX_PREALLOC_SIZE],
22+
Option<NonNull<ffi::Context>>,
23+
);
24+
25+
// SAFETY: the context object owns all its own data.
26+
unsafe impl Send for SelfContainedContext {}
27+
28+
impl SelfContainedContext {
29+
/// Creates a new uninitialized self-contained context.
30+
pub const fn new_uninitialized() -> Self {
31+
Self([MaybeUninit::uninit(); MAX_PREALLOC_SIZE], None)
32+
}
33+
34+
/// Accessor for the underlying raw context pointer
35+
fn buf(&mut self) -> NonNull<c_void> {
36+
NonNull::new(self.0.as_mut_ptr() as *mut c_void).unwrap()
37+
}
38+
39+
pub fn clone_into(&mut self, other: &mut SelfContainedContext) {
40+
// SAFETY: just FFI calls
41+
unsafe {
42+
let other = other.raw_ctx().as_ptr();
43+
assert!(
44+
ffi::secp256k1_context_preallocated_clone_size(other)
45+
<= core::mem::size_of::<[AlignedType; MAX_PREALLOC_SIZE]>(),
46+
"prealloc size exceeds our guessed compile-time upper bound",
47+
);
48+
ffi::secp256k1_context_preallocated_clone(other, self.buf());
49+
}
50+
}
51+
52+
/// Accessor for the context as a raw context pointer.
53+
///
54+
/// On the first call, this will create the context.
55+
pub fn raw_ctx(&mut self) -> NonNull<ffi::Context> {
56+
let buf = self.buf();
57+
*self.1.get_or_insert_with(|| {
58+
// SAFETY: just FFI calls
59+
unsafe {
60+
assert!(
61+
ffi::secp256k1_context_preallocated_size(AllPreallocated::FLAGS)
62+
<= core::mem::size_of::<[AlignedType; MAX_PREALLOC_SIZE]>(),
63+
"prealloc size exceeds our guessed compile-time upper bound",
64+
);
65+
ffi::secp256k1_context_preallocated_create(buf, AllPreallocated::FLAGS)
66+
}
67+
})
68+
}
69+
}
70+
}
71+
// Needs to be pub(super) so that we can define a constructor for
72+
// SpinLock<SelfContainedContext> in the spinlock module. (We cannot do so generically
73+
// because we need a const constructor.)
74+
pub(super) use self_contained_context::SelfContainedContext;
75+
76+
static SECP256K1: SpinLock<SelfContainedContext> = SpinLock::<SelfContainedContext>::new();
77+
78+
/// Borrows the global context and do some operation on it.
79+
///
80+
/// If `randomize_seed` is provided, it is used to rerandomize the context after the
81+
/// operation is complete. If it is not provided, randomization is skipped.
82+
///
83+
/// Only a bit or two per signing operation is needed; if you have any entropy at all,
84+
/// you should provide it, even if you can't provide 32 random bytes.
85+
pub fn with_global_context<T, Ctx: Context, F: FnOnce(&Secp256k1<Ctx>) -> T>(
86+
f: F,
87+
rerandomize_seed: Option<&[u8; 32]>,
88+
) -> T {
89+
with_raw_global_context(
90+
|ctx| {
91+
let secp = ManuallyDrop::new(Secp256k1 { ctx, phantom: PhantomData });
92+
f(&*secp)
93+
},
94+
rerandomize_seed,
95+
)
96+
}
97+
98+
/// Borrows the global context as a raw pointer and do some operation on it.
99+
///
100+
/// If `randomize_seed` is provided, it is used to rerandomize the context after the
101+
/// operation is complete. If it is not provided, randomization is skipped.
102+
///
103+
/// Only a bit or two per signing operation is needed; if you have any entropy at all,
104+
/// you should provide it, even if you can't provide 32 random bytes.
105+
pub fn with_raw_global_context<T, F: FnOnce(NonNull<ffi::Context>) -> T>(
106+
f: F,
107+
rerandomize_seed: Option<&[u8; 32]>,
108+
) -> T {
109+
// Our function may be expensive, so before calling it, we copy the global
110+
// context into this local buffer on the stack. Then we can release it,
111+
// allowing other callers to use it simultaneously.
112+
let mut ctx = SelfContainedContext::new_uninitialized();
113+
if let Some(mut guard) = SECP256K1.try_lock() {
114+
let global_ctx = &mut *guard;
115+
ctx.clone_into(global_ctx)
116+
// (the lock is now dropped)
117+
}
118+
119+
// Obtain a raw pointer to the context, creating one if it has not been already,
120+
// and call the function.
121+
let ctx_ptr = ctx.raw_ctx();
122+
let ret = f(ctx_ptr);
123+
124+
// ...then rerandomize the local copy, and try to replace the global one
125+
// with this. There are three cases for how this can work:
126+
//
127+
// 1. In the happy path, we succeeded in getting the lock above, have
128+
// a copy of the global context, are rerandomizing and storing it.
129+
// Great.
130+
// 2. Same as above, except that another thread is doing the same thing
131+
// in parallel. Now we both have copies that we're rerandomizing, and
132+
// both will try to store it. One of us will clobber the other, wasting
133+
// work but otherwise not causing any problems.
134+
// 3. If we -failed- to get the lock above, we are rerandomizing a fresh
135+
// copy of the context object. This may "undo" previous rerandomization.
136+
// In theory if an attacker is able to reliably and repeatedly trigger
137+
// this situation, they will have defeated the rerandomization. Since
138+
// this is a defense-in-depth measure, we will accept this.
139+
if let Some(seed) = rerandomize_seed {
140+
// SAFETY: just a FFI call
141+
unsafe {
142+
assert_eq!(ffi::secp256k1_context_randomize(ctx_ptr, seed.as_ptr()), 1);
143+
}
144+
if let Some(ref mut guard) = SECP256K1.try_lock() {
145+
guard.clone_into(&mut ctx);
146+
}
147+
}
148+
ret
149+
}
150+
151+
/// Rerandomize the global context, using the given data as a seed.
152+
///
153+
/// The provided data will be mixed with the entropy from previous calls in a timing
154+
/// analysis resistant way. It is safe to directly pass secret data to this function.
155+
pub fn rerandomize_global_context(seed: &[u8; 32]) { with_raw_global_context(|_| {}, Some(seed)) }

src/context/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ use crate::ffi::types::{c_uint, c_void, AlignedType};
1010
use crate::ffi::{self, CPtr};
1111
use crate::{Error, Secp256k1};
1212

13-
#[cfg(feature = "std")]
1413
#[cfg_attr(feature = "std", path = "internal_std.rs")]
14+
#[cfg_attr(not(feature = "std"), path = "internal_nostd.rs")]
1515
mod internal;
1616

17-
#[cfg(test)] // will have a better feature-gate in next commit
17+
#[cfg(not(feature = "std"))]
1818
mod spinlock;
1919

20-
#[cfg(feature = "std")]
2120
pub use internal::{rerandomize_global_context, with_global_context, with_raw_global_context};
2221

2322
#[cfg(all(feature = "global-context", feature = "std"))]
@@ -379,7 +378,8 @@ impl<'buf> Secp256k1<AllPreallocated<'buf>> {
379378
/// * The version of `libsecp256k1` used to create `raw_ctx` must be **exactly the one linked
380379
/// into this library**.
381380
/// * The lifetime of the `raw_ctx` pointer must outlive `'buf`.
382-
/// * `raw_ctx` must point to writable memory (cannot be `ffi::secp256k1_context_no_precomp`).
381+
/// * `raw_ctx` must point to writable memory (cannot be `ffi::secp256k1_context_no_precomp`),
382+
/// **or** the user must never attempt to rerandomize the context.
383383
pub unsafe fn from_raw_all(
384384
raw_ctx: NonNull<ffi::Context>,
385385
) -> ManuallyDrop<Secp256k1<AllPreallocated<'buf>>> {

src/context/spinlock.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use core::hint::spin_loop;
55
use core::ops::{Deref, DerefMut};
66
use core::sync::atomic::{AtomicBool, Ordering};
77

8+
use crate::context::internal::SelfContainedContext;
9+
810
const MAX_SPINLOCK_ATTEMPTS: usize = 128;
911

1012
// Best-Effort Spinlock
@@ -32,6 +34,15 @@ pub struct SpinLock<T> {
3234
// Safety: `data` is accessed only while the `flag` is held.
3335
unsafe impl<T: Send> Sync for SpinLock<T> {}
3436

37+
impl SpinLock<SelfContainedContext> {
38+
pub const fn new() -> Self {
39+
Self {
40+
flag: AtomicBool::new(false),
41+
data: UnsafeCell::new(SelfContainedContext::new_uninitialized()),
42+
}
43+
}
44+
}
45+
3546
#[cfg(test)]
3647
impl SpinLock<u64> {
3748
pub const fn new() -> Self { Self { flag: AtomicBool::new(false), data: UnsafeCell::new(100) } }

src/lib.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,16 +184,13 @@ pub use secp256k1_sys as ffi;
184184
#[cfg(feature = "serde")]
185185
pub use serde;
186186

187-
#[cfg(feature = "std")]
188187
pub use crate::context::{
189-
rerandomize_global_context, with_global_context, with_raw_global_context,
188+
rerandomize_global_context, with_global_context, with_raw_global_context, AllPreallocated,
189+
Context, PreallocatedContext, SignOnlyPreallocated, Signing, Verification,
190+
VerifyOnlyPreallocated,
190191
};
191192
#[cfg(feature = "alloc")]
192193
pub use crate::context::{All, SignOnly, VerifyOnly};
193-
pub use crate::context::{
194-
AllPreallocated, Context, PreallocatedContext, SignOnlyPreallocated, Signing, Verification,
195-
VerifyOnlyPreallocated,
196-
};
197194
use crate::ffi::types::AlignedType;
198195
use crate::ffi::CPtr;
199196
pub use crate::key::{InvalidParityValue, Keypair, Parity, PublicKey, SecretKey, XOnlyPublicKey};

0 commit comments

Comments
 (0)