Skip to main content

ed25519_heapless/
lib.rs

1//! Curve25519 primitives for embedded targets, generic over bigint backends.
2//!
3//! Provides two operations on top of a shared field machinery, exposed through
4//! standard RustCrypto traits:
5//!
6//! - **Ed25519 signing / verification** — [`SigningKey`] via `signature::Signer`
7//!   (deterministic) and `RandomizedSigner` (hedged + blinded); [`VerifyingKey`]
8//!   via `signature::Verifier`. Twisted Edwards form, SHA-512 challenge, NAF
9//!   double-scalar multiplication.
10//! - **X25519 key agreement** — the KEM [`x25519_kem::X25519Kem`] (`kem::Kem`),
11//!   with an [`Unblinded`](x25519_kem::Unblinded) default and a
12//!   [`Blinded`](x25519_kem::Blinded) personality. Montgomery x-only ladder,
13//!   RFC 7748.
14//!
15//! Raw scalar-mult primitives (`x25519`, `x25519_blinded`, …) and the
16//! amortized-field `sign`/`verify` live in [`hazmat`] — reach there only for
17//! static-static DH or custom protocols. There is no top-level free-function API.
18//!
19//! Both curves live in F_p where p = 2^255 - 19, so they share the same
20//! `UnsignedModularInt` trait, `MontgomeryCtx`, and lazy-reduction helpers.
21//!
22//! # Usage
23//!
24//! ```ignore
25//! use ed25519_heapless::VerifyingKey;
26//! use signature::Verifier;
27//! use fixed_bigint::FixedUInt;
28//!
29//! type T = FixedUInt<u32, 16>;
30//! let valid = VerifyingKey::<T>::from_bytes(public_key)
31//!     .verify(message, &signature)
32//!     .is_ok();
33//! ```
34//!
35//! # Features
36//!
37//! - `std` (default) — enables logging and timing
38//! - `fixed-bigint` — enables the `fixed-bigint` backend
39//! - `no_std` compatible with `--no-default-features`
40//!
41//! # Constant-time scope
42//!
43//! Ed25519 verification operates on public data only; constant-time isn't a
44//! requirement there. For X25519 the secret scalar is sensitive: the ladder's
45//! conditional swap is branchless, but the underlying field arithmetic
46//! (`MontgomeryCtx`, `lazy_field`) has not been audited as constant-time and
47//! may exhibit data-dependent timing on some backends. Suitable for embedded
48//! bring-up; not yet hardened against side-channel adversaries.
49
50#![cfg_attr(not(feature = "std"), no_std)]
51
52pub(crate) mod curve25519_field;
53// `strict` (Ed25519 verify) depends on the SHA-512 challenge hash; gate it
54// behind the SHA-512 backend features so x25519-only consumers (which don't
55// need any SHA-512) can build with `default-features = false` + only an
56// x25519-relevant fixed-bigint feature.
57#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
58pub(crate) mod jsf;
59#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
60pub(crate) mod scalar_field;
61#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
62pub(crate) mod signing_key;
63#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
64pub(crate) mod strict;
65#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
66pub(crate) mod strict_sign;
67
68#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
69pub use signing_key::{SignError, SigningKey};
70// `sign` stays crate-internal (backs the `Signer` impl); `sign_with_fields` moves
71// to `hazmat`. The top-level free `sign` is gone — use the `Signer` trait.
72#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
73use signing_key::sign;
74pub(crate) mod x25519;
75pub mod x25519_kem;
76
77pub use curve25519_field::{
78    Curve25519Field, Curve25519FieldCt, CurveSetupError, VerifyField, curve25519_schoolbook,
79};
80pub use modmath::{Field, FieldCt, FieldNct, Residue, ResidueCt, ResidueNct};
81
82use core::marker::PhantomData;
83
84/// Bound bundle for the generic bigint backend Ed25519 verify + X25519
85/// build on. Pure marker trait: no methods, just a named alias for the
86/// supertrait union. Byte (de)serialization goes through
87/// [`const_num_traits::FromByteSlice`] (fallible slice-in) and
88/// [`const_num_traits::ToBytes`] (owned bytes with `AsRef<[u8]>`),
89/// which any conforming backend implements without ed25519 knowing the
90/// backend type.
91pub trait UnsignedModularInt:
92    Sized
93    + Clone
94    + core::cmp::PartialOrd
95    + const_num_traits::One
96    + const_num_traits::Zero
97    + const_num_traits::BitsPrecision
98    + const_num_traits::WithPrecision
99    + const_num_traits::ops::overflowing::OverflowingAdd<Output = Self>
100    + const_num_traits::WrappingAdd<Output = Self>
101    + const_num_traits::WrappingSub<Output = Self>
102    + const_num_traits::WrappingMul<Output = Self>
103    + core::ops::Shr<usize, Output = Self>
104    + core::ops::BitAnd<Output = Self>
105    + core::ops::ShrAssign<usize>
106    + modmath::MontStorage
107    + modmath::Parity
108    + const_num_traits::FromByteSlice
109    + const_num_traits::ToBytes
110{
111}
112
113impl<T> UnsignedModularInt for T where
114    T: Sized
115        + Clone
116        + core::cmp::PartialOrd
117        + const_num_traits::One
118        + const_num_traits::Zero
119        + const_num_traits::BitsPrecision
120        + const_num_traits::WithPrecision
121        + const_num_traits::ops::overflowing::OverflowingAdd<Output = Self>
122        + const_num_traits::WrappingAdd<Output = Self>
123        + const_num_traits::WrappingSub<Output = Self>
124        + const_num_traits::WrappingMul<Output = Self>
125        + core::ops::Shr<usize, Output = Self>
126        + core::ops::BitAnd<Output = Self>
127        + core::ops::ShrAssign<usize>
128        + modmath::MontStorage
129        + modmath::Parity
130        + const_num_traits::FromByteSlice
131        + const_num_traits::ToBytes
132{
133}
134
135/// Carrier bound bundle for the **verify** path. Verify is variable-time on
136/// public data, so — unlike [`SignBackend`] — it needs neither `Copy` nor
137/// `subtle`: a `Clone` heap carrier (e.g. num-bigint) satisfies it. This is the
138/// `Clone` / consume-self / by-reference subset of [`UnsignedModularInt`] that
139/// verify's own code exercises, minus the Montgomery-only ops (those live on
140/// the field's `VerifyField` impl, not here). Any `UnsignedModularInt` carrier
141/// also satisfies it, so the fixed-width `Copy` verify path is unaffected.
142pub trait VerifyBackend:
143    Clone
144    + PartialOrd
145    + const_num_traits::One
146    + const_num_traits::Zero
147    + const_num_traits::BitsPrecision
148    + const_num_traits::WithPrecision
149    + const_num_traits::ops::overflowing::OverflowingAdd<Output = Self>
150    + const_num_traits::WrappingAdd<Output = Self>
151    + const_num_traits::WrappingSub<Output = Self>
152    + core::ops::Shr<usize, Output = Self>
153    + core::ops::ShrAssign<usize>
154    + modmath::Parity
155    + const_num_traits::FromByteSlice
156{
157}
158
159impl<T> VerifyBackend for T where
160    T: Clone
161        + PartialOrd
162        + const_num_traits::One
163        + const_num_traits::Zero
164        + const_num_traits::BitsPrecision
165        + const_num_traits::WithPrecision
166        + const_num_traits::ops::overflowing::OverflowingAdd<Output = Self>
167        + const_num_traits::WrappingAdd<Output = Self>
168        + const_num_traits::WrappingSub<Output = Self>
169        + core::ops::Shr<usize, Output = Self>
170        + core::ops::ShrAssign<usize>
171        + modmath::Parity
172        + const_num_traits::FromByteSlice
173{
174}
175
176/// Load a static crypto-constant byte sequence into `T`.
177///
178/// Wraps `FromByteSlice::from_le_slice`. The `curve25519()` factories reject a
179/// narrow backend before any constant load, so with `BYTE_WIDTH >= 32` the `Err`
180/// branch is structurally unreachable. The fail-closed `T::zero()` fallback keeps
181/// `panic_fmt` unlinked; a `T = 0` from a genuinely invalid backend selection
182/// fails the verify / sign consistency checks downstream rather than panicking.
183#[inline]
184pub(crate) fn from_le_bytes<T>(bytes: &[u8]) -> T
185where
186    T: const_num_traits::FromByteSlice + const_num_traits::Zero,
187{
188    <T as const_num_traits::FromByteSlice>::from_le_slice(bytes).unwrap_or_else(|_| T::zero())
189}
190
191/// Read `x`'s little-endian byte encoding through a `&T` receiver — no
192/// unwrapped `T` value materializes on the stack. Wraps the returned
193/// `Bytes` in `Zeroizing` for defense-in-depth.
194///
195/// The awkward incantation this hides — `<&T as ToBytes>::to_le_bytes(x)` —
196/// exists because `x.to_le_bytes()` and `(*x).to_le_bytes()` both auto-deref
197/// and dispatch to the owned impl, copying `T` off the `Zeroizing<T>` stack
198/// slot before consuming it — the exact leak the `&T` impl prevents.
199#[inline]
200pub(crate) fn to_le_bytes_ct<T>(
201    x: &T,
202) -> zeroize::Zeroizing<<T as const_num_traits::ToBytes>::Bytes>
203where
204    T: const_num_traits::ToBytes,
205    for<'a> &'a T: const_num_traits::ToBytes<Bytes = <T as const_num_traits::ToBytes>::Bytes>,
206    <T as const_num_traits::ToBytes>::Bytes: zeroize::Zeroize,
207{
208    zeroize::Zeroizing::new(<&T as const_num_traits::ToBytes>::to_le_bytes(x))
209}
210
211/// Additive scalar blinding: `out = scalar + blinder·modulus`, little-endian and
212/// unreduced. When `modulus` annihilates the target group (`modulus·G = 𝒪`),
213/// `out·G = scalar·G`, but `out` — wider and varying with `blinder` per call —
214/// makes a fixed-base ladder process a different bit pattern each execution,
215/// defeating DPA trace averaging on the secret scalar.
216///
217/// 32-bit-limb schoolbook via [`const_num_traits::CarryingMul`], which widens
218/// through `u64` (no software 128-bit math on AVR / Cortex-M0). The scalar add
219/// is folded into the multiply-accumulate: `blinder·m + carry + s ≤ (2³²−1)² +
220/// 2·(2³²−1) < 2⁶⁴`, so `(lo, hi)` are one limb each and `hi` is the running
221/// carry. Word-aligned: `modulus` is a whole number of `u32` limbs and `N` a
222/// multiple of 4 holding `scalar + blinder·modulus` (one limb past it).
223///
224/// Shared by the x25519 (`8·ℓ·ℓ'`) and Ed25519-sign (`ℓ`) blinded paths.
225pub(crate) fn blind_scalar<const N: usize>(
226    scalar: &[u8; 32],
227    blinder: u32,
228    modulus: &[u8],
229) -> zeroize::Zeroizing<[u8; N]> {
230    // Fail-closed limb read; the chunk is always 4 bytes (`chunks_exact`).
231    fn read_le_u32(c: &[u8]) -> u32 {
232        <[u8; 4]>::try_from(c).map(u32::from_le_bytes).unwrap_or(0)
233    }
234    debug_assert_eq!(N % 4, 0);
235    debug_assert_eq!(modulus.len() % 4, 0);
236
237    let mut out = zeroize::Zeroizing::new([0u8; N]);
238    let mut mod_limbs = modulus.chunks_exact(4);
239    let mut scalar_limbs = scalar.chunks_exact(4);
240    let mut carry: u32 = 0;
241    for out_limb in out.chunks_exact_mut(4) {
242        let m = mod_limbs.next().map(read_le_u32).unwrap_or(0);
243        let s = scalar_limbs.next().map(read_le_u32).unwrap_or(0);
244        let (lo, hi) = const_num_traits::CarryingMul::carrying_mul_add(blinder, m, carry, s);
245        out_limb.copy_from_slice(&lo.to_le_bytes());
246        carry = hi;
247    }
248    debug_assert_eq!(carry, 0);
249
250    out
251}
252
253/// Aggregate bound bundle for the constant-time sign path: CT field
254/// arithmetic on Curve25519, byte (de)serialization, branchless
255/// selection, and `Zeroize` for secret-intermediate wiping.
256///
257/// `SigningKey<T>`, `sign`, `sign_with_fields`, and every CT point /
258/// scalar primitive in `strict_sign` would otherwise repeat the same
259/// 13-trait `where` clause. Auto-implemented for any backend that
260/// satisfies the listed bounds, so consumers don't write an explicit
261/// impl. The `for<'a> &'a T: …` HRTB stays at the call site because
262/// supertrait-elaboration of HRTBs is not yet reliable enough to
263/// propagate it automatically.
264#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
265pub trait SignBackend:
266    UnsignedModularInt
267    + Copy
268    + modmath::WideMul
269    + modmath::CiosMontMulCt
270    + const_num_traits::CtIsZero
271    + subtle::ConditionallySelectable
272    + subtle::ConstantTimeLess
273    + zeroize::DefaultIsZeroes
274{
275}
276
277#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
278impl<T> SignBackend for T where
279    T: UnsignedModularInt
280        + Copy
281        + modmath::WideMul
282        + modmath::CiosMontMulCt
283        + const_num_traits::CtIsZero
284        + subtle::ConditionallySelectable
285        + subtle::ConstantTimeLess
286        + zeroize::DefaultIsZeroes
287{
288}
289
290// ED25519 constants. Hex text is big-endian (matches every reference —
291// RFC 8032, dalek, `python -c 'print(hex(p))'` — so bytes can be
292// checked by eye); `hx_le` reverses to the little-endian byte order
293// the rest of the crate uses. Compile-time only: bad hex or wrong
294// length is a build error, and rodata is identical to a hand-written
295// LE array.
296pub(crate) const fn hx_le<const N: usize>(s: &str) -> [u8; N] {
297    const fn nib(c: u8) -> u8 {
298        match c {
299            b'0'..=b'9' => c - b'0',
300            b'a'..=b'f' => c - b'a' + 10,
301            b'A'..=b'F' => c - b'A' + 10,
302            _ => panic!("bad hex digit in curve constant"),
303        }
304    }
305    let s = s.as_bytes();
306    assert!(s.len() == 2 * N, "hex length must be 2*N chars");
307    let mut out = [0u8; N];
308    let mut i = 0;
309    while i < N {
310        // byte j of the big-endian text lands at index N-1-j in the LE array.
311        out[N - 1 - i] = (nib(s[2 * i]) << 4) | nib(s[2 * i + 1]);
312        i += 1;
313    }
314    out
315}
316
317// p = 2^255 - 19 (field prime).
318pub const P_BYTES: [u8; 32] =
319    hx_le("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed");
320// d = -121665/121666 mod p (twist coefficient of the edwards form).
321pub const D_BYTES: [u8; 32] =
322    hx_le("52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3");
323// q = 2^252 + 27742317777372353535851937790883648493 (scalar order).
324pub const Q_BYTES: [u8; 32] =
325    hx_le("1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed");
326
327// Base point B = (Gx, Gy) with Gy = 4/5 mod p, plus Gt = Gx·Gy for
328// the extended-twisted-edwards coordinate that carries the precomputed
329// product.
330pub const G_X_BYTES: [u8; 32] =
331    hx_le("216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51a");
332pub const G_Y_BYTES: [u8; 32] =
333    hx_le("6666666666666666666666666666666666666666666666666666666666666658");
334pub const G_T_BYTES: [u8; 32] =
335    hx_le("67875f0fd78b766566ea4e8e64abe37d20f09f80775152f56dde8ab3a5b7dda3");
336
337// modp_sqrt_m1 = 2^((p-1)/4) mod p = sqrt(-1) mod p, used in point decompression.
338pub const MODP_SQRT_M1_BYTES: [u8; 32] =
339    hx_le("2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0");
340
341// `verify` stays crate-internal (backs the `Verifier` impl below); the top-level
342// free `verify` is gone — use the `Verifier` trait. `verify_with_field` moves to
343// `hazmat`.
344#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
345use strict::verify;
346
347/// X25519 constants (RFC 7748). The scalar-mult primitives live in [`hazmat`].
348pub use x25519::{A24_BYTES, BASE_U_BYTES, BLINDING_MODULUS_BYTES};
349
350/// Low-level primitives with no safe trait wrapper — the RustCrypto `hazmat`
351/// convention (cf. `signature::hazmat`). Prefer the KEM ([`x25519_kem`]) and the
352/// `signature` traits; reach here only for the raw scalar-mult (static-static DH,
353/// custom protocols) or the amortized-field sign/verify.
354pub mod hazmat {
355    pub use crate::x25519::{clamp, x25519, x25519_base, x25519_base_blinded, x25519_blinded};
356
357    #[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
358    pub use crate::signing_key::sign_with_fields;
359    #[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
360    pub use crate::strict::verify_with_field;
361}
362
363/// Verifying key wrapper that implements `signature` crate traits.
364pub struct VerifyingKey<T> {
365    public: [u8; 32],
366    _marker: PhantomData<T>,
367}
368
369impl<T> VerifyingKey<T> {
370    /// Construct a verifying key from raw Ed25519 public key bytes.
371    pub const fn from_bytes(public: [u8; 32]) -> Self {
372        Self {
373            public,
374            _marker: PhantomData,
375        }
376    }
377
378    /// Return the wrapped public key bytes.
379    pub const fn to_bytes(&self) -> [u8; 32] {
380        self.public
381    }
382}
383
384impl<T> From<[u8; 32]> for VerifyingKey<T> {
385    fn from(public: [u8; 32]) -> Self {
386        Self::from_bytes(public)
387    }
388}
389
390impl<T> Copy for VerifyingKey<T> {}
391
392impl<T> Clone for VerifyingKey<T> {
393    fn clone(&self) -> Self {
394        *self
395    }
396}
397
398impl<T> PartialEq for VerifyingKey<T> {
399    fn eq(&self, other: &Self) -> bool {
400        self.public == other.public
401    }
402}
403
404impl<T> Eq for VerifyingKey<T> {}
405
406impl<T> core::fmt::Debug for VerifyingKey<T> {
407    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
408        f.debug_struct("VerifyingKey")
409            .field("public", &self.public)
410            .finish()
411    }
412}
413
414#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
415fn parse_signature(signature: &[u8]) -> Result<[u8; 64], signature::Error> {
416    signature.try_into().map_err(|_| signature::Error::new())
417}
418
419#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
420impl<T, S> signature::Verifier<S> for VerifyingKey<T>
421where
422    S: AsRef<[u8]>,
423    T: UnsignedModularInt + Copy + modmath::WideMul + modmath::CiosMontMul + modmath::NonCt,
424    for<'a> &'a T: core::ops::BitAnd<Output = T>
425        + const_num_traits::WrappingAdd<Output = T>
426        + const_num_traits::WrappingSub<Output = T>,
427{
428    fn verify(&self, msg: &[u8], signature: &S) -> Result<(), signature::Error> {
429        let signature = parse_signature(signature.as_ref())?;
430        if verify::<T>(self.public, msg, signature) {
431            Ok(())
432        } else {
433            Err(signature::Error::new())
434        }
435    }
436}
437
438#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
439impl<T> signature::Signer<[u8; 64]> for SigningKey<T>
440where
441    T: SignBackend,
442    for<'a> &'a T: const_num_traits::WrappingAdd<Output = T>
443        + const_num_traits::WrappingSub<Output = T>
444        + const_num_traits::ToBytes<Bytes = <T as const_num_traits::ToBytes>::Bytes>,
445    <T as const_num_traits::ToBytes>::Bytes: zeroize::Zeroize,
446{
447    fn try_sign(&self, msg: &[u8]) -> Result<[u8; 64], signature::Error> {
448        sign::<T>(self, msg).map_err(|_| signature::Error::new())
449    }
450}
451
452/// Hedged, side-channel-blinded Ed25519 signing. Produces a standard RFC 8032
453/// signature (any verifier accepts it), but the nonce is hedged with RNG output
454/// so the output is non-deterministic, and the `r·G` scalar multiply is scalar-
455/// and coordinate-blinded. The blinding is best-effort against physical power/EM
456/// analysis and is not validated by leakage-measurement hardware. For plain
457/// deterministic RFC 8032 signing, use the [`signature::Signer`] impl.
458#[cfg(any(feature = "sha512-hmac-sha512", feature = "sha512-sha2"))]
459impl<T> signature::RandomizedSigner<[u8; 64]> for SigningKey<T>
460where
461    T: SignBackend,
462    for<'a> &'a T: const_num_traits::WrappingAdd<Output = T>
463        + const_num_traits::WrappingSub<Output = T>
464        + const_num_traits::ToBytes<Bytes = <T as const_num_traits::ToBytes>::Bytes>,
465    <T as const_num_traits::ToBytes>::Bytes: zeroize::Zeroize,
466{
467    fn try_sign_with_rng<R: rand_core::TryCryptoRng + ?Sized>(
468        &self,
469        rng: &mut R,
470        msg: &[u8],
471    ) -> Result<[u8; 64], signature::Error> {
472        signing_key::sign_blinded::<T, R>(rng, self, msg).map_err(|_| signature::Error::new())
473    }
474}