From 4c3c94af59f9b7b23338be833f327c604b0af572 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 22 Oct 2026 00:29:10 +0200 Subject: [PATCH 1/3] crypto: a 5x51 field with typed bounds, and erasure at the boundary The 2^255 - 19 field multiplied each of its 25 products by a runtime 1 or 19 as u128 (three multiplications each), squared by multiplying, carried in three full passes and erased every element and its u128 workspace with volatile stores and a fence. Inversion was a square and a multiply per exponent bit. The field now has two element types for its limb bounds, as Erbsen et al. describe ("Simple High-Level Code For Cryptographic Arithmetic", IEEE S&P 2019): `Tight`, what a multiplication or carry leaves, and `Loose`, the sum or difference of two tight elements, which multiplication accepts and addition refuses at compile time. Multiplication premultiplies four of b's limbs by 19, squaring takes 15 products, `mul_small` is one row, and the carry is one pass with the top carry folded by 19 and one short carry into limb 1. Every carry and product uses wrapping operations whose bounds the types establish, so the shipped profile adds no overflow check. Inversion and the square root's power use the curve25519 addition chain (Bernstein 2006, section 5): 254 squarings and 11 multiplications. X25519's ladder swaps with one XOR mask per coordinate pair instead of six selects, and multiplies by 121665 with `mul_small`. Ed25519 doubles with RFC 8032's dedicated doubling in its fixed-schedule multiplication and its small-order check. Erasure follows the owner's conditional approval of item 26: field elements, NIST residues and points are `Copy` intermediates and are no longer erased one by one. Secret owners still erase on drop, and a secret operation's explicit state is a `secret::Workspace`, erased when the operation returns on any path: the X25519 ladder's coordinates and the Ed25519 and NIST scalar multiplications' accumulators. `secret`'s header states the contract, including what it does not erase: registers, spills, compiler copies, moved-from locations and finished frames. This lands only with M1's measurement and the contract's review. The integer-oracle tests cover squaring, mul_small, negation, inversion and the square root's power, loose operands into every multiplication, and the largest limbs each bound admits. Co-Authored-By: Claude Opus 5.5 --- lib/crypto/src/ed25519.rs | 119 ++++++---- lib/crypto/src/field25519.rs | 372 ++++++++++++++++++++++-------- lib/crypto/src/lib.rs | 8 +- lib/crypto/src/montgomery.rs | 28 +-- lib/crypto/src/nist.rs | 37 +-- lib/crypto/src/secret.rs | 41 +++- lib/crypto/src/test_arithmetic.rs | 79 +++++-- lib/crypto/src/x25519.rs | 47 ++-- 8 files changed, 499 insertions(+), 232 deletions(-) diff --git a/lib/crypto/src/ed25519.rs b/lib/crypto/src/ed25519.rs index 019709acaf..5bbff61944 100644 --- a/lib/crypto/src/ed25519.rs +++ b/lib/crypto/src/ed25519.rs @@ -4,19 +4,25 @@ //! Signing (feature `sign`) uses fixed-schedule field/scalar arithmetic and //! complete Edwards addition; neither tables nor branches depend on secrets. -use crate::{Error, ct, field25519::F, hash::Sha512, scalar25519::S}; +use crate::{ + Error, ct, + field25519::{Multiply, Tight}, + hash::Sha512, + scalar25519::S, + secret::Workspace, +}; #[cfg(feature = "sign")] use crate::{Random, secret::Secret}; /// 2d for the curve's d = -121665/121666 (RFC 8032 section 5.1). -const D2: F = F::from_bytes(&[ +const D2: Tight = Tight::from_bytes(&[ 0x59, 0xf1, 0xb2, 0x26, 0x94, 0x9b, 0xd6, 0xeb, 0x56, 0xb1, 0x83, 0x82, 0x9a, 0x14, 0xe0, 0x00, 0x30, 0xd1, 0xf3, 0xee, 0xf2, 0x80, 0x8e, 0x19, 0xe7, 0xfc, 0xdf, 0x56, 0xdc, 0xd9, 0x06, 0x24, ]); -const D: F = F::from_bytes(&[ +const D: Tight = Tight::from_bytes(&[ 0xa3, 0x78, 0x59, 0x13, 0xca, 0x4d, 0xeb, 0x75, 0xab, 0xd8, 0x41, 0x41, 0x4d, 0x0a, 0x70, 0x00, 0x98, 0xe8, 0x79, 0x77, 0x79, 0x40, 0xc7, 0x8c, 0x73, 0xfe, 0x6f, 0x2b, 0xee, 0x6c, 0x03, 0x52, ]); -const SQRT_M1: F = F::from_bytes(&[ +const SQRT_M1: Tight = Tight::from_bytes(&[ 0xb0, 0xa0, 0x0e, 0x4a, 0x27, 0x1b, 0xee, 0xc4, 0x78, 0xe4, 0x2f, 0xad, 0x06, 0x18, 0x43, 0x2f, 0xa7, 0xd7, 0xfb, 0x3d, 0x99, 0x00, 0x4d, 0x2b, 0x0b, 0xdf, 0xc1, 0x4f, 0x80, 0x24, 0x83, 0x2b, ]); @@ -25,66 +31,66 @@ const B_ENCODED: [u8; 32] = [ 0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, ]; -const B_X: F = F::from_bytes(&[ +const B_X: Tight = Tight::from_bytes(&[ 0x1a, 0xd5, 0x25, 0x8f, 0x60, 0x2d, 0x56, 0xc9, 0xb2, 0xa7, 0x25, 0x95, 0x60, 0xc7, 0x2c, 0x69, 0x5c, 0xdc, 0xd6, 0xfd, 0x31, 0xe2, 0xa4, 0xc0, 0xfe, 0x53, 0x6e, 0xcd, 0xd3, 0x36, 0x69, 0x21, ]); -const B_Y: F = F::from_bytes(&B_ENCODED); +const B_Y: Tight = Tight::from_bytes(&B_ENCODED); /// x * y of B. -const B_T: F = F::from_bytes(&[ +const B_T: Tight = Tight::from_bytes(&[ 0xa3, 0xdd, 0xb7, 0xa5, 0xb3, 0x8a, 0xde, 0x6d, 0xf5, 0x52, 0x51, 0x77, 0x80, 0x9f, 0xf0, 0x20, 0x7d, 0xe3, 0xab, 0x64, 0x8e, 0x4e, 0xea, 0x66, 0x65, 0x76, 0x8b, 0xd7, 0x0f, 0x5f, 0x87, 0x67, ]); +#[derive(Clone, Copy)] +/// Extended coordinates (X : Y : Z : T) with x = X/Z, y = Y/Z, xy = T/Z. struct Point { - x: F, - y: F, - z: F, - t: F, + x: Tight, + y: Tight, + z: Tight, + t: Tight, } impl Point { fn identity() -> Self { Self { - x: F::zero(), - y: F::one(), - z: F::one(), - t: F::zero(), + x: Tight::ZERO, + y: Tight::ONE, + z: Tight::ONE, + t: Tight::ZERO, } } fn base() -> Self { Self { x: B_X, y: B_Y, - z: F::one(), + z: Tight::ONE, t: B_T, } } + /// RFC 8032 section 5.1.3. Refusal branches depend on the public encoding. fn decode(input: &[u8; 32]) -> Result { let mut b = *input; let sign = u64::from(b[31] >> 7); b[31] &= 127; - let y = F::from_bytes(&b); + let y = Tight::from_bytes(&b); if !ct::equal(&b, &y.bytes()) { return Err(Error::InvalidPoint); } let y2 = y.square(); - let u = y2.sub(&F::one()); - let v = D.mul(&y2).add(&F::one()); + let u = y2.sub(&Tight::ONE); + let v = D.mul(&y2).add(&Tight::ONE).carry(); let q = u.mul(&v.invert()); - let mut exponent = [0xff; 32]; - exponent[0] = 0xfe; - exponent[31] = 0x0f; - let root = q.pow(&exponent, 252); + let root = q.mul(&q.pow_p58()); let alt = root.mul(&SQRT_M1); - let root = F::select(&alt, &root, root.square().eq(&q)); - if root.square().eq(&q) == 0 || (root.eq(&F::zero()) == 1 && sign == 1) { + let root = Tight::select(&alt, &root, root.square().eq(&q)); + if root.square().eq(&q) == 0 || (root.is_zero() == 1 && sign == 1) { return Err(Error::InvalidPoint); } - let x = F::select(&root, &root.neg(), u64::from(root.bytes()[0] & 1) ^ sign); + let x = Tight::select(&root, &root.neg().carry(), root.is_negative() ^ sign); let t = x.mul(&y); Ok(Self { x, y, - z: F::one(), + z: Tight::ONE, t, }) } @@ -93,15 +99,15 @@ impl Point { let inverse = self.z.invert(); let x = self.x.mul(&inverse); let mut y = self.y.mul(&inverse).bytes(); - y[31] |= (x.bytes()[0] & 1) << 7; + y[31] |= (x.is_negative() as u8) << 7; y } + /// RFC 8032 section 5.1.4's addition, complete on the curve. fn add(&self, q: &Self) -> Self { let a = self.y.sub(&self.x).mul(&q.y.sub(&q.x)); let b = self.y.add(&self.x).mul(&q.y.add(&q.x)); let c = self.t.mul(&q.t).mul(&D2); - let zz = self.z.mul(&q.z); - let d = zz.add(&zz); + let d = self.z.mul(&q.z.add(&q.z)); let e = b.sub(&a); let f = d.sub(&c); let g = d.add(&c); @@ -113,33 +119,51 @@ impl Point { z: f.mul(&g), } } + /// RFC 8032 section 5.1.4's doubling. + fn double(&self) -> Self { + let a = self.x.square(); + let b = self.y.square(); + let c = self.z.square().mul_small(2); + let h = a.add(&b).carry(); + let e = h.sub(&self.x.add(&self.y).square()); + let g = a.sub(&b).carry(); + let f = c.add(&g); + Self { + x: e.mul(&f), + y: g.mul(&h), + t: e.mul(&h), + z: f.mul(&g), + } + } fn select(a: &Self, b: &Self, bit: u64) -> Self { Self { - x: F::select(&a.x, &b.x, bit), - y: F::select(&a.y, &b.y, bit), - z: F::select(&a.z, &b.z, bit), - t: F::select(&a.t, &b.t, bit), + x: Tight::select(&a.x, &b.x, bit), + y: Tight::select(&a.y, &b.y, bit), + z: Tight::select(&a.z, &b.z, bit), + t: Tight::select(&a.t, &b.t, bit), } } + /// scalar * self, on a fixed schedule. The accumulator is the operation's + /// secret state, erased when it returns; the result is the caller's. fn mul(&self, scalar: &[u8; 32]) -> Self { - let mut r = Self::identity(); + let mut r = Workspace(Self::identity()); for i in (0..256).rev() { - let double = r.add(&r); + let double = r.0.double(); let added = double.add(self); - r = Self::select(&double, &added, u64::from((scalar[i / 8] >> (i % 8)) & 1)); + r.0 = Self::select(&double, &added, u64::from((scalar[i / 8] >> (i % 8)) & 1)); } - r + r.0 } fn eq(&self, q: &Self) -> u64 { self.x.mul(&q.z).eq(&q.x.mul(&self.z)) & self.y.mul(&q.z).eq(&q.y.mul(&self.z)) } fn small_order(&self) -> bool { - let p = self.add(self); - let p = p.add(&p); - let p = p.add(&p); - p.eq(&Self::identity()) == 1 + self.double().double().double().eq(&Self::identity()) == 1 } } + +impl crate::secret::Zeroable for Point {} + pub struct PublicKey { encoded: [u8; 32], point: Point, @@ -245,9 +269,14 @@ mod tests { for (a, b) in [(&decoded.x, &base.x), (&decoded.y, &base.y), (&decoded.t, &base.t)] { assert_eq!(a.bytes(), b.bytes()); } - assert_eq!(D.add(&D).bytes(), D2.bytes()); - let d = F::small(121665).neg().mul(&F::small(121666).invert()); + assert_eq!(D.add(&D).carry().bytes(), D2.bytes()); + let small = |x: u32| { + let mut b = [0; 32]; + b[..4].copy_from_slice(&x.to_le_bytes()); + Tight::from_bytes(&b) + }; + let d = small(121665).neg().mul(&small(121666).invert()); assert_eq!(d.bytes(), D.bytes()); - assert_eq!(SQRT_M1.square().bytes(), F::one().neg().bytes()); + assert_eq!(SQRT_M1.square().bytes(), Tight::ONE.neg().carry().bytes()); } } diff --git a/lib/crypto/src/field25519.rs b/lib/crypto/src/field25519.rs index 0e25ee987b..1c76671f6c 100644 --- a/lib/crypto/src/field25519.rs +++ b/lib/crypto/src/field25519.rs @@ -1,125 +1,301 @@ -//! Arithmetic modulo 2^255-19. Five radix-2^51 limbs; products fit u128. -//! Every operation has a fixed schedule. Encodings and equality are public at -//! verification boundaries. Arithmetic storage is erased on drop. +//! Arithmetic modulo p = 2^255 - 19 in five radix-2^51 limbs with u128 +//! products (Bernstein, "Curve25519: new Diffie-Hellman speed records", PKC +//! 2006). The limb bounds are types: a [`Tight`] element is what a +//! multiplication or a carry leaves, a [`Loose`] one the sum or difference of +//! two tight ones. Multiplication takes either; addition and subtraction take +//! tight operands. Every operation has a fixed schedule and no branch or +//! address that depends on an element. Encodings and equality are public at +//! verification boundaries. +//! +//! Elements are `Copy` values and are not erased: an owner of secret state +//! erases its own storage (see `secret`). -use crate::{ct, secret::wipe64}; -const M: u64 = (1 << 51) - 1; -pub(crate) struct F([u64; 5]); -impl Drop for F { - fn drop(&mut self) { - wipe64(&mut self.0) - } +use crate::ct; + +const MASK: u64 = (1 << 51) - 1; + +/// Limbs 0, 2, 3 and 4 below 2^51, limb 1 below 2^51 + 2^19. +#[derive(Clone, Copy)] +pub(crate) struct Tight([u64; 5]); + +/// Every limb below 2^53. +#[derive(Clone, Copy)] +pub(crate) struct Loose([u64; 5]); + +/// A multiplication operand: either bound. +pub(crate) trait Operand { + fn limbs(&self) -> &[u64; 5]; } -impl F { - pub(crate) fn zero() -> Self { - Self([0; 5]) - } - pub(crate) fn one() -> Self { - Self([1, 0, 0, 0, 0]) + +impl Operand for Tight { + #[inline(always)] + fn limbs(&self) -> &[u64; 5] { + &self.0 } - pub(crate) fn small(x: u32) -> Self { - Self([u64::from(x), 0, 0, 0, 0]) +} + +impl Operand for Loose { + #[inline(always)] + fn limbs(&self) -> &[u64; 5] { + &self.0 } +} + +/// 2p, limb by limb: above every tight limb, so a subtraction never borrows. +const TWO_P: [u64; 5] = [2 * (MASK - 18), 2 * MASK, 2 * MASK, 2 * MASK, 2 * MASK]; + +#[inline(always)] +fn wide(a: u64, b: u64) -> u128 { + u128::from(a).wrapping_mul(u128::from(b)) +} + +/// A column of products. The limb bounds keep every sum below 2^115, so the +/// wrapping additions are exact and carry no overflow check. +macro_rules! column { + ($first:expr $(, $rest:expr)*) => { $first $(.wrapping_add($rest))* }; +} + +/// Columns below 2^115 into tight limbs: one carry pass, the top carry times +/// 19 into limb 0 (2^255 = 19 mod p), and one more carry into limb 1. +#[inline(always)] +fn carry(v: [u128; 5]) -> Tight { + let [v0, v1, v2, v3, v4] = v; + let v1 = v1.wrapping_add(v0 >> 51); + let v2 = v2.wrapping_add(v1 >> 51); + let v3 = v3.wrapping_add(v2 >> 51); + let v4 = v4.wrapping_add(v3 >> 51); + let r0 = u128::from(v0 as u64 & MASK).wrapping_add((v4 >> 51).wrapping_mul(19)); + let r1 = (v1 as u64 & MASK).wrapping_add((r0 >> 51) as u64); + Tight([ + r0 as u64 & MASK, + r1, + v2 as u64 & MASK, + v3 as u64 & MASK, + v4 as u64 & MASK, + ]) +} + +impl Tight { + pub(crate) const ZERO: Self = Self([0; 5]); + pub(crate) const ONE: Self = Self([1, 0, 0, 0, 0]); + /// The low 255 bits of a little-endian encoding. pub(crate) const fn from_bytes(b: &[u8; 32]) -> Self { - let mut v = [0u64; 5]; + let mut w = [0u64; 4]; let mut i = 0; - while i < 255 { - v[i / 51] |= (((b[i / 8] >> (i % 8)) & 1) as u64) << (i % 51); + while i < 32 { + w[i / 8] |= (b[i] as u64) << (8 * (i % 8)); i += 1; } - Self(v) - } - fn carry(v: &mut [u128; 5]) -> Self { - // Products start at <=77*(2^51-1)^2; three passes leave every limb <2^51. - for _ in 0..3 { - for i in 0..4 { - let c = v[i] >> 51; - v[i] &= u128::from(M); - v[i + 1] += c; - } - let c = v[4] >> 51; - v[4] &= u128::from(M); - v[0] += c * 19; - } - let out = Self(core::array::from_fn(|i| v[i] as u64)); - // Volatile erasure covers the explicit wide product workspace. - for x in v { - unsafe { core::ptr::write_volatile(x, 0) } - } - core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); - out + Self([ + w[0] & MASK, + (w[0] >> 51 | w[1] << 13) & MASK, + (w[1] >> 38 | w[2] << 26) & MASK, + (w[2] >> 25 | w[3] << 39) & MASK, + (w[3] >> 12) & MASK, + ]) } - pub(crate) fn add(&self, b: &Self) -> Self { - Self::carry(&mut core::array::from_fn(|i| { - u128::from(self.0[i]) + u128::from(b.0[i]) - })) + + #[cfg(test)] + pub(crate) fn from_limbs(limbs: [u64; 5]) -> Self { + Self(limbs) } - pub(crate) fn sub(&self, b: &Self) -> Self { - Self::carry(&mut core::array::from_fn(|i| { - u128::from(self.0[i]) + 2 * u128::from(if i == 0 { M - 18 } else { M }) - - u128::from(b.0[i]) - })) + + pub(crate) fn add(&self, b: &Tight) -> Loose { + let (a, b) = (&self.0, &b.0); + Loose([ + a[0].wrapping_add(b[0]), + a[1].wrapping_add(b[1]), + a[2].wrapping_add(b[2]), + a[3].wrapping_add(b[3]), + a[4].wrapping_add(b[4]), + ]) } - pub(crate) fn neg(&self) -> Self { - Self::zero().sub(self) + + /// self + 2p - b, limb by limb. + pub(crate) fn sub(&self, b: &Tight) -> Loose { + let (a, b) = (&self.0, &b.0); + Loose([ + a[0].wrapping_add(TWO_P[0]).wrapping_sub(b[0]), + a[1].wrapping_add(TWO_P[1]).wrapping_sub(b[1]), + a[2].wrapping_add(TWO_P[2]).wrapping_sub(b[2]), + a[3].wrapping_add(TWO_P[3]).wrapping_sub(b[3]), + a[4].wrapping_add(TWO_P[4]).wrapping_sub(b[4]), + ]) } - pub(crate) fn mul(&self, b: &Self) -> Self { - let mut v = [0u128; 5]; - for i in 0..5 { - for j in 0..5 { - let k = i + j; - v[k % 5] += - u128::from(self.0[i]) * u128::from(b.0[j]) * if k >= 5 { 19 } else { 1 }; - } + + pub(crate) fn neg(&self) -> Loose { + Self::ZERO.sub(self) + } + + /// self^(2^k). + pub(crate) fn square_times(&self, k: u32) -> Tight { + let mut r = self.square(); + for _ in 1..k { + r = r.square(); } - Self::carry(&mut v) + r + } + + /// self^(2^250 - 1) and self^11, the shared head of the inversion and + /// square-root chains (Bernstein 2006, section 5). + fn pow_2_250_1(&self) -> (Tight, Tight) { + let z2 = self.square(); + let z9 = z2.square_times(2).mul(self); + let z11 = z9.mul(&z2); + let z_5_0 = z11.square().mul(&z9); + let z_10_0 = z_5_0.square_times(5).mul(&z_5_0); + let z_20_0 = z_10_0.square_times(10).mul(&z_10_0); + let z_40_0 = z_20_0.square_times(20).mul(&z_20_0); + let z_50_0 = z_40_0.square_times(10).mul(&z_10_0); + let z_100_0 = z_50_0.square_times(50).mul(&z_50_0); + let z_200_0 = z_100_0.square_times(100).mul(&z_100_0); + let z_250_0 = z_200_0.square_times(50).mul(&z_50_0); + (z_250_0, z11) + } + + /// self^(p - 2) = self^(2^255 - 21): 254 squarings and 11 multiplications. + pub(crate) fn invert(&self) -> Tight { + let (z_250_0, z11) = self.pow_2_250_1(); + z_250_0.square_times(5).mul(&z11) } - pub(crate) fn square(&self) -> Self { - self.mul(self) + + /// self^((p - 5) / 8) = self^(2^252 - 3), for square roots. + pub(crate) fn pow_p58(&self) -> Tight { + let (z_250_0, _) = self.pow_2_250_1(); + z_250_0.square_times(2).mul(self) } + pub(crate) fn select(a: &Self, b: &Self, bit: u64) -> Self { - Self(core::array::from_fn(|i| ct::select(a.0[i], b.0[i], bit))) - } - pub(crate) fn pow(&self, e: &[u8; 32], bits: usize) -> Self { - let mut r = Self::one(); - for i in (0..bits).rev() { - let square = r.square(); - let product = square.mul(self); - r = Self::select(&square, &product, u64::from((e[i / 8] >> (i % 8)) & 1)); - } - r + let m = ct::mask(bit); + let (a, b) = (&a.0, &b.0); + Self([ + a[0] ^ ((a[0] ^ b[0]) & m), + a[1] ^ ((a[1] ^ b[1]) & m), + a[2] ^ ((a[2] ^ b[2]) & m), + a[3] ^ ((a[3] ^ b[3]) & m), + a[4] ^ ((a[4] ^ b[4]) & m), + ]) } - pub(crate) fn invert(&self) -> Self { - let mut e = [0xff; 32]; - e[0] = 0xeb; - e[31] = 0x7f; - self.pow(&e, 255) + + /// Exchanges a and b under a mask of all ones, and leaves them under zero. + pub(crate) fn swap(a: &mut Self, b: &mut Self, mask: u64) { + for i in 0..5 { + let t = (a.0[i] ^ b.0[i]) & mask; + a.0[i] ^= t; + b.0[i] ^= t; + } } + + /// The canonical little-endian encoding. pub(crate) fn bytes(&self) -> [u8; 32] { - let n = Self::carry(&mut self.0.map(u128::from)); - let mut t = [0u64; 5]; - let mut borrow = 0; - for (i, word) in t.iter_mut().enumerate() { - let p = if i == 0 { M - 18 } else { M }; - let d = (1u128 << 51) + u128::from(n.0[i]) - u128::from(p) - u128::from(borrow); - *word = d as u64 & M; - borrow = 1 - ((d >> 51) as u64); - } - let reduced = Self(t); - let value = Self::select(&reduced, &n, borrow); + let h = &self.0; + // q = floor((h + 19) / 2^255), which is 1 exactly when h >= p for a + // tight h < 2p. + let mut q = h[0].wrapping_add(19) >> 51; + q = h[1].wrapping_add(q) >> 51; + q = h[2].wrapping_add(q) >> 51; + q = h[3].wrapping_add(q) >> 51; + q = h[4].wrapping_add(q) >> 51; + // h - q p = h + 19 q - q 2^255, with bit 255 dropped. + let h0 = h[0].wrapping_add(q.wrapping_mul(19)); + let h1 = h[1].wrapping_add(h0 >> 51); + let h2 = h[2].wrapping_add(h1 >> 51); + let h3 = h[3].wrapping_add(h2 >> 51); + let h4 = h[4].wrapping_add(h3 >> 51); + let l = [h0 & MASK, h1 & MASK, h2 & MASK, h3 & MASK, h4 & MASK]; + let words = [ + l[0] | l[1] << 51, + l[1] >> 13 | l[2] << 38, + l[2] >> 26 | l[3] << 25, + l[3] >> 39 | l[4] << 12, + ]; let mut out = [0; 32]; - for i in 0..255 { - out[i / 8] |= ((value.0[i / 51] >> (i % 51)) as u8 & 1) << (i % 8) + for (i, word) in words.iter().enumerate() { + out[8 * i..8 * i + 8].copy_from_slice(&word.to_le_bytes()); } out } + + /// One when the canonical encoding's low bit is set. + pub(crate) fn is_negative(&self) -> u64 { + u64::from(self.bytes()[0] & 1) + } + + /// One when both encode the same element. pub(crate) fn eq(&self, b: &Self) -> u64 { - let mut a = self.bytes(); - let mut bb = b.bytes(); - let result = u64::from(ct::equal(&a, &bb)); - crate::secret::wipe(&mut a); - crate::secret::wipe(&mut bb); - result + u64::from(ct::equal(&self.bytes(), &b.bytes())) + } + + pub(crate) fn is_zero(&self) -> u64 { + self.eq(&Self::ZERO) + } +} + +impl Loose { + /// The same element, carried tight. + pub(crate) fn carry(&self) -> Tight { + let v = &self.0; + carry([v[0].into(), v[1].into(), v[2].into(), v[3].into(), v[4].into()]) } } + +/// Multiplications, each leaving a tight element. +pub(crate) trait Multiply: Operand { + /// The 25 products, with b's limbs that wrap past 2^255 premultiplied by + /// 19. + #[inline(always)] + fn mul(&self, b: &impl Operand) -> Tight { + let (a, b) = (self.limbs(), b.limbs()); + let b1 = b[1].wrapping_mul(19); + let b2 = b[2].wrapping_mul(19); + let b3 = b[3].wrapping_mul(19); + let b4 = b[4].wrapping_mul(19); + let v = [ + column!(wide(a[0], b[0]), wide(a[1], b4), wide(a[2], b3), wide(a[3], b2), wide(a[4], b1)), + column!(wide(a[0], b[1]), wide(a[1], b[0]), wide(a[2], b4), wide(a[3], b3), wide(a[4], b2)), + column!(wide(a[0], b[2]), wide(a[1], b[1]), wide(a[2], b[0]), wide(a[3], b4), wide(a[4], b3)), + column!(wide(a[0], b[3]), wide(a[1], b[2]), wide(a[2], b[1]), wide(a[3], b[0]), wide(a[4], b4)), + column!(wide(a[0], b[4]), wide(a[1], b[3]), wide(a[2], b[2]), wide(a[3], b[1]), wide(a[4], b[0])), + ]; + carry(v) + } + + #[inline(always)] + fn square(&self) -> Tight { + square(self.limbs()) + } + + /// self * k for k < 2^17. + #[inline(always)] + fn mul_small(&self, k: u32) -> Tight { + let a = self.limbs(); + let k = u64::from(k); + let v = [wide(a[0], k), wide(a[1], k), wide(a[2], k), wide(a[3], k), wide(a[4], k)]; + carry(v) + } +} + +impl Multiply for Tight {} +impl Multiply for Loose {} + +/// The 15 distinct products of a square. +#[inline(always)] +fn square(a: &[u64; 5]) -> Tight { + let d0 = a[0].wrapping_mul(2); + let d1 = a[1].wrapping_mul(2); + let d2 = a[2].wrapping_mul(2); + let d3 = a[3].wrapping_mul(2); + let a3_19 = a[3].wrapping_mul(19); + let a4_19 = a[4].wrapping_mul(19); + let v = [ + column!(wide(a[0], a[0]), wide(d1, a4_19), wide(d2, a3_19)), + column!(wide(d0, a[1]), wide(a[3], a3_19), wide(d2, a4_19)), + column!(wide(d0, a[2]), wide(a[1], a[1]), wide(d3, a4_19)), + column!(wide(d0, a[3]), wide(d1, a[2]), wide(a[4], a4_19)), + column!(wide(d0, a[4]), wide(d1, a[3]), wide(a[2], a[2])), + ]; + carry(v) +} + +impl crate::secret::Zeroable for Tight {} diff --git a/lib/crypto/src/lib.rs b/lib/crypto/src/lib.rs index 848946d4f0..d7ec89880e 100644 --- a/lib/crypto/src/lib.rs +++ b/lib/crypto/src/lib.rs @@ -6,10 +6,10 @@ //! algorithm choices and CPU capabilities are public. Module contracts identify //! operations permitted to depend on public data. //! -//! Owned secret storage is overwritten with volatile writes on drop. This does -//! not promise erasure of compiler-created copies, registers or abandoned -//! storage after process termination. Constant-time source structure requires -//! validation of the compiler and hardware used to deploy it. +//! Secret owners and secret operations' workspaces are erased as `secret`'s +//! contract states; arithmetic intermediates, registers and compiler-made +//! copies are not. Constant-time source structure requires validation of the +//! compiler and hardware used to deploy it. #![no_std] #![deny(unsafe_op_in_unsafe_fn)] diff --git a/lib/crypto/src/montgomery.rs b/lib/crypto/src/montgomery.rs index c91dd67c76..b3f833272c 100644 --- a/lib/crypto/src/montgomery.rs +++ b/lib/crypto/src/montgomery.rs @@ -2,12 +2,12 @@ //! words. Each modulus is a type, so a residue modulo one cannot meet a residue //! modulo another. Every operation on a residue has a fixed schedule and a //! masked final subtraction; inversion's exponent is public. Residues are -//! erased on drop and workspaces after use. +//! `Copy` intermediates, not erased (`secret`'s contract). #![allow( clippy::needless_range_loop, reason = "carry chains index parallel fixed-size arrays; iterator adaptors stay out of line in the shipped profile" )] -use crate::{ct, secret::wipe64}; +use crate::ct; use core::marker::PhantomData; /// An odd modulus p with 2^(64N-1) < p < 2^(64N), and its Montgomery @@ -30,11 +30,13 @@ pub(crate) const fn encode(x: &[u64; N], p: &[u64; N]) -> [u64; /// A residue modulo `M` in Montgomery form, below `M::P`. pub(crate) struct Residue, const N: usize>([u64; N], PhantomData); -impl, const N: usize> Drop for Residue { - fn drop(&mut self) { - wipe64(&mut self.0) +impl, const N: usize> Clone for Residue { + fn clone(&self) -> Self { + *self } } +impl, const N: usize> Copy for Residue {} +impl, const N: usize> crate::secret::Zeroable for Residue {} impl, const N: usize> Residue { /// Words already in Montgomery form and below p: a compile-time constant @@ -58,16 +60,13 @@ impl, const N: usize> Residue { } /// An integer x < 2^(64N) < 2p, reduced once. pub(crate) fn from_wide(x: &[u64; N]) -> Self { - let mut v = *x; - let reduced = Self(reduce(&v, 0, &M::P), PhantomData); - wipe64(&mut v); - reduced.mul(&Self(M::R2, PhantomData)) + Self(reduce(x, 0, &M::P), PhantomData).mul(&Self(M::R2, PhantomData)) } fn from_reduced(x: &[u64; N]) -> Self { Self(*x, PhantomData).mul(&Self(M::R2, PhantomData)) } /// The canonical integer this residue represents. - pub(crate) fn to_integer(&self) -> [u64; N] { + pub(crate) fn integer(&self) -> [u64; N] { let one = Self(one(), PhantomData); self.mul(&one).0 } @@ -77,9 +76,7 @@ impl, const N: usize> Residue { for i in 0..N { (v[i], carry) = carrying_add(self.0[i], b.0[i], carry); } - let result = Self(reduce(&v, u64::from(carry), &M::P), PhantomData); - wipe64(&mut v); - result + Self(reduce(&v, u64::from(carry), &M::P), PhantomData) } pub(crate) fn double(&self) -> Self { self.add(self) @@ -119,9 +116,7 @@ impl, const N: usize> Residue { // t < 2p after each step, so at most one of the two is set. high = u64::from(overflow) | u64::from(carry); } - let result = Self(reduce(&t, high, &M::P), PhantomData); - wipe64(&mut t); - result + Self(reduce(&t, high, &M::P), PhantomData) } pub(crate) fn square(&self) -> Self { self.mul(self) @@ -209,7 +204,6 @@ fn reduce(v: &[u64; N], high: u64, p: &[u64; N]) -> [u64; N] { for i in 0..N { out[i] = (v[i] & !m) | (d[i] & m); } - wipe64(&mut d); out } diff --git a/lib/crypto/src/nist.rs b/lib/crypto/src/nist.rs index 008b10abcc..76d67a2e39 100644 --- a/lib/crypto/src/nist.rs +++ b/lib/crypto/src/nist.rs @@ -10,7 +10,7 @@ use crate::{ Error, montgomery::{self, Modulus, Residue, below, from_be, to_be}, - secret::wipe64, + secret::{Workspace, wipe64}, }; /// A short Weierstrass curve y^2 = x^3 - 3x + b of prime order and cofactor @@ -85,6 +85,13 @@ pub(crate) struct Point, const N: usize> { y: Coord, z: Coord, } +impl, const N: usize> Clone for Point { + fn clone(&self) -> Self { + *self + } +} +impl, const N: usize> Copy for Point {} +impl, const N: usize> crate::secret::Zeroable for Point {} impl, const N: usize> Point { fn identity() -> Self { Self { @@ -143,14 +150,16 @@ impl, const N: usize> Point { let out = Self::select(&out, self, q.z.is_zero()); Self::select(&out, q, self.z.is_zero()) } + /// scalar * self on a fixed schedule. The accumulator is the operation's + /// secret state, erased when it returns; the result is the caller's. pub(crate) fn mul(&self, scalar: &Scalar) -> Self { - let mut r = Self::identity(); + let mut r = Workspace(Self::identity()); for i in (0..N * 64).rev() { - let doubled = r.double(); + let doubled = r.0.double(); let added = doubled.add(self); - r = Self::select(&doubled, &added, scalar.bit(i)) + r.0 = Self::select(&doubled, &added, scalar.bit(i)) } - r + r.0 } fn affine(&self) -> Result<([u64; N], [u64; N]), Error> { if self.z.is_zero() == 1 { @@ -159,8 +168,8 @@ impl, const N: usize> Point { let inverse = self.z.invert(); let inv2 = inverse.square(); Ok(( - self.x.mul(&inv2).to_integer(), - self.y.mul(&inv2.mul(&inverse)).to_integer(), + self.x.mul(&inv2).integer(), + self.y.mul(&inv2.mul(&inverse)).integer(), )) } /// The uncompressed SEC 1 encoding, 1 + 16N bytes. @@ -168,12 +177,10 @@ impl, const N: usize> Point { if out.len() != 1 + N * 16 { return Err(Error::Length); } - let (mut x, mut y) = self.affine()?; + let (x, y) = self.affine()?; out[0] = 4; to_be(&x, &mut out[1..1 + N * 8]); to_be(&y, &mut out[1 + N * 8..]); - wipe64(&mut x); - wipe64(&mut y); Ok(()) } /// An uncompressed SEC 1 point on the curve. @@ -207,13 +214,11 @@ impl, const N: usize> Point { pub(crate) fn agree(&self, scalar: &Scalar, output: &mut [u8]) { // Both curves have prime order and cofactor one; validated nonzero // scalars and finite peers cannot multiply to infinity. - let (mut x, mut y) = self + let (x, _) = self .mul(scalar) .affine() .expect("validated prime-order point and nonzero scalar"); to_be(&x, output); - wipe64(&mut x); - wipe64(&mut y); } /// ECDSA verification of a DER signature over an 8N-byte digest. All /// inputs are public. @@ -226,11 +231,11 @@ impl, const N: usize> Point { let s = Scalar::::from_bytes(&s[..N * 8]).map_err(|_| Error::InvalidSignature)?; let z = ModN::::from_wide(&from_be(digest)); let inverse = ModN::::from_wide(&s.0).invert(); - let u = Scalar(z.mul(&inverse).to_integer(), core::marker::PhantomData); - let v = Scalar(ModN::::from_wide(&r.0).mul(&inverse).to_integer(), core::marker::PhantomData); + let u = Scalar(z.mul(&inverse).integer(), core::marker::PhantomData); + let v = Scalar(ModN::::from_wide(&r.0).mul(&inverse).integer(), core::marker::PhantomData); let result = Self::base().mul(&u).add(&self.mul(&v)); let (x, _) = result.affine().map_err(|_| Error::InvalidSignature)?; - let x = ModN::::from_wide(&x).to_integer(); + let x = ModN::::from_wide(&x).integer(); if x == r.0 { Ok(()) } else { diff --git a/lib/crypto/src/secret.rs b/lib/crypto/src/secret.rs index 41ce17ef82..415b718773 100644 --- a/lib/crypto/src/secret.rs +++ b/lib/crypto/src/secret.rs @@ -1,4 +1,23 @@ -//! Owned secrets. Volatile stores erase this allocation, not prior moves. +//! Erasure of secret storage. The crate's erasure contract: +//! +//! - **Erased: owners.** A [`Secret`], and every type that owns a key, seed, +//! expanded key, key schedule, scalar or shared secret, erases its own +//! storage when it drops. +//! - **Erased: workspaces.** A secret operation's explicitly managed state, +//! such as the X25519 ladder's coordinates or a scalar multiplication's +//! accumulator, is a [`Workspace`]. It drops when the operation returns, on +//! every path out of it, early returns and `?` included. +//! - **How.** Volatile stores of zero over the value's own storage, then a +//! sequentially consistent compiler fence. They erase that location and no +//! other. +//! - **Not erased.** Arithmetic intermediates (field elements and residues +//! are `Copy` values); registers; spill slots and every other copy the +//! compiler makes, by-value moves and returns included; a moved value's +//! earlier location; finished stack frames; the caller's buffers and public +//! outputs. These remain until something overwrites them. +//! - **Panics.** An unwinding panic drops, and so erases, every owner and +//! workspace on the unwound frames. Under `panic = "abort"` nothing runs +//! after the panic. use core::sync::atomic::{Ordering, compiler_fence}; @@ -35,13 +54,13 @@ impl Zeroable for u64 {} impl Zeroable for u128 {} impl Zeroable for [T; N] {} -/// Erases a whole array, 16 bytes per volatile store. A single volatile store +/// Erases a whole value, 16 bytes per volatile store. A single volatile store /// of a large array is lowered through a stack copy, a `memcpy` call. -pub(crate) fn erase(array: &mut [T; N]) { - let bytes = core::mem::size_of::<[T; N]>(); - let base = (array as *mut [T; N]).cast::(); +pub(crate) fn erase(value: &mut T) { + let bytes = core::mem::size_of::(); + let base = (value as *mut T).cast::(); for i in 0..bytes / 16 { - // The 16 bytes lie inside the live, exclusively borrowed array, and + // The 16 bytes lie inside the live, exclusively borrowed value, and // zero bytes are a valid `T`. unsafe { core::ptr::write_volatile(base.add(16 * i).cast::<[u8; 16]>(), [0; 16]) }; } @@ -60,3 +79,13 @@ impl Drop for Secret { erase(&mut self.0); } } + +/// An operation's secret working state, erased when the operation ends, on +/// every return path. +pub(crate) struct Workspace(pub(crate) T); + +impl Drop for Workspace { + fn drop(&mut self) { + erase(&mut self.0); + } +} diff --git a/lib/crypto/src/test_arithmetic.rs b/lib/crypto/src/test_arithmetic.rs index 3e68a557ee..ab2fd61267 100644 --- a/lib/crypto/src/test_arithmetic.rs +++ b/lib/crypto/src/test_arithmetic.rs @@ -1,6 +1,6 @@ //! Integer-oracle checks of reductions, carry chains and representation changes. use crate::{ - field25519::F, + field25519::{Multiply, Tight}, montgomery::{Modulus, Residue}, nist::{P256Field, P256Order, P384Field, P384Order}, scalar25519::S, @@ -42,27 +42,60 @@ fn field25519_carries_and_noncanonical_inputs_match_integer_oracle() { let p = (BigUint::from(1u8) << 255) - 19u8; let mut values = fixtures(&p, 32); values.extend([p.clone(), &p + 1u8, (BigUint::from(1u8) << 255) - 1u8]); + let int = |x: &Tight| BigUint::from_bytes_le(&x.bytes()); + let int_loose = |x: &crate::field25519::Loose| BigUint::from_bytes_le(&x.carry().bytes()); for (i, a) in values.iter().enumerate() { - let a_field = F::from_bytes(&fixed(a)); - assert_eq!(BigUint::from_bytes_le(&a_field.bytes()), a % &p); + let x = Tight::from_bytes(&fixed(a)); + let a = a % &p; + assert_eq!(int(&x), a); + assert_eq!(int(&x.square()), (&a * &a) % &p); + assert_eq!(int(&x.mul_small(121665)), (&a * 121665u32) % &p); + assert_eq!(int_loose(&x.neg()), (&p - &a) % &p); + if a != BigUint::from(0u8) { + assert_eq!(int(&x.mul(&x.invert())), BigUint::from(1u8)); + let e = (&p - 5u8) / 8u8; + assert_eq!(int(&x.pow_p58()), a.modpow(&e, &p)); + } for b in [&values[(i * 17 + 3) % values.len()], &values[2]] { - let b_field = F::from_bytes(&fixed(b)); - assert_eq!( - BigUint::from_bytes_le(&a_field.add(&b_field).bytes()), - (a + b) % &p - ); - assert_eq!( - BigUint::from_bytes_le(&a_field.sub(&b_field).bytes()), - ((a % &p) + &p - (b % &p)) % &p - ); - assert_eq!( - BigUint::from_bytes_le(&a_field.mul(&b_field).bytes()), - (a * b) % &p - ); + let y = Tight::from_bytes(&fixed(b)); + let b = b % &p; + let sum = x.add(&y); + let difference = x.sub(&y); + assert_eq!(int_loose(&sum), (&a + &b) % &p); + assert_eq!(int_loose(&difference), (&a + &p - &b) % &p); + assert_eq!(int(&x.mul(&y)), (&a * &b) % &p); + // Loose operands, at their largest, into every multiplication. + assert_eq!(int(&sum.mul(&difference)), ((&a + &b) * (&a + &p - &b)) % &p); + assert_eq!(int(&sum.square()), ((&a + &b) * (&a + &b)) % &p); + assert_eq!(int(&difference.mul_small(121665)), ((&a + &p - &b) * 121665u32) % &p); } } } +/// The largest limbs each bound admits, through every operation: the bounds +/// the types state are enough for the carries. +#[test] +fn field25519_limb_bounds_hold_at_their_extremes() { + let p = (BigUint::from(1u8) << 255) - 19u8; + let tight = Tight::from_limbs([(1 << 51) - 1, (1 << 51) + (1 << 19) - 1, (1 << 51) - 1, (1 << 51) - 1, (1 << 51) - 1]); + let value = |l: &[u64; 5]| l.iter().enumerate().fold(BigUint::from(0u8), |v, (i, &x)| v + (BigUint::from(x) << (51 * i))); + let t = value(crate::field25519::Operand::limbs(&tight)) % &p; + let loose = tight.add(&tight); + let l = value(crate::field25519::Operand::limbs(&loose)) % &p; + assert_eq!(BigUint::from_bytes_le(&tight.bytes()), t); + assert_eq!(BigUint::from_bytes_le(&loose.mul(&loose).bytes()), (&l * &l) % &p); + assert_eq!(BigUint::from_bytes_le(&loose.square().bytes()), (&l * &l) % &p); + assert_eq!(BigUint::from_bytes_le(&tight.sub(&tight).carry().bytes()), BigUint::from(0u8)); + assert_eq!(BigUint::from_bytes_le(&loose.mul_small(121665).bytes()), (&l * 121665u32) % &p); + let widest = tight.sub(&Tight::ZERO); + let w = value(crate::field25519::Operand::limbs(&widest)) % &p; + assert_eq!(BigUint::from_bytes_le(&widest.mul(&loose).bytes()), (&w * &l) % &p); + assert_eq!(BigUint::from_bytes_le(&widest.square().bytes()), (&w * &w) % &p); + for limb in crate::field25519::Operand::limbs(&loose.mul(&loose)) { + assert!(*limb < (1 << 51) + (1 << 19)); + } +} + #[test] fn scalar25519_reduction_and_signing_arithmetic_match_integer_oracle() { let order = BigUint::parse_bytes( @@ -111,21 +144,21 @@ fn check_montgomery, const N: usize>() { }; let values = fixtures(&p, N * 8); assert!(Residue::::from_integer(&M::P).is_none()); - assert_eq!(integer(&Residue::::one().to_integer()), BigUint::from(1u8)); + assert_eq!(integer(&Residue::::one().integer()), BigUint::from(1u8)); let all_ones = (BigUint::from(1u8) << (64 * N)) - 1u8; - assert_eq!(integer(&Residue::::from_wide(&words(&all_ones)).to_integer()), &all_ones % &p); + assert_eq!(integer(&Residue::::from_wide(&words(&all_ones)).integer()), &all_ones % &p); for (i, a) in values.iter().enumerate() { let a_encoded = Residue::::from_integer(&words(a)).unwrap(); - assert_eq!(&integer(&a_encoded.to_integer()), a); + assert_eq!(&integer(&a_encoded.integer()), a); for b in [&values[(i * 17 + 3) % values.len()], &values[2]] { let b_encoded = Residue::::from_integer(&words(b)).unwrap(); let sum = a_encoded.add(&b_encoded); let difference = a_encoded.sub(&b_encoded); let product = a_encoded.mul(&b_encoded); - assert_eq!(integer(&sum.to_integer()), (a + b) % &p); - assert_eq!(integer(&difference.to_integer()), (a + &p - b) % &p); - assert_eq!(integer(&product.to_integer()), (a * b) % &p); - assert_eq!(integer(&a_encoded.square().to_integer()), (a * a) % &p); + assert_eq!(integer(&sum.integer()), (a + b) % &p); + assert_eq!(integer(&difference.integer()), (a + &p - b) % &p); + assert_eq!(integer(&product.integer()), (a * b) % &p); + assert_eq!(integer(&a_encoded.square().integer()), (a * a) % &p); } if (1..=4).contains(&i) { assert_eq!(a_encoded.mul(&a_encoded.invert()).eq(&Residue::one()), 1); diff --git a/lib/crypto/src/x25519.rs b/lib/crypto/src/x25519.rs index 803db1cdab..43c1704b20 100644 --- a/lib/crypto/src/x25519.rs +++ b/lib/crypto/src/x25519.rs @@ -3,7 +3,11 @@ //! RFC's noncanonical field encodings. All-zero shared secrets are refused. //! The caller supplies entropy; this module never queries the CPU for it. -use crate::{Error, Random, ct, field25519::F, secret::Secret}; +use crate::{ + Error, Random, ct, + field25519::{Multiply, Tight}, + secret::{Secret, Workspace}, +}; #[derive(Clone, Copy, Eq, PartialEq)] pub struct PublicKey(pub [u8; 32]); pub struct SecretKey(Secret<32>); @@ -38,38 +42,35 @@ impl SecretKey { } } } +/// The u-coordinate of scalar * u. The ladder's (x2, z2, x3, z3) is the +/// operation's secret state, erased when it returns. fn multiply(scalar: &[u8; 32], point: &[u8; 32]) -> Secret<32> { - let x1 = F::from_bytes(point); - let mut x2 = F::one(); - let mut z2 = F::zero(); - let mut x3 = F::from_bytes(point); - let mut z3 = F::one(); + let x1 = Tight::from_bytes(point); + let mut state = Workspace([Tight::ONE, Tight::ZERO, x1, Tight::ONE]); + let [x2, z2, x3, z3] = &mut state.0; let mut swap = 0; for i in (0..255).rev() { let bit = u64::from((scalar[i / 8] >> (i % 8)) & 1); - swap ^= bit; - let xx = F::select(&x2, &x3, swap); - x3 = F::select(&x3, &x2, swap); - x2 = xx; - let zz = F::select(&z2, &z3, swap); - z3 = F::select(&z3, &z2, swap); - z2 = zz; + let mask = ct::mask(swap ^ bit); + Tight::swap(x2, x3, mask); + Tight::swap(z2, z3, mask); swap = bit; - let a = x2.add(&z2); + let a = x2.add(z2); let aa = a.square(); - let b = x2.sub(&z2); + let b = x2.sub(z2); let bb = b.square(); let e = aa.sub(&bb); - let c = x3.add(&z3); - let d = x3.sub(&z3); + let c = x3.add(z3); + let d = x3.sub(z3); let da = d.mul(&a); let cb = c.mul(&b); - x3 = da.add(&cb).square(); - z3 = x1.mul(&da.sub(&cb).square()); - x2 = aa.mul(&bb); - z2 = e.mul(&aa.add(&F::small(121665).mul(&e))); + *x3 = da.add(&cb).square(); + *z3 = x1.mul(&da.sub(&cb).square()); + *x2 = aa.mul(&bb); + *z2 = e.mul(&aa.add(&e.mul_small(121665))); } - x2 = F::select(&x2, &x3, swap); - z2 = F::select(&z2, &z3, swap); + let mask = ct::mask(swap); + Tight::swap(x2, x3, mask); + Tight::swap(z2, z3, mask); Secret(x2.mul(&z2.invert()).bytes()) } From 41e89b9ec5e5239842e00ba397143e4d23a94cd5 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 22 Oct 2026 01:30:19 +0200 Subject: [PATCH 2/3] crypto: review round 1 of the 5x51 field and boundary erasure - Key cleanup is kept: each agreement erases its shared value before it becomes bytes. P-256/P-384's product point is a Workspace and its affine coordinates are wiped; X25519's x2 / z2 is a Workspace. - The Ed25519 and NIST scalar multiplications' accumulator Workspaces are gone: each held only the result the function returned. - secret.rs's contract states what is erased (owners and the hash, Keccak, SHA-1 and Poly1305 states; key-agreement results; workspaces, with what the ladder's actually covers), which types still erase per value (Ed25519 scalars, ML-KEM polynomials) and what is not erased. - Zeroable is an unsafe trait; each impl states why zero is valid. - square_times documents and debug-asserts k >= 1; the extreme-limb test is named for the reachable limbs it exercises. - The track's weakness record points to the contract and states the weakening and the owner's condition. Co-Authored-By: Claude Opus 5.5 --- ...in-the-tree-on-par-with-ring-and-secure.md | 9 +++- lib/crypto/src/ed25519.rs | 15 +++--- lib/crypto/src/field25519.rs | 6 ++- lib/crypto/src/montgomery.rs | 3 +- lib/crypto/src/nist.rs | 24 +++++---- lib/crypto/src/secret.rs | 49 ++++++++++++------- lib/crypto/src/test_arithmetic.rs | 6 +-- lib/crypto/src/x25519.rs | 7 +-- 8 files changed, 72 insertions(+), 47 deletions(-) diff --git a/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md b/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md index 4a8f4b2fbe..1a82f6729a 100644 --- a/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md +++ b/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md @@ -423,8 +423,13 @@ Not consumers: ## Weaknesses until the exits hold -- Owned secret storage is wiped on drop. Compiler copies, registers, spills - and caller-held bytes are not wiped. +- Erasure is what `lib/crypto/src/secret.rs`'s contract states: secret + owners, each key agreement's shared value and named workspaces are + erased. Since the parity work's 5x51 field (#992), 2^255 - 19 field + elements, NIST residues and curve points are not erased one by one. The + owner approved that on condition of a measured benefit (M1: 0.80–0.94 of + per-temporary erasure on the T14) and a reviewed contract. Compiler + copies, registers, spills and caller-held bytes are not wiped. - **Owner**: the orchestrator. **Exit**: stage 4 states what the shipped targets keep, and a consumer holding a long-lived key stores it under a wiping owner or a Key Locker handle. diff --git a/lib/crypto/src/ed25519.rs b/lib/crypto/src/ed25519.rs index 5bbff61944..69e1b8ea43 100644 --- a/lib/crypto/src/ed25519.rs +++ b/lib/crypto/src/ed25519.rs @@ -9,7 +9,6 @@ use crate::{ field25519::{Multiply, Tight}, hash::Sha512, scalar25519::S, - secret::Workspace, }; #[cfg(feature = "sign")] use crate::{Random, secret::Secret}; @@ -41,8 +40,8 @@ const B_T: Tight = Tight::from_bytes(&[ 0xa3, 0xdd, 0xb7, 0xa5, 0xb3, 0x8a, 0xde, 0x6d, 0xf5, 0x52, 0x51, 0x77, 0x80, 0x9f, 0xf0, 0x20, 0x7d, 0xe3, 0xab, 0x64, 0x8e, 0x4e, 0xea, 0x66, 0x65, 0x76, 0x8b, 0xd7, 0x0f, 0x5f, 0x87, 0x67, ]); -#[derive(Clone, Copy)] /// Extended coordinates (X : Y : Z : T) with x = X/Z, y = Y/Z, xy = T/Z. +#[derive(Clone, Copy)] struct Point { x: Tight, y: Tight, @@ -143,16 +142,15 @@ impl Point { t: Tight::select(&a.t, &b.t, bit), } } - /// scalar * self, on a fixed schedule. The accumulator is the operation's - /// secret state, erased when it returns; the result is the caller's. + /// scalar * self, on a fixed schedule. fn mul(&self, scalar: &[u8; 32]) -> Self { - let mut r = Workspace(Self::identity()); + let mut r = Self::identity(); for i in (0..256).rev() { - let double = r.0.double(); + let double = r.double(); let added = double.add(self); - r.0 = Self::select(&double, &added, u64::from((scalar[i / 8] >> (i % 8)) & 1)); + r = Self::select(&double, &added, u64::from((scalar[i / 8] >> (i % 8)) & 1)); } - r.0 + r } fn eq(&self, q: &Self) -> u64 { self.x.mul(&q.z).eq(&q.x.mul(&self.z)) & self.y.mul(&q.z).eq(&q.y.mul(&self.z)) @@ -162,7 +160,6 @@ impl Point { } } -impl crate::secret::Zeroable for Point {} pub struct PublicKey { encoded: [u8; 32], diff --git a/lib/crypto/src/field25519.rs b/lib/crypto/src/field25519.rs index 1c76671f6c..f4d10d708b 100644 --- a/lib/crypto/src/field25519.rs +++ b/lib/crypto/src/field25519.rs @@ -128,8 +128,9 @@ impl Tight { Self::ZERO.sub(self) } - /// self^(2^k). + /// self^(2^k), for k >= 1. pub(crate) fn square_times(&self, k: u32) -> Tight { + debug_assert!(k >= 1); let mut r = self.square(); for _ in 1..k { r = r.square(); @@ -298,4 +299,5 @@ fn square(a: &[u64; 5]) -> Tight { carry(v) } -impl crate::secret::Zeroable for Tight {} +// Five words: zero is a valid element. +unsafe impl crate::secret::Zeroable for Tight {} diff --git a/lib/crypto/src/montgomery.rs b/lib/crypto/src/montgomery.rs index b3f833272c..71934f8a4d 100644 --- a/lib/crypto/src/montgomery.rs +++ b/lib/crypto/src/montgomery.rs @@ -36,7 +36,8 @@ impl, const N: usize> Clone for Residue { } } impl, const N: usize> Copy for Residue {} -impl, const N: usize> crate::secret::Zeroable for Residue {} +// Words and a zero-sized marker: zero is a valid residue. +unsafe impl, const N: usize> crate::secret::Zeroable for Residue {} impl, const N: usize> Residue { /// Words already in Montgomery form and below p: a compile-time constant diff --git a/lib/crypto/src/nist.rs b/lib/crypto/src/nist.rs index 76d67a2e39..67a034c5fb 100644 --- a/lib/crypto/src/nist.rs +++ b/lib/crypto/src/nist.rs @@ -91,7 +91,8 @@ impl, const N: usize> Clone for Point { } } impl, const N: usize> Copy for Point {} -impl, const N: usize> crate::secret::Zeroable for Point {} +// Three residues: zero is a valid point representation. +unsafe impl, const N: usize> crate::secret::Zeroable for Point {} impl, const N: usize> Point { fn identity() -> Self { Self { @@ -150,16 +151,15 @@ impl, const N: usize> Point { let out = Self::select(&out, self, q.z.is_zero()); Self::select(&out, q, self.z.is_zero()) } - /// scalar * self on a fixed schedule. The accumulator is the operation's - /// secret state, erased when it returns; the result is the caller's. + /// scalar * self on a fixed schedule. pub(crate) fn mul(&self, scalar: &Scalar) -> Self { - let mut r = Workspace(Self::identity()); + let mut r = Self::identity(); for i in (0..N * 64).rev() { - let doubled = r.0.double(); + let doubled = r.double(); let added = doubled.add(self); - r.0 = Self::select(&doubled, &added, scalar.bit(i)) + r = Self::select(&doubled, &added, scalar.bit(i)) } - r.0 + r } fn affine(&self) -> Result<([u64; N], [u64; N]), Error> { if self.z.is_zero() == 1 { @@ -210,15 +210,19 @@ impl, const N: usize> Point { z: Residue::one(), }) } - /// The x-coordinate of scalar * self into `output`, 8N bytes. + /// The x-coordinate of scalar * self into `output`, 8N bytes. The product + /// and its affine coordinates are the shared secret's, erased here. pub(crate) fn agree(&self, scalar: &Scalar, output: &mut [u8]) { + let product = Workspace(self.mul(scalar)); // Both curves have prime order and cofactor one; validated nonzero // scalars and finite peers cannot multiply to infinity. - let (x, _) = self - .mul(scalar) + let (mut x, mut y) = product + .0 .affine() .expect("validated prime-order point and nonzero scalar"); to_be(&x, output); + wipe64(&mut x); + wipe64(&mut y); } /// ECDSA verification of a DER signature over an 8N-byte digest. All /// inputs are public. diff --git a/lib/crypto/src/secret.rs b/lib/crypto/src/secret.rs index 415b718773..65fa6e5101 100644 --- a/lib/crypto/src/secret.rs +++ b/lib/crypto/src/secret.rs @@ -2,19 +2,29 @@ //! //! - **Erased: owners.** A [`Secret`], and every type that owns a key, seed, //! expanded key, key schedule, scalar or shared secret, erases its own -//! storage when it drops. -//! - **Erased: workspaces.** A secret operation's explicitly managed state, -//! such as the X25519 ladder's coordinates or a scalar multiplication's -//! accumulator, is a [`Workspace`]. It drops when the operation returns, on -//! every path out of it, early returns and `?` included. +//! storage when it drops. So do the hash, HMAC, Keccak, SHA-1 and Poly1305 +//! states. +//! - **Erased: key-equivalent results.** Each key agreement erases its +//! shared value before it becomes the returned bytes: X25519's x2 / z2, and +//! P-256/P-384's product point with its affine coordinates. The bytes are +//! then the caller's `SharedSecret`, an owner. +//! - **Erased: workspaces.** A secret operation's explicitly managed state is +//! a [`Workspace`], dropped when the operation returns, on every path out of +//! it, early returns and `?` included. The X25519 ladder's (x2, z2, x3, z3) +//! is one. Its slot is what LLVM writes at the ladder's end; the copies the +//! loop works on in registers and other slots are not erased. +//! - **Erased per value, for now.** Ed25519 scalars (`scalar25519::S`, until +//! its reduction is rewritten) and ML-KEM polynomials (`mlkem::Poly`) still +//! erase every temporary on drop. //! - **How.** Volatile stores of zero over the value's own storage, then a //! sequentially consistent compiler fence. They erase that location and no //! other. -//! - **Not erased.** Arithmetic intermediates (field elements and residues -//! are `Copy` values); registers; spill slots and every other copy the -//! compiler makes, by-value moves and returns included; a moved value's -//! earlier location; finished stack frames; the caller's buffers and public -//! outputs. These remain until something overwrites them. +//! - **Not erased.** Other arithmetic intermediates: 2^255 - 19 field +//! elements, NIST residues and both curves' points are `Copy` values. +//! Registers; spill slots and every other copy the compiler makes, by-value +//! moves and returns included; a moved value's earlier location; finished +//! stack frames; the caller's buffers and public outputs. These remain +//! until something overwrites them. //! - **Panics.** An unwinding panic drops, and so erases, every owner and //! workspace on the unwound frames. Under `panic = "abort"` nothing runs //! after the panic. @@ -46,13 +56,18 @@ pub(crate) fn wipe64(words: &mut [u64]) { compiler_fence(Ordering::SeqCst); } -/// Element types whose all-zero bytes are a valid value. -pub(crate) trait Zeroable: Copy {} -impl Zeroable for u8 {} -impl Zeroable for u32 {} -impl Zeroable for u64 {} -impl Zeroable for u128 {} -impl Zeroable for [T; N] {} +/// Types whose all-zero bytes are a valid value. +/// +/// # Safety +/// Every bit pattern of zero bytes must be a valid `Self`: no references, +/// `NonZero` fields or enums without a zero discriminant. +pub(crate) unsafe trait Zeroable: Copy {} +// Integers and arrays of zeroable elements are valid at zero. +unsafe impl Zeroable for u8 {} +unsafe impl Zeroable for u32 {} +unsafe impl Zeroable for u64 {} +unsafe impl Zeroable for u128 {} +unsafe impl Zeroable for [T; N] {} /// Erases a whole value, 16 bytes per volatile store. A single volatile store /// of a large array is lowered through a stack copy, a `memcpy` call. diff --git a/lib/crypto/src/test_arithmetic.rs b/lib/crypto/src/test_arithmetic.rs index ab2fd61267..f21d0a970f 100644 --- a/lib/crypto/src/test_arithmetic.rs +++ b/lib/crypto/src/test_arithmetic.rs @@ -72,10 +72,10 @@ fn field25519_carries_and_noncanonical_inputs_match_integer_oracle() { } } -/// The largest limbs each bound admits, through every operation: the bounds -/// the types state are enough for the carries. +/// The largest limbs the operations reach, through every operation: the +/// bounds the types state are enough for the carries. #[test] -fn field25519_limb_bounds_hold_at_their_extremes() { +fn field25519_largest_reachable_limbs_stay_within_their_bounds() { let p = (BigUint::from(1u8) << 255) - 19u8; let tight = Tight::from_limbs([(1 << 51) - 1, (1 << 51) + (1 << 19) - 1, (1 << 51) - 1, (1 << 51) - 1, (1 << 51) - 1]); let value = |l: &[u64; 5]| l.iter().enumerate().fold(BigUint::from(0u8), |v, (i, &x)| v + (BigUint::from(x) << (51 * i))); diff --git a/lib/crypto/src/x25519.rs b/lib/crypto/src/x25519.rs index 43c1704b20..2141ce16a5 100644 --- a/lib/crypto/src/x25519.rs +++ b/lib/crypto/src/x25519.rs @@ -42,8 +42,8 @@ impl SecretKey { } } } -/// The u-coordinate of scalar * u. The ladder's (x2, z2, x3, z3) is the -/// operation's secret state, erased when it returns. +/// The u-coordinate of scalar * u. The ladder's (x2, z2, x3, z3) and the +/// shared value x2 / z2 are erased when it returns. fn multiply(scalar: &[u8; 32], point: &[u8; 32]) -> Secret<32> { let x1 = Tight::from_bytes(point); let mut state = Workspace([Tight::ONE, Tight::ZERO, x1, Tight::ONE]); @@ -72,5 +72,6 @@ fn multiply(scalar: &[u8; 32], point: &[u8; 32]) -> Secret<32> { let mask = ct::mask(swap); Tight::swap(x2, x3, mask); Tight::swap(z2, z3, mask); - Secret(x2.mul(&z2.invert()).bytes()) + let shared = Workspace(x2.mul(&z2.invert())); + Secret(shared.0.bytes()) } From d63355844374ca54171c813b8fcb04c776bfc9bf Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 22 Oct 2026 03:39:34 +0200 Subject: [PATCH 3/3] crypto: review round 2 of boundary erasure, the key-agreement wording - Each agreement writes its output into a Secret it owns: X25519's multiply writes into the caller's buffer, and NIST's affine writes into the caller's arrays, which agree holds in a Workspace. The returned coordinates' copies in agree's frame are gone. - The contract states the order (the values are erased once the output is written) and what remains: the copies of the output bytes that moving them into SharedSecret leaves in the finished frame, and the computation's arithmetic intermediates. - The workspace bullet keeps only what holds whatever the code generation: a workspace erases its own storage, not the copies the computation works on. - Zeroable's safety wording; the track record adds the AMD bench's M1. Co-Authored-By: Claude Opus 5.5 --- ...in-the-tree-on-par-with-ring-and-secure.md | 3 +- lib/crypto/src/nist.rs | 29 ++++++++++--------- lib/crypto/src/secret.rs | 17 ++++++----- lib/crypto/src/x25519.rs | 15 ++++++---- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md b/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md index 1a82f6729a..224501006c 100644 --- a/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md +++ b/issues/toyos-crypto-is-in-the-tree-on-par-with-ring-and-secure.md @@ -428,7 +428,8 @@ Not consumers: erased. Since the parity work's 5x51 field (#992), 2^255 - 19 field elements, NIST residues and curve points are not erased one by one. The owner approved that on condition of a measured benefit (M1: 0.80–0.94 of - per-temporary erasure on the T14) and a reviewed contract. Compiler + per-temporary erasure on the T14, 0.79–0.88 on the AMD bench) and a + reviewed contract. Compiler copies, registers, spills and caller-held bytes are not wiped. - **Owner**: the orchestrator. **Exit**: stage 4 states what the shipped targets keep, and a consumer holding a long-lived key stores it under a diff --git a/lib/crypto/src/nist.rs b/lib/crypto/src/nist.rs index 67a034c5fb..f068049d49 100644 --- a/lib/crypto/src/nist.rs +++ b/lib/crypto/src/nist.rs @@ -161,23 +161,24 @@ impl, const N: usize> Point { } r } - fn affine(&self) -> Result<([u64; N], [u64; N]), Error> { + /// The affine coordinates as integers, into the caller's arrays. + fn affine(&self, x: &mut [u64; N], y: &mut [u64; N]) -> Result<(), Error> { if self.z.is_zero() == 1 { return Err(Error::InvalidPoint); } let inverse = self.z.invert(); let inv2 = inverse.square(); - Ok(( - self.x.mul(&inv2).integer(), - self.y.mul(&inv2.mul(&inverse)).integer(), - )) + *x = self.x.mul(&inv2).integer(); + *y = self.y.mul(&inv2.mul(&inverse)).integer(); + Ok(()) } /// The uncompressed SEC 1 encoding, 1 + 16N bytes. pub(crate) fn encode(&self, out: &mut [u8]) -> Result<(), Error> { if out.len() != 1 + N * 16 { return Err(Error::Length); } - let (x, y) = self.affine()?; + let (mut x, mut y) = ([0; N], [0; N]); + self.affine(&mut x, &mut y)?; out[0] = 4; to_be(&x, &mut out[1..1 + N * 8]); to_be(&y, &mut out[1 + N * 8..]); @@ -211,18 +212,19 @@ impl, const N: usize> Point { }) } /// The x-coordinate of scalar * self into `output`, 8N bytes. The product - /// and its affine coordinates are the shared secret's, erased here. + /// and its affine coordinates are the shared secret's, erased here once + /// `output` is written. pub(crate) fn agree(&self, scalar: &Scalar, output: &mut [u8]) { let product = Workspace(self.mul(scalar)); + let mut coordinates = Workspace([[0; N]; 2]); + let [x, y] = &mut coordinates.0; // Both curves have prime order and cofactor one; validated nonzero // scalars and finite peers cannot multiply to infinity. - let (mut x, mut y) = product + product .0 - .affine() + .affine(x, y) .expect("validated prime-order point and nonzero scalar"); - to_be(&x, output); - wipe64(&mut x); - wipe64(&mut y); + to_be(x, output); } /// ECDSA verification of a DER signature over an 8N-byte digest. All /// inputs are public. @@ -238,7 +240,8 @@ impl, const N: usize> Point { let u = Scalar(z.mul(&inverse).integer(), core::marker::PhantomData); let v = Scalar(ModN::::from_wide(&r.0).mul(&inverse).integer(), core::marker::PhantomData); let result = Self::base().mul(&u).add(&self.mul(&v)); - let (x, _) = result.affine().map_err(|_| Error::InvalidSignature)?; + let (mut x, mut y) = ([0; N], [0; N]); + result.affine(&mut x, &mut y).map_err(|_| Error::InvalidSignature)?; let x = ModN::::from_wide(&x).integer(); if x == r.0 { Ok(()) diff --git a/lib/crypto/src/secret.rs b/lib/crypto/src/secret.rs index 65fa6e5101..c6b47e736c 100644 --- a/lib/crypto/src/secret.rs +++ b/lib/crypto/src/secret.rs @@ -4,15 +4,18 @@ //! expanded key, key schedule, scalar or shared secret, erases its own //! storage when it drops. So do the hash, HMAC, Keccak, SHA-1 and Poly1305 //! states. -//! - **Erased: key-equivalent results.** Each key agreement erases its -//! shared value before it becomes the returned bytes: X25519's x2 / z2, and -//! P-256/P-384's product point with its affine coordinates. The bytes are -//! then the caller's `SharedSecret`, an owner. +//! - **Erased: key-agreement values.** Each key agreement writes its output +//! bytes into a `Secret` it owns, then erases the values they came from: +//! X25519's x2 / z2, and P-256/P-384's product point and affine +//! coordinates, each held in a [`Workspace`]. The output `Secret` is moved +//! into the returned `SharedSecret`, an owner. Not erased: the copies of +//! the output bytes that move leaves in the agreement's finished frame, and +//! the computation's arithmetic intermediates (below). //! - **Erased: workspaces.** A secret operation's explicitly managed state is //! a [`Workspace`], dropped when the operation returns, on every path out of //! it, early returns and `?` included. The X25519 ladder's (x2, z2, x3, z3) -//! is one. Its slot is what LLVM writes at the ladder's end; the copies the -//! loop works on in registers and other slots are not erased. +//! is one. A workspace erases its own storage; the copies the computation +//! works on in registers and other slots are not erased. //! - **Erased per value, for now.** Ed25519 scalars (`scalar25519::S`, until //! its reduction is rewritten) and ML-KEM polynomials (`mlkem::Poly`) still //! erase every temporary on drop. @@ -59,7 +62,7 @@ pub(crate) fn wipe64(words: &mut [u64]) { /// Types whose all-zero bytes are a valid value. /// /// # Safety -/// Every bit pattern of zero bytes must be a valid `Self`: no references, +/// The all-zero bit pattern must be a valid `Self`: no references, /// `NonZero` fields or enums without a zero discriminant. pub(crate) unsafe trait Zeroable: Copy {} // Integers and arrays of zeroable elements are valid at zero. diff --git a/lib/crypto/src/x25519.rs b/lib/crypto/src/x25519.rs index 2141ce16a5..f49534858a 100644 --- a/lib/crypto/src/x25519.rs +++ b/lib/crypto/src/x25519.rs @@ -31,10 +31,13 @@ impl SecretKey { pub fn public_key(&self) -> PublicKey { let mut base = [0; 32]; base[0] = 9; - PublicKey(multiply(&self.0.0, &base).0) + let mut out = [0; 32]; + multiply(&self.0.0, &base, &mut out); + PublicKey(out) } pub fn agree(self, peer: &PublicKey) -> Result { - let result = multiply(&self.0.0, &peer.0); + let mut result = Secret([0; 32]); + multiply(&self.0.0, &peer.0, &mut result.0); if ct::equal(&result.0, &[0; 32]) { Err(Error::InvalidPoint) } else { @@ -42,9 +45,9 @@ impl SecretKey { } } } -/// The u-coordinate of scalar * u. The ladder's (x2, z2, x3, z3) and the -/// shared value x2 / z2 are erased when it returns. -fn multiply(scalar: &[u8; 32], point: &[u8; 32]) -> Secret<32> { +/// The u-coordinate of scalar * u into `out`. The ladder's (x2, z2, x3, z3) +/// and the shared value x2 / z2 are erased when it returns. +fn multiply(scalar: &[u8; 32], point: &[u8; 32], out: &mut [u8; 32]) { let x1 = Tight::from_bytes(point); let mut state = Workspace([Tight::ONE, Tight::ZERO, x1, Tight::ONE]); let [x2, z2, x3, z3] = &mut state.0; @@ -73,5 +76,5 @@ fn multiply(scalar: &[u8; 32], point: &[u8; 32]) -> Secret<32> { Tight::swap(x2, x3, mask); Tight::swap(z2, z3, mask); let shared = Workspace(x2.mul(&z2.invert())); - Secret(shared.0.bytes()) + *out = shared.0.bytes(); }