use crate::field::Fe;
use crate::field_ops::{dispatch, Field};
use crate::scalar;
use ic_core::ct::Choice;
#[cfg(feature = "std")]
mod basepoint_table;
#[cfg(all(
feature = "std",
target_arch = "x86_64",
not(any(ic_limb32, ic_ec_portable))
))]
mod fast;
#[cfg(all(
feature = "std",
target_arch = "x86_64",
not(any(ic_limb32, ic_ec_portable))
))]
mod vector;
use ic_core::traits::internal::Digest;
use ic_core::traits::{Algorithm, SelfTest, SignatureScheme};
use ic_core::{ensure, Result, Zeroize};
use ic_hash::Sha512;
#[cfg(not(feature = "std"))]
pub(crate) use self::EdField as Curve;
#[cfg(feature = "std")]
pub(crate) use basepoint_table::TableField as Curve;
#[cfg(test)]
const BASEPOINT_COMPRESSED: [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 D_LIMBS: [u64; 5] = [
929_955_233_495_203,
466_365_720_129_213,
1_662_059_464_998_953,
2_033_849_074_728_123,
1_442_794_654_840_575,
];
const D2_LIMBS: [u64; 5] = [
1_859_910_466_990_425,
932_731_440_258_426,
1_072_319_116_312_658,
1_815_898_335_770_999,
633_789_495_995_903,
];
const SQRT_M1_LIMBS: [u64; 5] = [
1_718_705_420_411_056,
234_908_883_556_509,
2_233_514_472_574_048,
2_117_202_627_021_982,
765_476_049_583_133,
];
pub(crate) trait EdField: Field + core::fmt::Debug + 'static {
const D: Self;
const D2: Self;
const SQRT_M1: Self;
const CHEAP_INVERSION: bool = false;
#[cfg(all(feature = "std", any(test, feature = "bench-internals")))]
fn to_portable(self) -> Fe {
Fe::from_bytes(&self.to_bytes())
}
#[cfg(all(feature = "std", any(test, feature = "bench-internals")))]
fn from_portable(x: &Fe) -> Self {
Self::from_bytes(&x.to_bytes())
}
}
impl EdField for Fe {
const D: Fe = Fe::from_limbs51(D_LIMBS);
const D2: Fe = Fe::from_limbs51(D2_LIMBS);
const SQRT_M1: Fe = Fe::from_limbs51(SQRT_M1_LIMBS);
#[cfg(all(feature = "std", any(test, feature = "bench-internals")))]
fn to_portable(self) -> Fe {
self
}
#[cfg(all(feature = "std", any(test, feature = "bench-internals")))]
fn from_portable(x: &Fe) -> Self {
*x
}
}
#[cfg(all(target_arch = "x86_64", not(any(ic_limb32, ic_ec_portable))))]
impl EdField for crate::fe64::Fe64 {
const D: Self = crate::fe64::Fe64::from_limbs51(D_LIMBS);
const D2: Self = crate::fe64::Fe64::from_limbs51(D2_LIMBS);
const SQRT_M1: Self = crate::fe64::Fe64::from_limbs51(SQRT_M1_LIMBS);
const CHEAP_INVERSION: bool = true;
}
#[derive(Clone, Copy, Debug)]
pub struct Point(Ext<Fe>);
impl Point {
pub const IDENTITY: Point = Point(Ext::IDENTITY);
pub fn add(&self, other: &Point) -> Point {
Point(self.0.add(&other.0))
}
pub fn double(&self) -> Point {
Point(self.0.double())
}
pub fn mul_scalar(&self, s: &[u8; 32]) -> Point {
Point(self.0.mul_scalar(s))
}
pub fn compress(&self) -> [u8; 32] {
self.0.compress()
}
pub fn decompress(bytes: &[u8; 32]) -> Option<Point> {
Ext::decompress(bytes).map(Point)
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Ext<F> {
x: F,
y: F,
z: F,
t: F,
}
#[derive(Clone, Copy)]
pub(crate) struct Completed<F> {
x: F,
y: F,
z: F,
t: F,
}
#[derive(Clone, Copy)]
pub(crate) struct Projective<F> {
x: F,
y: F,
z: F,
}
#[derive(Clone, Copy)]
pub(crate) struct Niels<F> {
ypx: F,
ymx: F,
z: F,
t2d: F,
}
#[cfg(feature = "std")]
#[derive(Clone, Copy)]
#[repr(C)]
pub(crate) struct AffineNiels<F> {
ypx: F,
ymx: F,
t2d: F,
}
#[cfg(any(not(feature = "std"), test))]
impl<F: EdField> Niels<F> {
const IDENTITY: Self = Niels {
ypx: F::ONE,
ymx: F::ONE,
z: F::ONE,
t2d: F::ZERO,
};
fn conditional_negate(&mut self, choice: Choice) {
let swapped_p = self.ymx;
let swapped_m = self.ypx;
let nt = self.t2d.neg();
F::cmov(&mut self.ypx, &swapped_p, choice);
F::cmov(&mut self.ymx, &swapped_m, choice);
F::cmov(&mut self.t2d, &nt, choice);
}
fn cmov(&mut self, other: &Niels<F>, choice: Choice) {
F::cmov(&mut self.ypx, &other.ypx, choice);
F::cmov(&mut self.ymx, &other.ymx, choice);
F::cmov(&mut self.z, &other.z, choice);
F::cmov(&mut self.t2d, &other.t2d, choice);
}
}
#[cfg(feature = "std")]
impl<F: EdField> AffineNiels<F> {
pub(crate) const IDENTITY: Self = AffineNiels {
ypx: F::ONE,
ymx: F::ONE,
t2d: F::ZERO,
};
pub(crate) fn conditional_negate(&mut self, bit: u64) {
let swapped_p = self.ymx;
let swapped_m = self.ypx;
let nt = self.t2d.neg();
F::cmov_bit(&mut self.ypx, &swapped_p, bit);
F::cmov_bit(&mut self.ymx, &swapped_m, bit);
F::cmov_bit(&mut self.t2d, &nt, bit);
}
pub(crate) fn cmov(&mut self, other: &AffineNiels<F>, bit: u64) {
F::cmov_bit(&mut self.ypx, &other.ypx, bit);
F::cmov_bit(&mut self.ymx, &other.ymx, bit);
F::cmov_bit(&mut self.t2d, &other.t2d, bit);
}
}
impl<F: EdField> Completed<F> {
fn to_projective(self) -> Projective<F> {
Projective {
x: self.x.mul(&self.t),
y: self.y.mul(&self.z),
z: self.z.mul(&self.t),
}
}
fn to_extended(self) -> Ext<F> {
Ext {
x: self.x.mul(&self.t),
y: self.y.mul(&self.z),
z: self.z.mul(&self.t),
t: self.x.mul(&self.y),
}
}
}
impl<F: EdField> Projective<F> {
fn to_extended_from_projective(self) -> Ext<F> {
Ext {
x: self.x.mul(&self.z),
y: self.y.mul(&self.z),
z: self.z.square(),
t: self.x.mul(&self.y),
}
}
fn double_projective(self) -> Projective<F> {
let xx = self.x.square();
let yy = self.y.square();
let zz2 = {
let t = self.z.square();
t.add(&t)
};
let xy_sq = self.x.add(&self.y).square();
let yy_plus_xx = yy.add(&xx);
let yy_minus_xx = yy.sub(&xx);
let cx = xy_sq.sub(&yy_plus_xx);
let cy = yy_plus_xx;
let cz = yy_minus_xx;
let ct = zz2.sub(&yy_minus_xx);
Projective {
x: cx.mul(&ct),
y: cy.mul(&cz),
z: cz.mul(&ct),
}
}
fn double(&self) -> Completed<F> {
let xx = self.x.square();
let yy = self.y.square();
let zz2 = {
let t = self.z.square();
t.add(&t)
};
let xy_sq = self.x.add(&self.y).square();
let yy_plus_xx = yy.add(&xx);
let yy_minus_xx = yy.sub(&xx);
Completed {
x: xy_sq.sub(&yy_plus_xx),
y: yy_plus_xx,
z: yy_minus_xx,
t: zz2.sub(&yy_minus_xx),
}
}
}
impl<F: EdField> Ext<F> {
pub(crate) fn from_affine(x: F, y: F) -> Ext<F> {
Ext {
x,
y,
z: F::ONE,
t: x.mul(&y),
}
}
#[cfg(all(feature = "std", any(test, feature = "bench-internals")))]
pub(crate) fn convert<G: EdField>(&self) -> Ext<G> {
let c = |v: &F| G::from_portable(&v.to_portable());
Ext {
x: c(&self.x),
y: c(&self.y),
z: c(&self.z),
t: c(&self.t),
}
}
pub const IDENTITY: Self = Ext {
x: F::ZERO,
y: F::ONE,
z: F::ONE,
t: F::ZERO,
};
pub fn add(&self, other: &Ext<F>) -> Ext<F> {
let a = self.y.sub(&self.x).mul(&other.y.sub(&other.x));
let b = self.y.add(&self.x).mul(&other.y.add(&other.x));
let c = self.t.mul(&F::D2).mul(&other.t);
let d = self.z.mul(&other.z);
let d = d.add(&d);
let e = b.sub(&a);
let f = d.sub(&c);
let g = d.add(&c);
let h = b.add(&a);
Ext {
x: e.mul(&f),
y: g.mul(&h),
t: e.mul(&h),
z: f.mul(&g),
}
}
pub fn double(&self) -> Ext<F> {
let aa = self.x.square();
let bb = self.y.square();
let c = self.z.square();
let c = c.add(&c);
let d = aa.neg();
let xy = self.x.add(&self.y);
let e = xy.square().sub(&aa).sub(&bb);
let g = d.add(&bb);
let f = g.sub(&c);
let h = d.sub(&bb);
Ext {
x: e.mul(&f),
y: g.mul(&h),
t: e.mul(&h),
z: f.mul(&g),
}
}
fn to_projective(self) -> Projective<F> {
Projective {
x: self.x,
y: self.y,
z: self.z,
}
}
fn to_niels(self) -> Niels<F> {
Niels {
ypx: self.y.add(&self.x),
ymx: self.y.sub(&self.x),
z: self.z,
t2d: self.t.mul(&F::D2),
}
}
fn add_niels(&self, other: &Niels<F>) -> Completed<F> {
let pp = self.y.add(&self.x).mul(&other.ypx);
let mm = self.y.sub(&self.x).mul(&other.ymx);
let tt2d = self.t.mul(&other.t2d);
let zz = self.z.mul(&other.z);
let zz2 = zz.add(&zz);
Completed {
x: pp.sub(&mm),
y: pp.add(&mm),
z: zz2.add(&tt2d),
t: zz2.sub(&tt2d),
}
}
fn sub_niels(&self, other: &Niels<F>) -> Completed<F> {
let pp = self.y.add(&self.x).mul(&other.ymx);
let mm = self.y.sub(&self.x).mul(&other.ypx);
let tt2d = self.t.mul(&other.t2d);
let zz = self.z.mul(&other.z);
let zz2 = zz.add(&zz);
Completed {
x: pp.sub(&mm),
y: pp.add(&mm),
z: zz2.sub(&tt2d),
t: zz2.add(&tt2d),
}
}
#[cfg(feature = "std")]
pub(crate) fn to_affine_niels(self) -> AffineNiels<F> {
let z_inv = self.z.invert();
let x = self.x.mul(&z_inv);
let y = self.y.mul(&z_inv);
AffineNiels {
ypx: y.add(&x),
ymx: y.sub(&x),
t2d: x.mul(&y).mul(&F::D2),
}
}
#[cfg(feature = "std")]
pub(crate) fn add_affine_niels(&self, other: &AffineNiels<F>) -> Completed<F> {
let pp = self.y.add(&self.x).mul(&other.ypx);
let mm = self.y.sub(&self.x).mul(&other.ymx);
let tt2d = self.t.mul(&other.t2d);
let zz2 = self.z.add(&self.z);
Completed {
x: pp.sub(&mm),
y: pp.add(&mm),
z: zz2.add(&tt2d),
t: zz2.sub(&tt2d),
}
}
#[cfg(feature = "std")]
pub(crate) fn sub_affine_niels(&self, other: &AffineNiels<F>) -> Completed<F> {
let pp = self.y.add(&self.x).mul(&other.ymx);
let mm = self.y.sub(&self.x).mul(&other.ypx);
let tt2d = self.t.mul(&other.t2d);
let zz2 = self.z.add(&self.z);
Completed {
x: pp.sub(&mm),
y: pp.add(&mm),
z: zz2.sub(&tt2d),
t: zz2.add(&tt2d),
}
}
fn cmov(&mut self, other: &Ext<F>, choice: Choice) {
F::cmov(&mut self.x, &other.x, choice);
F::cmov(&mut self.y, &other.y, choice);
F::cmov(&mut self.z, &other.z, choice);
F::cmov(&mut self.t, &other.t, choice);
}
pub fn mul_scalar(&self, s: &[u8; 32]) -> Ext<F> {
let mut acc = Ext::IDENTITY;
for i in (0..256).rev() {
acc = acc.double();
let sum = acc.add(self);
let bit = Choice::from_u8((s[i / 8] >> (i % 8)) & 1);
acc.cmov(&sum, bit);
}
acc
}
fn negate(&self) -> Ext<F> {
Ext {
x: self.x.neg(),
y: self.y,
z: self.z,
t: self.t.neg(),
}
}
fn has_small_order(&self) -> bool {
self.double()
.double()
.double()
.eq_projective(&Ext::<F>::IDENTITY)
}
fn eq_projective(&self, other: &Ext<F>) -> bool {
self.x.mul(&other.z).to_bytes() == other.x.mul(&self.z).to_bytes()
&& self.y.mul(&other.z).to_bytes() == other.y.mul(&self.z).to_bytes()
}
pub fn compress(&self) -> [u8; 32] {
let z_inv = self.z.invert();
let x = self.x.mul(&z_inv);
let y = self.y.mul(&z_inv);
let mut out = y.to_bytes();
out[31] |= x.is_negative().unwrap_u8() << 7;
out
}
pub fn decompress(bytes: &[u8; 32]) -> Option<Ext<F>> {
let start = Decompression::<F>::start(bytes)?;
let pow = start.uv7.pow22523();
start.finish(&pow)
}
fn decompress_pair(a: &[u8; 32], b: &[u8; 32]) -> (Option<Ext<F>>, Option<Ext<F>>) {
match (Decompression::<F>::start(a), Decompression::<F>::start(b)) {
(Some(sa), Some(sb)) => {
let (pa, pb) = F::pow22523_pair(&sa.uv7, &sb.uv7);
(sa.finish(&pa), sb.finish(&pb))
}
(sa, sb) => (
sa.and_then(|s| s.finish(&s.uv7.pow22523())),
sb.and_then(|s| s.finish(&s.uv7.pow22523())),
),
}
}
}
struct Decompression<F> {
y: F,
u: F,
v: F,
uv3: F,
uv7: F,
sign: Choice,
}
impl<F: EdField> Decompression<F> {
fn start(bytes: &[u8; 32]) -> Option<Self> {
let sign = Choice::from_u8(bytes[31] >> 7);
let mut y_bytes = *bytes;
y_bytes[31] &= 0x7f;
let y = F::from_bytes(&y_bytes);
if y.to_bytes() != y_bytes {
return None;
}
let y2 = y.square();
let u = y2.sub(&F::ONE);
let v = y2.mul(&F::D).add(&F::ONE);
let v3 = v.square().mul(&v);
let v7 = v3.square().mul(&v);
Some(Self {
y,
u,
v,
uv3: u.mul(&v3),
uv7: u.mul(&v7),
sign,
})
}
fn finish(&self, pow: &F) -> Option<Ext<F>> {
let (y, u, v, sign) = (self.y, self.u, self.v, self.sign);
let mut x = self.uv3.mul(pow);
let check = v.mul(&x.square());
let correct = check.ct_eq(&u);
let flipped = check.ct_eq(&u.neg());
if !bool::from(correct.or(flipped)) {
return None;
}
let alt = x.mul(&F::SQRT_M1);
F::cmov(&mut x, &alt, flipped.and(correct.not()));
if bool::from(x.is_zero()) && bool::from(sign) {
return None;
}
let neg = x.neg();
let wrong_sign = Choice::from_u8(x.is_negative().unwrap_u8() ^ sign.unwrap_u8());
F::cmov(&mut x, &neg, wrong_sign);
Some(Ext {
x,
y,
z: F::ONE,
t: x.mul(&y),
})
}
}
#[cfg(feature = "std")]
pub(crate) fn prepare_tables() {
fn prepare<F: Curve>() {
basepoint_table::prepare::<F>();
}
dispatch!(prepare());
#[cfg(all(target_arch = "x86_64", not(any(ic_limb32, ic_ec_portable))))]
if crate::fe64::available()
&& crate::f51x4::Ifma::get().is_none()
&& crate::fe64::edwards::Avx2::get().is_some()
{
fast::prepare_wide();
}
}
fn mul_basepoint<F: Curve>(scalar: &[u8; 32]) -> Ext<F> {
#[cfg(feature = "std")]
{
basepoint_table::mul::<F>(scalar)
}
#[cfg(not(feature = "std"))]
{
mul_scalar_windowed(&basepoint::<F>(), scalar)
}
}
fn mul_basepoint_compressed(scalar: &[u8; 32]) -> [u8; 32] {
fn on<F: Curve>(scalar: &[u8; 32]) -> [u8; 32] {
mul_basepoint::<F>(scalar).compress()
}
dispatch!(on(scalar))
}
#[cfg(feature = "bench-internals")]
#[doc(hidden)]
pub fn double_scalar_mul_vartime_for_bench(a: &Point, k: &[u8; 32], s: &[u8; 32]) -> Point {
fn on<F: Curve>(a: &Ext<Fe>, k: &[u8; 32], s: &[u8; 32]) -> Point {
Point(double_scalar_mul_vartime(&a.convert::<F>(), k, s).convert::<Fe>())
}
dispatch!(on(&a.0, k, s))
}
#[cfg(any(not(feature = "std"), test))]
fn mul_scalar_windowed<F: EdField>(p: &Ext<F>, scalar: &[u8; 32]) -> Ext<F> {
let mut multiples = [*p; 8];
for i in 1..8 {
multiples[i] = multiples[i - 1].add(p);
}
let table: [Niels<F>; 8] = core::array::from_fn(|i| multiples[i].to_niels());
let select = |digit: i8| -> Niels<F> {
let negative = Choice::from_u8((digit as u8) >> 7);
let magnitude = ((digit as i16 ^ (digit as i16 >> 7)) - (digit as i16 >> 7)) as u8;
let mut out = Niels::IDENTITY;
for (i, entry) in table.iter().enumerate() {
out.cmov(entry, Choice::from_u8(u8::from(magnitude == (i as u8 + 1))));
}
out.conditional_negate(negative);
out
};
let digits = signed_digits(scalar);
let mut acc = Ext::IDENTITY.add_niels(&select(digits[63])).to_extended();
for i in (0..63).rev() {
let mut q = acc.to_projective();
for _ in 0..3 {
q = q.double_projective();
}
acc = q.double().to_extended();
acc = acc.add_niels(&select(digits[i])).to_extended();
}
acc
}
fn odd_multiples<F: EdField>(p: &Ext<F>) -> [Niels<F>; 8] {
let twice = p.double();
let mut odd = [*p; 8];
for i in 1..8 {
odd[i] = odd[i - 1].add(&twice);
}
core::array::from_fn(|i| odd[i].to_niels())
}
#[cfg(all(target_arch = "x86_64", not(any(ic_limb32, ic_ec_portable))))]
pub(crate) const DIGITS32: usize = 52;
#[cfg(all(target_arch = "x86_64", not(any(ic_limb32, ic_ec_portable))))]
#[cfg_attr(not(feature = "std"), allow(dead_code))]
pub(crate) fn signed_digits32(scalar: &[u8; 32]) -> [i8; DIGITS32] {
#[inline(always)]
fn field(scalar: &[u8; 32], i: usize) -> i8 {
let bit = 5 * i;
let lo = u16::from(scalar[bit / 8]);
let hi = u16::from(*scalar.get(bit / 8 + 1).unwrap_or(&0));
(((lo | (hi << 8)) >> (bit % 8)) & 31) as i8
}
macro_rules! fields {
($($i:literal)*) => { [$(field(scalar, $i)),*] };
}
let mut d: [i8; DIGITS32] = fields!(
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
);
macro_rules! carries {
($($i:literal)*) => {$({
let carry = (d[$i] + 16) >> 5;
d[$i] -= carry << 5;
d[$i + 1] += carry;
})*};
}
carries!(
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
);
d
}
fn signed_digits(scalar: &[u8; 32]) -> [i8; 64] {
debug_assert!(
scalar[31] <= 127,
"the top digit can only absorb the final carry for scalars below 2^255"
);
let mut nibbles = [0i8; 64];
for (i, byte) in scalar.iter().enumerate() {
nibbles[i * 2] = (byte & 0x0f) as i8;
nibbles[i * 2 + 1] = (byte >> 4) as i8;
}
for i in 0..63 {
let carry = (nibbles[i] + 8) >> 4;
nibbles[i] -= carry << 4;
nibbles[i + 1] += carry;
}
nibbles
}
#[cfg(feature = "std")]
fn double_scalar_mul_vartime<F: Curve>(a: &Ext<F>, k: &[u8; 32], s: &[u8; 32]) -> Ext<F> {
F::double_mul(a, k, s)
}
#[cfg(feature = "std")]
fn double_scalar_mul_vartime_generic<F: Curve>(a: &Ext<F>, k: &[u8; 32], s: &[u8; 32]) -> Ext<F> {
basepoint_table::prepare::<F>();
shared_doublings(a, k, &wnaf(s, 8), |e, digit| {
let n = basepoint_table::odd_multiple::<F>((digit.unsigned_abs() as usize) / 2);
if digit > 0 {
e.add_affine_niels(n)
} else {
e.sub_affine_niels(n)
}
})
}
#[cfg(any(not(feature = "std"), test))]
fn double_scalar_mul_vartime_no_table<F: EdField>(
a: &Ext<F>,
k: &[u8; 32],
s: &[u8; 32],
) -> Ext<F> {
let odd_b = odd_multiples(&basepoint::<F>());
shared_doublings(a, k, &wnaf(s, 5), |e, digit| {
let n = &odd_b[(digit.unsigned_abs() as usize) / 2];
if digit > 0 {
e.add_niels(n)
} else {
e.sub_niels(n)
}
})
}
#[inline(always)]
fn shared_doublings<F: EdField>(
a: &Ext<F>,
k: &[u8; 32],
naf_b: &[i8; 258],
add_b: impl Fn(&Ext<F>, i8) -> Completed<F>,
) -> Ext<F> {
let odd_a = odd_multiples(a);
let naf_a = wnaf(k, 5);
let mut i = 257;
while i > 0 && naf_a[i] == 0 && naf_b[i] == 0 {
i -= 1;
}
let mut acc = Ext::IDENTITY.to_projective();
loop {
if naf_a[i] == 0 && naf_b[i] == 0 {
acc = acc.double_projective();
if i == 0 {
return acc.to_extended_from_projective();
}
i -= 1;
continue;
}
let mut t = acc.double();
if naf_a[i] != 0 {
let e = t.to_extended();
let n = &odd_a[(naf_a[i].unsigned_abs() as usize) / 2];
t = if naf_a[i] > 0 {
e.add_niels(n)
} else {
e.sub_niels(n)
};
}
if naf_b[i] != 0 {
t = add_b(&t.to_extended(), naf_b[i]);
}
if i == 0 {
return t.to_extended();
}
acc = t.to_projective();
i -= 1;
}
}
#[cfg(feature = "bench-internals")]
#[doc(hidden)]
pub fn mul_basepoint_for_bench(scalar: &[u8; 32]) -> Point {
fn on<F: Curve>(scalar: &[u8; 32]) -> Point {
Point(mul_basepoint::<F>(scalar).convert::<Fe>())
}
dispatch!(on(scalar))
}
fn basepoint<F: EdField>() -> Ext<F> {
Ext {
x: F::from_limbs51(BASEPOINT_X),
y: F::from_limbs51(BASEPOINT_Y),
z: F::ONE,
t: F::from_limbs51(BASEPOINT_T),
}
}
const BASEPOINT_X: [u64; 5] = [
1_738_742_601_995_546,
1_146_398_526_822_698,
2_070_867_633_025_821,
562_264_141_797_630,
587_772_402_128_613,
];
const BASEPOINT_Y: [u64; 5] = [
1_801_439_850_948_184,
1_351_079_888_211_148,
450_359_962_737_049,
900_719_925_474_099,
1_801_439_850_948_198,
];
const BASEPOINT_T: [u64; 5] = [
1_841_354_044_333_475,
16_398_895_984_059,
755_974_180_946_558,
900_171_276_175_154,
1_821_297_809_914_039,
];
pub struct Ed25519;
impl Algorithm for Ed25519 {
const ID: &'static str = "ed25519";
const NAME: &'static str = "Ed25519";
}
fn expand_seed(seed: &[u8]) -> ([u8; 32], [u8; 32]) {
let h = Sha512::digest(seed);
let mut a = [0u8; 32];
let mut prefix = [0u8; 32];
a.copy_from_slice(&h.as_ref()[..32]);
prefix.copy_from_slice(&h.as_ref()[32..]);
a[0] &= 248;
a[31] &= 127;
a[31] |= 64;
(a, prefix)
}
fn wnaf(scalar: &[u8; 32], w: u32) -> [i8; 258] {
debug_assert!((2..=8).contains(&w), "window width out of range");
let half = 1i64 << (w - 1);
let full = 1i64 << w;
let mask = (full - 1) as u64;
let mut naf = [0i8; 258];
let mut k = [0u64; 5];
for (i, limb) in k.iter_mut().take(4).enumerate() {
let mut b = [0u8; 8];
b.copy_from_slice(&scalar[i * 8..i * 8 + 8]);
*limb = u64::from_le_bytes(b);
}
let mut i = 0;
while k.iter().any(|&x| x != 0) {
if k[0] & 1 == 1 {
let mut d = (k[0] & mask) as i64;
if d >= half {
d -= full;
}
naf[i] = d as i8;
if d > 0 {
sub_u64(&mut k, d as u64);
} else {
add_u64(&mut k, d.unsigned_abs());
}
}
shr1(&mut k);
i += 1;
}
naf
}
fn sub_u64(k: &mut [u64; 5], v: u64) {
let (d, mut borrow) = k[0].overflowing_sub(v);
k[0] = d;
for limb in k.iter_mut().skip(1) {
if !borrow {
break;
}
let (d, b) = limb.overflowing_sub(1);
*limb = d;
borrow = b;
}
}
fn add_u64(k: &mut [u64; 5], v: u64) {
let (d, mut carry) = k[0].overflowing_add(v);
k[0] = d;
for limb in k.iter_mut().skip(1) {
if !carry {
break;
}
let (d, c) = limb.overflowing_add(1);
*limb = d;
carry = c;
}
}
fn shr1(k: &mut [u64; 5]) {
for i in 0..4 {
k[i] = (k[i] >> 1) | (k[i + 1] << 63);
}
k[4] >>= 1;
}
fn hash_to_scalar(parts: &[&[u8]]) -> [u8; 32] {
let mut h = Sha512::new();
for p in parts {
h.update(p);
}
let digest = h.finalize();
let mut wide = [0u8; 64];
wide.copy_from_slice(digest.as_ref());
scalar::reduce_wide(&wide)
}
pub struct Ed25519Key {
scalar: [u8; 32],
prefix: [u8; 32],
public: [u8; 32],
}
impl Drop for Ed25519Key {
fn drop(&mut self) {
self.scalar.zeroize();
self.prefix.zeroize();
}
}
impl Ed25519Key {
pub fn from_seed(seed: &[u8]) -> Result<Self> {
ic_core::module::operational()?;
ensure!(seed.len() == 32, InvalidLength, "ed25519 seed");
let (scalar, prefix) = expand_seed(seed);
let public = mul_basepoint_compressed(&scalar);
Ok(Self {
scalar,
prefix,
public,
})
}
pub fn public_key(&self) -> &[u8; 32] {
&self.public
}
pub fn sign(&self, message: &[u8], signature: &mut [u8]) -> Result<()> {
ic_core::module::operational()?;
ensure!(
signature.len() == 64,
InvalidLength,
"ed25519 signature buffer"
);
let mut r = hash_to_scalar(&[&self.prefix, message]);
let big_r = mul_basepoint_compressed(&r);
let k = hash_to_scalar(&[&big_r, &self.public, message]);
let s = scalar::mul_add(&k, &self.scalar, &r);
signature[..32].copy_from_slice(&big_r);
signature[32..].copy_from_slice(&s);
r.zeroize();
Ok(())
}
}
impl SignatureScheme for Ed25519 {
const PRIVATE_KEY_LEN: usize = 32;
const PUBLIC_KEY_LEN: usize = 32;
const SIGNATURE_LEN: usize = 64;
fn public_key(private_key: &[u8], out: &mut [u8]) -> Result<()> {
ic_core::module::operational()?;
ensure!(private_key.len() == 32, InvalidLength, "ed25519 seed");
ensure!(out.len() == 32, InvalidLength, "ed25519 public key buffer");
let (mut a, mut prefix) = expand_seed(private_key);
out.copy_from_slice(&mul_basepoint_compressed(&a));
a.zeroize();
prefix.zeroize();
Ok(())
}
fn sign(private_key: &[u8], message: &[u8], signature: &mut [u8]) -> Result<()> {
ic_core::module::operational()?;
ensure!(private_key.len() == 32, InvalidLength, "ed25519 seed");
ensure!(
signature.len() == 64,
InvalidLength,
"ed25519 signature buffer"
);
Ed25519Key::from_seed(private_key)?.sign(message, signature)
}
fn verify(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<()> {
ic_core::module::operational()?;
if public_key.len() == 32 && signature.len() == 64 {
let mut pk = [0u8; 32];
pk.copy_from_slice(public_key);
let mut big_r = [0u8; 32];
big_r.copy_from_slice(&signature[..32]);
let mut s = [0u8; 32];
s.copy_from_slice(&signature[32..]);
if scalar::is_canonical(&s) {
let k = hash_to_scalar(&[&big_r, &pk, message]);
return match dispatch!(check_one_shot(&pk, &big_r, &k, &s)) {
OneShot::Key(KeyDecode::Malformed) => Err(ic_core::err!(
MalformedEncoding,
"ed25519 public key is not on the curve or not canonical"
)),
OneShot::Key(_) => Err(ic_core::err!(
InvalidParameter,
"ed25519 public key has small order"
)),
OneShot::Signature(c) => c.into_result(),
};
}
}
Ed25519VerifyKey::from_bytes(public_key)?.verify(message, signature)
}
}
pub struct Ed25519VerifyKey {
compressed: [u8; 32],
neg_a: ([u8; 32], [u8; 32]),
}
enum KeyDecode {
Malformed,
SmallOrder,
Negated([u8; 32], [u8; 32]),
}
fn decode_key<F: Curve>(bytes: &[u8; 32]) -> KeyDecode {
match Ext::<F>::decompress(bytes) {
None => KeyDecode::Malformed,
Some(a) if a.has_small_order() => KeyDecode::SmallOrder,
Some(a) => {
let n = a.negate();
KeyDecode::Negated(n.x.to_bytes(), n.y.to_bytes())
}
}
}
#[derive(PartialEq, Eq, Debug)]
enum Check {
RMalformed,
RSmallOrder,
Mismatch,
Holds,
}
impl Check {
fn into_result(self) -> Result<()> {
match self {
Check::RMalformed => Err(ic_core::err!(
MalformedEncoding,
"ed25519 signature R is not on the curve or not canonical"
)),
Check::RSmallOrder => Err(ic_core::err!(
MalformedEncoding,
"ed25519 signature R has small order"
)),
Check::Mismatch => Err(ic_core::err!(AuthenticationFailed, "ed25519")),
Check::Holds => Ok(()),
}
}
}
enum OneShot {
Key(KeyDecode),
Signature(Check),
}
fn check_one_shot<F: Curve>(
pk: &[u8; 32],
big_r: &[u8; 32],
k: &[u8; 32],
s: &[u8; 32],
) -> OneShot {
let (a, r) = if F::CHEAP_INVERSION {
(Ext::<F>::decompress(pk), None)
} else {
let (a, r) = Ext::<F>::decompress_pair(pk, big_r);
(a, Some(r))
};
let a = match a {
None => return OneShot::Key(KeyDecode::Malformed),
Some(a) if a.has_small_order() => return OneShot::Key(KeyDecode::SmallOrder),
Some(a) => a,
};
OneShot::Signature(match r {
None => verdict(&a.negate(), big_r, k, s),
Some(r) => verdict_decoded(&a.negate(), r, k, s),
})
}
fn verdict_decoded<F: Curve>(
neg_a: &Ext<F>,
r: Option<Ext<F>>,
k: &[u8; 32],
s: &[u8; 32],
) -> Check {
match r {
None => Check::RMalformed,
Some(r) if r.has_small_order() => Check::RSmallOrder,
Some(r) if left_side(neg_a, k, s).eq_projective(&r) => Check::Holds,
Some(_) => Check::Mismatch,
}
}
fn left_side<F: Curve>(neg_a: &Ext<F>, k: &[u8; 32], s: &[u8; 32]) -> Ext<F> {
#[cfg(feature = "std")]
let lhs = double_scalar_mul_vartime(neg_a, k, s);
#[cfg(not(feature = "std"))]
let lhs = double_scalar_mul_vartime_no_table(neg_a, k, s);
lhs
}
fn check_equation<F: Curve>(
neg_a: &([u8; 32], [u8; 32]),
big_r: &[u8; 32],
k: &[u8; 32],
s: &[u8; 32],
) -> Check {
let a = Ext::from_affine(F::from_bytes(&neg_a.0), F::from_bytes(&neg_a.1));
verdict(&a, big_r, k, s)
}
fn verdict<F: Curve>(neg_a: &Ext<F>, big_r: &[u8; 32], k: &[u8; 32], s: &[u8; 32]) -> Check {
let lhs = left_side(neg_a, k, s);
if lhs.compress() == *big_r {
if lhs.has_small_order() {
Check::RSmallOrder
} else {
Check::Holds
}
} else {
match Ext::<F>::decompress(big_r) {
None => Check::RMalformed,
Some(r) if r.has_small_order() => Check::RSmallOrder,
Some(_) => Check::Mismatch,
}
}
}
impl Ed25519VerifyKey {
pub fn from_bytes(public_key: &[u8]) -> Result<Self> {
ensure!(public_key.len() == 32, InvalidLength, "ed25519 public key");
let mut compressed = [0u8; 32];
compressed.copy_from_slice(public_key);
match dispatch!(decode_key(&compressed)) {
KeyDecode::Malformed => Err(ic_core::err!(
MalformedEncoding,
"ed25519 public key is not on the curve or not canonical"
)),
KeyDecode::SmallOrder => Err(ic_core::err!(
InvalidParameter,
"ed25519 public key has small order"
)),
KeyDecode::Negated(x, y) => Ok(Self {
compressed,
neg_a: (x, y),
}),
}
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.compressed
}
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<()> {
ic_core::module::operational()?;
ensure!(signature.len() == 64, InvalidLength, "ed25519 signature");
let mut big_r = [0u8; 32];
big_r.copy_from_slice(&signature[..32]);
let mut s = [0u8; 32];
s.copy_from_slice(&signature[32..]);
ensure!(
scalar::is_canonical(&s),
MalformedEncoding,
"ed25519 signature S is not reduced"
);
let k = hash_to_scalar(&[&big_r, &self.compressed, message]);
dispatch!(check_equation(&self.neg_a, &big_r, &k, &s)).into_result()
}
}
impl SelfTest for Ed25519 {
fn self_test() -> Result<()> {
let mut seed = [0u8; 32];
ic_core::codec::hex_decode(
b"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
&mut seed,
)?;
let mut want_pk = [0u8; 32];
ic_core::codec::hex_decode(
b"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
&mut want_pk,
)?;
let mut want_sig = [0u8; 64];
ic_core::codec::hex_decode(
b"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b",
&mut want_sig,
)?;
let mut pk = [0u8; 32];
<Self as SignatureScheme>::public_key(&seed, &mut pk)?;
ensure!(
ic_core::ct::verify(&want_pk, &pk),
SelfTestFailed,
"ed25519"
);
let mut sig = [0u8; 64];
<Self as SignatureScheme>::sign(&seed, b"", &mut sig)?;
ensure!(
ic_core::ct::verify(&want_sig, &sig),
SelfTestFailed,
"ed25519"
);
<Self as SignatureScheme>::verify(&pk, b"", &sig)?;
sig[0] ^= 1;
ensure!(
<Self as SignatureScheme>::verify(&pk, b"", &sig).is_err(),
SelfTestFailed,
"ed25519"
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
type P = Ext<Fe>;
const D: Fe = <Fe as EdField>::D;
const D2: Fe = <Fe as EdField>::D2;
const SQRT_M1: Fe = <Fe as EdField>::SQRT_M1;
fn basepoint() -> P {
super::basepoint::<Fe>()
}
fn mul_basepoint(s: &[u8; 32]) -> P {
super::mul_basepoint::<Fe>(s)
}
fn neg_a(vk: &Ed25519VerifyKey) -> P {
P::from_affine(Fe::from_bytes(&vk.neg_a.0), Fe::from_bytes(&vk.neg_a.1))
}
use ic_core::codec::{hex, unhex};
#[test]
fn curve_constants_are_correct() {
let d = Fe::from_u64(121_665)
.neg()
.mul(&Fe::from_u64(121_666).invert());
assert_eq!(hex(&D.to_bytes()), hex(&d.to_bytes()), "d");
assert_eq!(hex(&D2.to_bytes()), hex(&d.add(&d).to_bytes()), "2d");
assert_eq!(
hex(&SQRT_M1.square().to_bytes()),
hex(&Fe::ONE.neg().to_bytes()),
"sqrt(-1)"
);
}
#[test]
fn basepoint_has_the_expected_coordinates() {
let b = basepoint();
let expected_y = Fe::from_u64(4).mul(&Fe::from_u64(5).invert());
let z_inv = b.z.invert();
assert_eq!(
hex(&b.y.mul(&z_inv).to_bytes()),
hex(&expected_y.to_bytes())
);
assert_eq!(hex(&b.compress()), hex(&BASEPOINT_COMPRESSED));
}
#[test]
fn doubling_agrees_with_adding_a_point_to_itself() {
let mut p = basepoint();
let mut checked = 0;
for _ in 0..16 {
assert_eq!(
p.double().compress(),
p.add(&p).compress(),
"dedicated doubling and self-addition differ"
);
p = p.add(&basepoint());
checked += 1;
}
assert_eq!(checked, 16, "the comparison did not run");
assert_eq!(P::IDENTITY.double().compress(), P::IDENTITY.compress());
}
#[test]
fn projective_equality_agrees_with_compressed_equality() {
let b = basepoint();
let mut points = std::vec![P::IDENTITY, b];
let mut p = b;
for _ in 0..6 {
p = p.double();
points.push(p);
}
let mut checked = 0;
for (i, a) in points.iter().enumerate() {
for (j, c) in points.iter().enumerate() {
let projective = a.eq_projective(c);
let compressed = a.compress() == c.compress();
assert_eq!(
projective, compressed,
"projective and compressed equality differ for {i} vs {j}"
);
checked += 1;
}
}
assert_eq!(checked, 64, "the comparison did not run");
let scaled = b.add(&P::IDENTITY);
assert!(b.eq_projective(&scaled), "equal points with different Z");
assert_eq!(b.compress(), scaled.compress());
}
#[test]
fn group_law_is_consistent() {
let b = basepoint();
assert_eq!(hex(&b.add(&P::IDENTITY).compress()), hex(&b.compress()));
let mut two = [0u8; 32];
two[0] = 2;
assert_eq!(
hex(&b.double().compress()),
hex(&b.mul_scalar(&two).compress())
);
let mut three = [0u8; 32];
three[0] = 3;
assert_eq!(
hex(&b.double().add(&b).compress()),
hex(&b.mul_scalar(&three).compress())
);
}
#[test]
fn order_of_the_basepoint_is_l() {
assert_eq!(
hex(&basepoint().mul_scalar(&scalar::L).compress()),
hex(&P::IDENTITY.compress())
);
}
#[test]
fn compression_roundtrips() {
let b = basepoint();
for k in [1u8, 2, 3, 47, 200] {
let mut s = [0u8; 32];
s[0] = k;
let p = b.mul_scalar(&s);
let c = p.compress();
let d = P::decompress(&c).expect("valid point");
assert_eq!(hex(&d.compress()), hex(&c), "k = {k}");
}
}
#[test]
fn a_signature_under_the_identity_key_is_refused() {
let identity = P::IDENTITY.compress();
let s = scalar::reduce(&[7u8; 32]);
let r = mul_basepoint(&s).compress();
let k = hash_to_scalar(&[&r, &identity, b"any message"]);
let neg_a = P::IDENTITY.negate();
let lhs = double_scalar_mul_vartime(&neg_a, &k, &s);
assert!(
lhs.eq_projective(&P::decompress(&r).unwrap()),
"the forgery satisfies the equation"
);
let err = Ed25519VerifyKey::from_bytes(&identity).err().unwrap();
assert_eq!(err.kind(), ic_core::ErrorKind::InvalidParameter);
let mut sig = [0u8; 64];
sig[..32].copy_from_slice(&r);
sig[32..].copy_from_slice(&s);
assert!(Ed25519::verify(&identity, b"any message", &sig).is_err());
}
#[test]
fn a_signature_with_an_identity_r_is_refused() {
let key = Ed25519Key::from_seed(&[0x42u8; 32]).unwrap();
let r = P::IDENTITY.compress();
let message = b"identity R";
let k = hash_to_scalar(&[&r, &key.public, message]);
let s = scalar::mul_add(&k, &key.scalar, &[0u8; 32]);
let mut sig = [0u8; 64];
sig[..32].copy_from_slice(&r);
sig[32..].copy_from_slice(&s);
let vk = Ed25519VerifyKey::from_bytes(&key.public).unwrap();
let lhs = double_scalar_mul_vartime(&neg_a(&vk), &k, &s);
assert!(
lhs.eq_projective(&P::IDENTITY),
"the signature satisfies the equation"
);
let err = vk.verify(message, &sig).unwrap_err();
assert_eq!(err.kind(), ic_core::ErrorKind::MalformedEncoding);
assert_eq!(lhs.compress(), r);
let err = Ed25519::verify(&key.public, message, &sig).unwrap_err();
assert_eq!(err.kind(), ic_core::ErrorKind::MalformedEncoding);
}
#[test]
fn non_canonical_encodings_do_not_decode() {
for sign in [0u8, 0x80] {
let mut canonical = [0u8; 32];
canonical[0] = 3;
canonical[31] |= sign;
let point = P::decompress(&canonical).expect("y = 3 is on the curve");
assert!(!point.has_small_order());
let mut alias = [0xffu8; 32];
alias[0] = 0xf0;
alias[31] = 0x7f | sign;
assert!(P::decompress(&alias).is_none(), "sign {sign:#x}");
}
}
#[test]
fn decompression_rejects_non_curve_points() {
let mut bad = [0u8; 32];
bad[0] = 2;
assert!(P::decompress(&bad).is_none());
}
#[test]
fn rfc8032_vectors() {
let cases: [(&str, &str, &str, &str); 3] = [
(
"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
"",
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b",
),
(
"4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
"3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
"72",
"92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00",
),
(
"c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
"fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
"af82",
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a",
),
];
for (seed_hex, pk_hex, msg_hex, sig_hex) in cases {
let seed = unhex(seed_hex).unwrap();
let msg = unhex(msg_hex).unwrap();
let mut pk = [0u8; 32];
Ed25519::public_key(&seed, &mut pk).unwrap();
assert_eq!(hex(&pk), pk_hex, "public key for {seed_hex}");
let mut sig = [0u8; 64];
Ed25519::sign(&seed, &msg, &mut sig).unwrap();
assert_eq!(hex(&sig), sig_hex, "signature for {seed_hex}");
Ed25519::verify(&pk, &msg, &sig).unwrap();
}
}
#[test]
fn verification_rejects_tampering() {
let seed = [0x42u8; 32];
let mut pk = [0u8; 32];
Ed25519::public_key(&seed, &mut pk).unwrap();
let mut sig = [0u8; 64];
Ed25519::sign(&seed, b"authentic", &mut sig).unwrap();
Ed25519::verify(&pk, b"authentic", &sig).unwrap();
assert!(Ed25519::verify(&pk, b"forged", &sig).is_err());
let mut bad = sig;
bad[0] ^= 1;
assert!(Ed25519::verify(&pk, b"authentic", &bad).is_err());
let mut bad = sig;
bad[40] ^= 1;
assert!(Ed25519::verify(&pk, b"authentic", &bad).is_err());
let mut other_pk = [0u8; 32];
Ed25519::public_key(&[0x43u8; 32], &mut other_pk).unwrap();
assert!(Ed25519::verify(&other_pk, b"authentic", &sig).is_err());
use ic_core::ErrorKind::{AuthenticationFailed, MalformedEncoding};
let vk = Ed25519VerifyKey::from_bytes(&pk).unwrap();
let kind = |r: Result<()>| r.unwrap_err().kind();
assert_eq!(
kind(Ed25519::verify(&pk, b"forged", &sig)),
AuthenticationFailed
);
assert_eq!(kind(vk.verify(b"forged", &sig)), AuthenticationFailed);
let mut bad = sig;
bad[40] ^= 1;
assert_eq!(kind(vk.verify(b"authentic", &bad)), AuthenticationFailed);
let mut bad = sig;
bad[..32].copy_from_slice(&[0u8; 32]);
bad[0] = 2;
assert!(Ext::<Fe>::decompress(&bad[..32].try_into().unwrap()).is_none());
assert_eq!(
kind(Ed25519::verify(&pk, b"authentic", &bad)),
MalformedEncoding
);
assert_eq!(kind(vk.verify(b"authentic", &bad)), MalformedEncoding);
}
#[test]
fn rejects_non_canonical_s() {
let seed = [0x42u8; 32];
let mut pk = [0u8; 32];
Ed25519::public_key(&seed, &mut pk).unwrap();
let mut sig = [0u8; 64];
Ed25519::sign(&seed, b"msg", &mut sig).unwrap();
let mut carry = 0u16;
for i in 0..32 {
let t = sig[32 + i] as u16 + scalar::L[i] as u16 + carry;
sig[32 + i] = t as u8;
carry = t >> 8;
}
assert!(Ed25519::verify(&pk, b"msg", &sig).is_err());
}
#[test]
fn the_cached_key_signs_identically_to_the_seed() {
let mut checked = 0;
for seed in [[0x11u8; 32], [0x9du8; 32], [0xffu8; 32]] {
for message in [&b""[..], &b"x"[..], &b"a longer message to sign"[..]] {
let mut from_seed = [0u8; 64];
Ed25519::sign(&seed, message, &mut from_seed).unwrap();
let key = Ed25519Key::from_seed(&seed).unwrap();
let mut from_key = [0u8; 64];
key.sign(message, &mut from_key).unwrap();
assert_eq!(from_seed, from_key, "the two signing paths diverged");
let mut derived = [0u8; 32];
Ed25519::public_key(&seed, &mut derived).unwrap();
assert_eq!(&derived, key.public_key());
Ed25519::verify(&derived, message, &from_key).unwrap();
checked += 1;
}
}
assert_eq!(checked, 9, "the comparison did not run");
}
fn windowed_scalars() -> std::vec::Vec<[u8; 32]> {
let mut one = [0u8; 32];
one[0] = 1;
let mut eight = [0u8; 32];
eight[0] = 8;
let mut top = [0xffu8; 32];
top[31] = 0x7f;
let mut clamped = [0x9du8; 32];
clamped[0] &= 248;
clamped[31] &= 127;
clamped[31] |= 64;
let mut l_minus_1 = scalar::L;
l_minus_1[0] -= 1;
let mut out = std::vec![[0u8; 32], one, eight, top, clamped, l_minus_1];
for fill in [0x88u8, 0x77, 0x99, 0x55, 0xaa] {
let mut s = [fill; 32];
s[31] &= 0x7f;
out.push(s);
}
out
}
#[test]
fn the_basepoint_constant_is_the_decompressed_encoding() {
let decoded = P::decompress(&BASEPOINT_COMPRESSED).expect("the RFC 8032 basepoint");
let zinv = decoded.z.invert();
for (name, constant, from_encoding) in [
("x", basepoint().x, decoded.x.mul(&zinv)),
("y", basepoint().y, decoded.y.mul(&zinv)),
("t", basepoint().t, decoded.t.mul(&zinv)),
] {
assert_eq!(
constant.to_bytes(),
from_encoding.to_bytes(),
"{name} differs"
);
}
assert_eq!(basepoint().z.to_bytes(), Fe::ONE.to_bytes());
assert_eq!(basepoint().compress(), BASEPOINT_COMPRESSED);
}
#[test]
fn the_windowed_multiplication_agrees_with_the_ladder() {
let b = basepoint();
let mut seven = [0u8; 32];
seven[0] = 7;
let p = b.mul_scalar(&seven);
let mut checked = 0;
for point in [b, p] {
for scalar in windowed_scalars() {
assert_eq!(
mul_scalar_windowed(&point, &scalar).compress(),
point.mul_scalar(&scalar).compress(),
"windowed and ladder differ for {scalar:02x?}"
);
checked += 1;
}
}
assert!(checked >= 20, "only {checked} comparisons ran");
}
#[test]
fn the_untabled_double_multiplication_agrees() {
let b = basepoint();
let mut checked = 0;
for (i, k) in windowed_scalars().into_iter().enumerate() {
let mut seed = [0u8; 32];
seed[0] = 3 + i as u8;
let a = b.mul_scalar(&seed);
for s in [k, [0xffu8; 32], [0x9du8; 32]] {
let untabled = double_scalar_mul_vartime_no_table(&a, &k, &s);
let tabled = double_scalar_mul_vartime(&a, &k, &s);
let ladders = a.mul_scalar(&k).add(&b.mul_scalar(&s));
assert_eq!(
untabled.compress(),
tabled.compress(),
"k={k:02x?} s={s:02x?}"
);
assert_eq!(
untabled.compress(),
ladders.compress(),
"k={k:02x?} s={s:02x?}"
);
checked += 1;
}
}
assert!(checked >= 30, "only {checked} comparisons ran");
}
#[test]
fn the_wnaf_digits_are_odd_sparse_and_faithful() {
for scalar in [[1u8; 32], [0x9du8; 32], [0xffu8; 32], [0x55u8; 32]] {
let naf = wnaf(&scalar, 5);
let mut previous_nonzero: Option<usize> = None;
for (i, d) in naf.iter().enumerate() {
if *d == 0 {
continue;
}
assert!(d % 2 != 0, "digit {d} at {i} is not odd");
assert!((-15..=15).contains(d), "digit {d} at {i} is out of range");
if let Some(j) = previous_nonzero {
assert!(i - j >= 5, "digits at {j} and {i} are adjacent");
}
previous_nonzero = Some(i);
}
const M: u128 = 1_000_000_007;
let mut from_digits = 0u128;
let mut power = 1u128;
for d in naf {
let term = ((d as i128).rem_euclid(M as i128)) as u128;
from_digits = (from_digits + term * power) % M;
power = power * 2 % M;
}
let mut from_bytes = 0u128;
let mut p = 1u128;
for byte in scalar {
from_bytes = (from_bytes + (byte as u128) * p) % M;
p = p * 256 % M;
}
assert_eq!(from_digits, from_bytes, "recoding changed the value");
}
}
#[test]
fn signing_is_deterministic() {
let seed = [0x7fu8; 32];
let mut a = [0u8; 64];
let mut b = [0u8; 64];
Ed25519::sign(&seed, b"same input", &mut a).unwrap();
Ed25519::sign(&seed, b"same input", &mut b).unwrap();
assert_eq!(a, b);
}
#[test]
fn rejects_wrong_lengths() {
let mut out = [0u8; 32];
assert!(Ed25519::public_key(&[0u8; 31], &mut out).is_err());
assert!(Ed25519::sign(&[0u8; 32], b"", &mut [0u8; 63]).is_err());
assert!(Ed25519::verify(&[0u8; 32], b"", &[0u8; 63]).is_err());
}
#[test]
fn self_test_passes() {
Ed25519::self_test().unwrap();
}
fn sample_points(n: usize) -> Vec<P> {
let mut out = Vec::new();
let mut p = basepoint();
for _ in 0..n {
out.push(p);
p = p.double().add(&basepoint());
}
out
}
#[test]
fn the_completed_doubling_agrees_with_the_extended_one() {
for p in sample_points(40) {
let want = p.double();
let got = p.to_projective().double().to_extended();
assert!(got.eq_projective(&want), "doubling disagrees");
let chained = p.to_projective().double().to_projective().double();
let twice = p.double().double();
assert!(chained.to_extended().eq_projective(&twice), "two doublings");
}
}
#[test]
fn niels_addition_agrees_with_the_general_one() {
let pts = sample_points(20);
for p in &pts {
for q in &pts {
let want = p.add(q);
let got = p.add_niels(&q.to_niels()).to_extended();
assert!(got.eq_projective(&want), "add_niels disagrees");
let want_sub = p.add(&q.negate());
let got_sub = p.sub_niels(&q.to_niels()).to_extended();
assert!(got_sub.eq_projective(&want_sub), "sub_niels disagrees");
}
}
}
#[test]
fn affine_niels_addition_agrees_with_the_general_one() {
let pts = sample_points(20);
for p in &pts {
for q in &pts {
let want = p.add(q);
let got = p.add_affine_niels(&q.to_affine_niels()).to_extended();
assert!(got.eq_projective(&want), "add_affine_niels disagrees");
let mut n = q.to_affine_niels();
n.conditional_negate(1);
let want_neg = p.add(&q.negate());
let got_neg = p.add_affine_niels(&n).to_extended();
assert!(got_neg.eq_projective(&want_neg), "negated form disagrees");
}
}
}
#[test]
#[ignore = "diagnostic, not a test"]
fn where_verify_spends_its_time() {
use std::time::Instant;
let seed = [7u8; 32];
let key = Ed25519Key::from_seed(&seed).unwrap();
let msg = b"benchmark message";
let mut sig = [0u8; 64];
key.sign(msg, &mut sig).unwrap();
let pk = *key.public_key();
let mut big_r = [0u8; 32];
big_r.copy_from_slice(&sig[..32]);
let mut s_sc = [0u8; 32];
s_sc.copy_from_slice(&sig[32..]);
let n = 2000;
let time = |label: &str, f: &mut dyn FnMut()| {
let mut best = f64::INFINITY;
for _ in 0..5 {
let t = Instant::now();
for _ in 0..n {
f();
}
let e = t.elapsed().as_secs_f64() / n as f64 * 1e6;
if e < best {
best = e;
}
}
println!(" {label:<34} {best:>9.2} us");
best
};
let a_point = P::decompress(&pk).unwrap();
let k = hash_to_scalar(&[&big_r, &pk, msg]);
println!(
"
ed25519 verify, cost breakdown:"
);
let d = time("decompress (x2 per verify)", &mut || {
core::hint::black_box(P::decompress(&pk));
});
let h = time("hash_to_scalar", &mut || {
core::hint::black_box(hash_to_scalar(&[&big_r, &pk, msg]));
});
let b = time("mul_basepoint (const time)", &mut || {
core::hint::black_box(mul_basepoint(&s_sc));
});
let v = time("double_scalar_mul_vartime", &mut || {
core::hint::black_box(double_scalar_mul_vartime(&a_point.negate(), &k, &s_sc));
});
time(" of which: wnaf(k,5)+wnaf(s,8)", &mut || {
core::hint::black_box(wnaf(&k, 5));
core::hint::black_box(wnaf(&s_sc, 8));
});
time(" of which: odd_a table build", &mut || {
let twice = a_point.double();
let mut odd = [a_point; 8];
for i in 1..8 {
odd[i] = odd[i - 1].add(&twice);
}
let t: [Niels<Fe>; 8] = core::array::from_fn(|i| odd[i].to_niels());
core::hint::black_box(t);
});
time(" of which: 255 doublings", &mut || {
let mut p = a_point;
for _ in 0..255 {
p = p.double();
}
core::hint::black_box(p);
});
time(" of which: 79 additions", &mut || {
let mut p = a_point;
for _ in 0..79 {
p = p.add(&a_point);
}
core::hint::black_box(p);
});
time("compress (one inversion)", &mut || {
core::hint::black_box(a_point.compress());
});
println!(
" {:<34} {:>9.2} us",
"-- accounted for",
2.0 * d + h + b + v
);
println!(
"
field and point primitives, nanoseconds:"
);
let nn = 200_000;
let ns = |label: &str, f: &mut dyn FnMut()| {
let mut best = f64::INFINITY;
for _ in 0..5 {
let t = Instant::now();
for _ in 0..nn {
f();
}
let e = t.elapsed().as_secs_f64() / nn as f64 * 1e9;
if e < best {
best = e;
}
}
println!(" {label:<34} {best:>9.2} ns");
};
let fx = a_point.x;
let fy = a_point.y;
ns("Fe::mul", &mut || {
core::hint::black_box(core::hint::black_box(&fx).mul(core::hint::black_box(&fy)));
});
ns("Fe::square", &mut || {
core::hint::black_box(core::hint::black_box(&fx).square());
});
ns("Fe::add", &mut || {
core::hint::black_box(core::hint::black_box(&fx).add(core::hint::black_box(&fy)));
});
ns("Fe::sub", &mut || {
core::hint::black_box(core::hint::black_box(&fx).sub(core::hint::black_box(&fy)));
});
ns("Fe::neg", &mut || {
core::hint::black_box(core::hint::black_box(&fx).neg());
});
let proj = a_point.to_projective();
let comp = proj.double();
let an = a_point.to_affine_niels();
ns("Projective::double (4S)", &mut || {
core::hint::black_box(core::hint::black_box(&proj).double());
});
ns("Projective::double_projective", &mut || {
core::hint::black_box(core::hint::black_box(proj).double_projective());
});
ns("Completed::to_projective (3M)", &mut || {
core::hint::black_box(core::hint::black_box(&comp).to_projective());
});
ns("Completed::to_extended (4M)", &mut || {
core::hint::black_box(core::hint::black_box(&comp).to_extended());
});
ns("P::add_affine_niels (3M)", &mut || {
core::hint::black_box(
core::hint::black_box(&a_point).add_affine_niels(core::hint::black_box(&an)),
);
});
ns("P::double", &mut || {
core::hint::black_box(core::hint::black_box(&a_point).double());
});
ns("P::add", &mut || {
core::hint::black_box(
core::hint::black_box(&a_point).add(core::hint::black_box(&a_point)),
);
});
}
#[test]
#[ignore = "diagnostic, not a test"]
fn count_the_point_operations() {
let mut doublings = 0usize;
let mut adds_a = 0usize;
let mut adds_b = 0usize;
let mut state = 0x1234_5678_9abc_def0u64;
let trials = 200;
for _ in 0..trials {
let mut kb = [0u8; 32];
for c in kb.chunks_exact_mut(8) {
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
c.copy_from_slice(&state.wrapping_mul(0x2545_f491_4f6c_dd1d).to_le_bytes());
}
kb[31] &= 0x0f;
let na = wnaf(&kb, 5);
let nb = wnaf(&kb, 8);
let mut i = 257;
while i > 0 && na[i] == 0 && nb[i] == 0 {
i -= 1;
}
doublings += i + 1;
adds_a += na.iter().filter(|d| **d != 0).count();
adds_b += nb.iter().filter(|d| **d != 0).count();
}
let d = doublings as f64 / trials as f64;
let aa = adds_a as f64 / trials as f64;
let ab = adds_b as f64 / trials as f64;
println!(
"
per double-scalar multiplication, averaged over {trials} scalars:"
);
println!(" doublings {d:>8.1}");
println!(" additions, w=5 table (A) {aa:>8.1}");
println!(" additions, w=8 table (B) {ab:>8.1}");
println!(" additions, building A {:>8.1}", 8.0);
println!(" ---");
println!(" total additions {:>8.1}", aa + ab + 8.0);
println!(
" field muls, at 4M+4S per doubling and 9M per addition: {:>6.0}",
d * 8.0 + (aa + ab + 8.0) * 9.0
);
println!(
" the same at dalek's 3M+4S and 7M: {:>6.0}",
d * 7.0 + (aa + ab + 8.0) * 7.0
);
}
}