From dd4164eefe3b6847419a3be50e05f37be36e3e86 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Wed, 17 Jun 2026 09:45:41 -0400 Subject: [PATCH] impl secp256r1 (#45) * impl secp256r1 * CI * refactor: remove sigantures; add msm * clean up scalar_mul APIs * ci * address comments * CI * fix Eq/PartialEq bug for projective point --- Cargo.lock | 141 +++++++ Cargo.toml | 4 + secp256r1/Cargo.toml | 27 ++ secp256r1/README.md | 125 ++++++ secp256r1/benches/field.rs | 179 +++++++++ secp256r1/benches/group.rs | 331 ++++++++++++++++ secp256r1/benches/scalar.rs | 115 ++++++ secp256r1/src/field.rs | 665 ++++++++++++++++++++++++++++++++ secp256r1/src/group.rs | 746 ++++++++++++++++++++++++++++++++++++ secp256r1/src/lib.rs | 29 ++ secp256r1/src/scalar.rs | 561 +++++++++++++++++++++++++++ 11 files changed, 2923 insertions(+) create mode 100644 secp256r1/Cargo.toml create mode 100644 secp256r1/README.md create mode 100644 secp256r1/benches/field.rs create mode 100644 secp256r1/benches/group.rs create mode 100644 secp256r1/benches/scalar.rs create mode 100644 secp256r1/src/field.rs create mode 100644 secp256r1/src/group.rs create mode 100644 secp256r1/src/lib.rs create mode 100644 secp256r1/src/scalar.rs diff --git a/Cargo.lock b/Cargo.lock index eeb8c4c..533eb7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,6 +222,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64ct" version = "1.8.3" @@ -539,6 +545,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -681,6 +699,23 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -768,6 +803,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "funty" version = "2.0.0" @@ -806,6 +856,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1103,12 +1154,59 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "owo-colors" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "elliptic-curve", + "primeorder", +] + [[package]] name = "p3-air" version = "0.4.3" @@ -1421,6 +1519,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "plotters" version = "0.3.7" @@ -1468,6 +1572,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1708,6 +1821,28 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256r1" +version = "0.1.0" +dependencies = [ + "criterion", + "openssl", + "p256", +] + [[package]] name = "semver" version = "1.0.28" @@ -2086,6 +2221,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 1f6a8be..06b778f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "curve25519/curve25519-cuda", "curve25519/solana-ed25519", "experimental/ed25519-pokos", + "secp256r1", "syscall/bls12-381-syscall", "syscall/bn254-syscall", ] @@ -41,7 +42,10 @@ group = { version = "0.13.0", default-features = false } hashbrown = "0.15" hex = "0.4.2" hex-literal = "1.1.0" +hmac = "0.12" +openssl = "0.10" pairing = "0.23.0" +p256 = { version = "0.13", default-features = false, features = ["arithmetic", "expose-field"] } pkcs8 = { version = "0.10.1" } once_cell = "1.21" proptest = "=1.6.0" diff --git a/secp256r1/Cargo.toml b/secp256r1/Cargo.toml new file mode 100644 index 0000000..0dd16c3 --- /dev/null +++ b/secp256r1/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "secp256r1" +description = "Pure-Rust secp256r1/P-256 field, scalar, and group operations" +version = "0.1.0" +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = "2024" +readme = "README.md" + +[dev-dependencies] +criterion.workspace = true +openssl.workspace = true +p256.workspace = true + +[[bench]] +name = "field" +harness = false + +[[bench]] +name = "group" +harness = false + +[[bench]] +name = "scalar" +harness = false diff --git a/secp256r1/README.md b/secp256r1/README.md new file mode 100644 index 0000000..9344421 --- /dev/null +++ b/secp256r1/README.md @@ -0,0 +1,125 @@ +# secp256r1 + +Pure-Rust secp256r1/P-256 field, scalar, and group operations. + +This crate is scoped to low-level public curve arithmetic for benchmarking, +experimentation, and syscall plumbing. It does not expose ECDSA signing or +verification APIs. + +## Status + +This crate is performance-oriented and experimental. It has not been audited. +Group scalar multiplication APIs are variable time and intended for public +inputs. Do not use them with secret scalars in environments where local +timing/cache side channels are in scope. + +Current scope: + +- Base-field arithmetic modulo the P-256 field modulus +- Scalar-field arithmetic modulo the P-256 group order +- Affine and Jacobian projective point operations +- Compressed and uncompressed fixed-length point input +- Uncompressed fixed-length point output +- Single-scalar, fixed-base scalar, double-scalar, and multiscalar multiplication + +OpenSSL and `p256` are used only as dev/benchmark comparison dependencies. + +## Installation + +```toml +[dependencies] +secp256r1 = { path = "." } +``` + +## API + +```rust +use secp256r1::{ + group::{AffinePoint, ProjectivePoint}, + scalar::Scalar, +}; +``` + +### Scalar Multiplication + +```rust +use secp256r1::group::{AffinePoint, ProjectivePoint}; + +let scalar = [7u8; 32]; + +let fixed_base = ProjectivePoint::fixed_base_scalar_mul_vartime(scalar); +let variable_base = ProjectivePoint::from_affine(AffinePoint::generator()) + .mul_scalar_vartime(scalar); + +assert_eq!(fixed_base.to_affine(), variable_base.to_affine()); +``` + +### Multiscalar Multiplication + +```rust +use secp256r1::group::{AffinePoint, ProjectivePoint}; + +let points = [AffinePoint::generator(), ProjectivePoint::generator().double().to_affine()]; +let scalars = [[7u8; 32], [11u8; 32]]; + +let msm = ProjectivePoint::multi_scalar_mul_vartime(&points, &scalars).unwrap(); +let separate = ProjectivePoint::from_affine(points[0]).mul_scalar_vartime(scalars[0]) + + ProjectivePoint::from_affine(points[1]).mul_scalar_vartime(scalars[1]); + +assert_eq!(msm.to_affine(), separate.to_affine()); +``` + +### Encoded Points + +```rust +use secp256r1::group::{AffinePoint, ProjectivePoint}; + +let uncompressed = ProjectivePoint::generator().to_uncompressed().unwrap(); +let parsed = AffinePoint::from_uncompressed(uncompressed).unwrap(); + +assert_eq!(parsed, AffinePoint::generator()); +``` + +## Benchmarks + +Run all secp256r1 benchmarks: + +```sh +cargo bench -p secp256r1 +``` + +Focused benchmark groups: + +```sh +cargo bench -p secp256r1 --bench field +cargo bench -p secp256r1 --bench scalar +cargo bench -p secp256r1 --bench group +``` + +Representative local results from this workspace: + +### Group Ops + +| Benchmark | rust | p256 | OpenSSL | +|---|---:|---:|---:| +| point double | 81.184 ns | 198.83 ns | 222.82 ns public EC | +| point add | 131.49 ns | 222.38 ns | 216.44 ns public EC | +| mixed add | 95.753 ns | 195.68 ns | n/a | +| variable-base scalar mul | 30.579 us | 75.541 us | n/a | +| fixed-base scalar mul | 3.087 us | n/a | 3.539 us | +| double scalar mul | 36.716 us | 150.58 us separate | 25.352 us | + +### Multiscalar Multiplication + +| Benchmark | rust MSM | rust separate | p256 separate | +|---|---:|---:|---:| +| 8-point MSM | 96.571 us | 244.41 us | 601.21 us | +| 32-point MSM | 322.63 us | 1.300 ms | 2.410 ms | + +Benchmark numbers are machine- and compiler-dependent. Re-run locally before +making performance decisions. + +## Safety + +The crate forbids `unsafe` in library code. Benchmark code uses OpenSSL public +APIs for comparison and is not part of the library. diff --git a/secp256r1/benches/field.rs b/secp256r1/benches/field.rs new file mode 100644 index 0000000..17ce822 --- /dev/null +++ b/secp256r1/benches/field.rs @@ -0,0 +1,179 @@ +use std::hint::black_box; + +use criterion::{Criterion, criterion_group, criterion_main}; +use openssl::bn::{BigNum, BigNumContext}; +use p256::{FieldElement as P256FieldElement, elliptic_curve::ff::PrimeField}; +use secp256r1::field::FieldElement; + +const A: [u8; 32] = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, +]; +const B: [u8; 32] = [ + 0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x80, 0x70, 0x60, 0x50, 0x40, 0x30, 0x20, 0x10, +]; +const P: [u8; 32] = [ + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +]; + +struct Fixture { + rust_a: FieldElement, + rust_b: FieldElement, + openssl_a: BigNum, + openssl_b: BigNum, + openssl_p: BigNum, + p256_a: P256FieldElement, + p256_b: P256FieldElement, +} + +impl Fixture { + fn new() -> Self { + Self { + rust_a: FieldElement::from_be_bytes(A).unwrap(), + rust_b: FieldElement::from_be_bytes(B).unwrap(), + openssl_a: BigNum::from_slice(&A).unwrap(), + openssl_b: BigNum::from_slice(&B).unwrap(), + openssl_p: BigNum::from_slice(&P).unwrap(), + p256_a: p256_field(A), + p256_b: p256_field(B), + } + } +} + +fn p256_field(bytes: [u8; 32]) -> P256FieldElement { + Option::from(P256FieldElement::from_repr(bytes.into())).unwrap() +} + +fn bench_field_add(c: &mut Criterion) { + let fixture = Fixture::new(); + let mut context = BigNumContext::new().unwrap(); + let mut openssl_out = BigNum::new().unwrap(); + let mut group = c.benchmark_group("secp256r1_field_add"); + + group.bench_function("rust", |b| { + b.iter(|| black_box(fixture.rust_a) + black_box(fixture.rust_b)); + }); + + group.bench_function("p256", |b| { + b.iter(|| black_box(fixture.p256_a) + black_box(fixture.p256_b)); + }); + + group.bench_function("openssl_bn_mod_add", |b| { + b.iter(|| { + openssl_out + .mod_add( + black_box(&fixture.openssl_a), + black_box(&fixture.openssl_b), + black_box(&fixture.openssl_p), + &mut context, + ) + .unwrap(); + black_box(&openssl_out); + }); + }); + + group.finish(); +} + +fn bench_field_sub(c: &mut Criterion) { + let fixture = Fixture::new(); + let mut context = BigNumContext::new().unwrap(); + let mut openssl_out = BigNum::new().unwrap(); + let mut group = c.benchmark_group("secp256r1_field_sub"); + + group.bench_function("rust", |b| { + b.iter(|| black_box(fixture.rust_a) - black_box(fixture.rust_b)); + }); + + group.bench_function("p256", |b| { + b.iter(|| black_box(fixture.p256_a) - black_box(fixture.p256_b)); + }); + + group.bench_function("openssl_bn_mod_sub", |b| { + b.iter(|| { + openssl_out + .mod_sub( + black_box(&fixture.openssl_a), + black_box(&fixture.openssl_b), + black_box(&fixture.openssl_p), + &mut context, + ) + .unwrap(); + black_box(&openssl_out); + }); + }); + + group.finish(); +} + +fn bench_field_mul(c: &mut Criterion) { + let fixture = Fixture::new(); + let mut context = BigNumContext::new().unwrap(); + let mut openssl_out = BigNum::new().unwrap(); + let mut group = c.benchmark_group("secp256r1_field_mul"); + + group.bench_function("rust", |b| { + b.iter(|| black_box(fixture.rust_a) * black_box(fixture.rust_b)); + }); + + group.bench_function("p256", |b| { + b.iter(|| black_box(fixture.p256_a) * black_box(fixture.p256_b)); + }); + + group.bench_function("openssl_bn_mod_mul", |b| { + b.iter(|| { + openssl_out + .mod_mul( + black_box(&fixture.openssl_a), + black_box(&fixture.openssl_b), + black_box(&fixture.openssl_p), + &mut context, + ) + .unwrap(); + black_box(&openssl_out); + }); + }); + + group.finish(); +} + +fn bench_field_square(c: &mut Criterion) { + let fixture = Fixture::new(); + let mut context = BigNumContext::new().unwrap(); + let mut openssl_out = BigNum::new().unwrap(); + let mut group = c.benchmark_group("secp256r1_field_square"); + + group.bench_function("rust", |b| { + b.iter(|| black_box(fixture.rust_a).square()); + }); + + group.bench_function("p256", |b| { + b.iter(|| black_box(fixture.p256_a).square()); + }); + + group.bench_function("openssl_bn_mod_sqr", |b| { + b.iter(|| { + openssl_out + .mod_sqr( + black_box(&fixture.openssl_a), + black_box(&fixture.openssl_p), + &mut context, + ) + .unwrap(); + black_box(&openssl_out); + }); + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_field_add, + bench_field_sub, + bench_field_mul, + bench_field_square +); +criterion_main!(benches); diff --git a/secp256r1/benches/group.rs b/secp256r1/benches/group.rs new file mode 100644 index 0000000..a122be9 --- /dev/null +++ b/secp256r1/benches/group.rs @@ -0,0 +1,331 @@ +use std::hint::black_box; + +use criterion::{Criterion, criterion_group, criterion_main}; +use openssl::{ + bn::{BigNum, BigNumContext}, + ec::{EcGroup, EcPoint}, + nid::Nid, +}; +use p256::{ + AffinePoint as P256AffinePoint, ProjectivePoint as P256ProjectivePoint, Scalar, + elliptic_curve::{ff::PrimeField, group::Group}, +}; +use secp256r1::group::{AffinePoint, ProjectivePoint}; + +const SCALAR: [u8; 32] = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, +]; + +const MSM_8_POINTS: usize = 8; +const MSM_32_POINTS: usize = 32; + +#[derive(Clone, Copy)] +struct Fixture { + rust_g: ProjectivePoint, + rust_2g: ProjectivePoint, + rust_affine_g: AffinePoint, + p256_g: P256ProjectivePoint, + p256_2g: P256ProjectivePoint, + p256_affine_g: P256AffinePoint, + p256_scalar: Scalar, +} + +impl Fixture { + fn new() -> Self { + let rust_g = ProjectivePoint::generator(); + let p256_g = P256ProjectivePoint::generator(); + + Self { + rust_g, + rust_2g: rust_g.double(), + rust_affine_g: AffinePoint::generator(), + p256_g, + p256_2g: p256_g.double(), + p256_affine_g: P256AffinePoint::GENERATOR, + p256_scalar: Option::::from(Scalar::from_repr(SCALAR.into())).unwrap(), + } + } +} + +fn scalar_for_index(index: usize) -> [u8; 32] { + let mut scalar = SCALAR; + scalar[30] ^= (index as u8).wrapping_mul(17); + scalar[31] = scalar[31].wrapping_add(index as u8); + scalar +} + +fn rust_msm_fixture(count: usize) -> (Vec, Vec<[u8; 32]>) { + let generator = ProjectivePoint::generator(); + let mut point = generator; + let mut points = Vec::with_capacity(count); + let mut scalars = Vec::with_capacity(count); + + for i in 0..count { + points.push(point.to_affine()); + scalars.push(scalar_for_index(i)); + point = point + generator; + } + + (points, scalars) +} + +fn p256_msm_fixture(count: usize) -> (Vec, Vec) { + let generator = P256ProjectivePoint::generator(); + let mut point = generator; + let mut points = Vec::with_capacity(count); + let mut scalars = Vec::with_capacity(count); + + for i in 0..count { + points.push(point); + scalars + .push(Option::::from(Scalar::from_repr(scalar_for_index(i).into())).unwrap()); + point += generator; + } + + (points, scalars) +} + +fn openssl_fixture() -> (EcGroup, BigNumContext, EcPoint, EcPoint) { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let mut context = BigNumContext::new().unwrap(); + let generator = group.generator_opt().unwrap().to_owned(&group).unwrap(); + let mut double_generator = EcPoint::new(&group).unwrap(); + double_generator + .add(&group, &generator, &generator, &mut context) + .unwrap(); + + (group, context, generator, double_generator) +} + +fn bench_group_double(c: &mut Criterion) { + let fixture = Fixture::new(); + let (openssl_group, mut openssl_context, openssl_g, _) = openssl_fixture(); + let mut openssl_out = EcPoint::new(&openssl_group).unwrap(); + let mut group = c.benchmark_group("secp256r1_group_double"); + + group.bench_function("rust", |b| { + b.iter(|| black_box(fixture.rust_g).double()); + }); + + group.bench_function("p256", |b| { + b.iter(|| black_box(fixture.p256_g).double()); + }); + + group.bench_function("openssl_ec_point_add_self", |b| { + b.iter(|| { + openssl_out + .add( + &openssl_group, + black_box(&openssl_g), + black_box(&openssl_g), + &mut openssl_context, + ) + .unwrap(); + black_box(&openssl_out); + }); + }); + + group.finish(); +} + +fn bench_group_add(c: &mut Criterion) { + let fixture = Fixture::new(); + let (openssl_group, mut openssl_context, openssl_g, openssl_2g) = openssl_fixture(); + let mut openssl_out = EcPoint::new(&openssl_group).unwrap(); + let mut group = c.benchmark_group("secp256r1_group_add"); + + group.bench_function("rust", |b| { + b.iter(|| black_box(fixture.rust_2g) + black_box(fixture.rust_g)); + }); + + group.bench_function("p256", |b| { + b.iter(|| black_box(fixture.p256_2g) + black_box(fixture.p256_g)); + }); + + group.bench_function("openssl_ec_point_add", |b| { + b.iter(|| { + openssl_out + .add( + &openssl_group, + black_box(&openssl_2g), + black_box(&openssl_g), + &mut openssl_context, + ) + .unwrap(); + black_box(&openssl_out); + }); + }); + + group.finish(); +} + +fn bench_group_mixed_add(c: &mut Criterion) { + let fixture = Fixture::new(); + let mut group = c.benchmark_group("secp256r1_group_mixed_add"); + + group.bench_function("rust", |b| { + b.iter(|| black_box(fixture.rust_2g).add_mixed(black_box(fixture.rust_affine_g))); + }); + + group.bench_function("p256", |b| { + b.iter(|| black_box(fixture.p256_2g) + black_box(fixture.p256_affine_g)); + }); + + group.finish(); +} + +fn bench_group_base_scalar_mul(c: &mut Criterion) { + let fixture = Fixture::new(); + let (openssl_group, mut openssl_context, _, _) = openssl_fixture(); + let openssl_scalar = BigNum::from_slice(&SCALAR).unwrap(); + let mut openssl_out = EcPoint::new(&openssl_group).unwrap(); + let mut group = c.benchmark_group("secp256r1_group_base_scalar_mul"); + + group.bench_function("rust_variable_base", |b| { + b.iter(|| black_box(fixture.rust_g).mul_scalar_vartime(black_box(SCALAR))); + }); + + group.bench_function("rust_fixed_base", |b| { + b.iter(|| ProjectivePoint::fixed_base_scalar_mul_vartime(black_box(SCALAR))); + }); + + group.bench_function("p256", |b| { + b.iter(|| black_box(fixture.p256_g) * black_box(fixture.p256_scalar)); + }); + + group.bench_function("openssl_ec_point_mul_generator", |b| { + b.iter(|| { + openssl_out + .mul_generator2( + &openssl_group, + black_box(&openssl_scalar), + &mut openssl_context, + ) + .unwrap(); + black_box(&openssl_out); + }); + }); + + group.finish(); +} + +fn bench_group_double_scalar_mul(c: &mut Criterion) { + let fixture = Fixture::new(); + let rust_q = fixture.rust_2g.to_affine(); + let rust_msm_points = [AffinePoint::generator(), rust_q]; + let rust_msm_scalars = [SCALAR, SCALAR]; + let (openssl_group, mut openssl_context, _, openssl_q) = openssl_fixture(); + let openssl_scalar = BigNum::from_slice(&SCALAR).unwrap(); + let mut openssl_out = EcPoint::new(&openssl_group).unwrap(); + let mut group = c.benchmark_group("secp256r1_group_double_scalar_mul"); + + group.bench_function("rust_separate_projective_q", |b| { + b.iter(|| { + ProjectivePoint::fixed_base_scalar_mul_vartime(black_box(SCALAR)) + + ProjectivePoint::from_affine(black_box(rust_q)) + .mul_scalar_vartime(black_box(SCALAR)) + }); + }); + + group.bench_function("rust_msm_window4", |b| { + b.iter(|| { + ProjectivePoint::multi_scalar_mul_vartime( + black_box(&rust_msm_points), + black_box(&rust_msm_scalars), + ) + .unwrap() + }); + }); + + group.bench_function("rust_double_scalar", |b| { + b.iter(|| { + ProjectivePoint::double_scalar_mul_vartime( + black_box(SCALAR), + black_box(rust_q), + black_box(SCALAR), + ) + }); + }); + + group.bench_function("p256", |b| { + b.iter(|| { + (black_box(fixture.p256_g) * black_box(fixture.p256_scalar)) + + (black_box(fixture.p256_2g) * black_box(fixture.p256_scalar)) + }); + }); + + group.bench_function("openssl_ec_point_mul_full", |b| { + b.iter(|| { + openssl_out + .mul_full( + &openssl_group, + black_box(&openssl_scalar), + black_box(&openssl_q), + black_box(&openssl_scalar), + &mut openssl_context, + ) + .unwrap(); + black_box(&openssl_out); + }); + }); + + group.finish(); +} + +fn bench_group_multi_scalar_mul(c: &mut Criterion) { + for count in [MSM_8_POINTS, MSM_32_POINTS] { + let (rust_points, rust_scalars) = rust_msm_fixture(count); + let (p256_points, p256_scalars) = p256_msm_fixture(count); + let mut group = c.benchmark_group(format!("secp256r1_group_multi_scalar_mul_{count}")); + + group.bench_function("rust_msm_window4", |b| { + b.iter(|| { + ProjectivePoint::multi_scalar_mul_vartime( + black_box(rust_points.as_slice()), + black_box(rust_scalars.as_slice()), + ) + .unwrap() + }); + }); + + group.bench_function("rust_separate", |b| { + b.iter(|| { + let mut out = ProjectivePoint::identity(); + + for (point, scalar) in rust_points.iter().zip(rust_scalars.iter()) { + out = out + + ProjectivePoint::from_affine(black_box(*point)) + .mul_scalar_vartime(black_box(*scalar)); + } + + out + }); + }); + + group.bench_function("p256_separate", |b| { + b.iter(|| { + let mut out = P256ProjectivePoint::IDENTITY; + + for (point, scalar) in p256_points.iter().zip(p256_scalars.iter()) { + out += black_box(*point) * black_box(*scalar); + } + + out + }); + }); + + group.finish(); + } +} + +criterion_group!( + benches, + bench_group_double, + bench_group_add, + bench_group_mixed_add, + bench_group_base_scalar_mul, + bench_group_double_scalar_mul, + bench_group_multi_scalar_mul +); +criterion_main!(benches); diff --git a/secp256r1/benches/scalar.rs b/secp256r1/benches/scalar.rs new file mode 100644 index 0000000..3558756 --- /dev/null +++ b/secp256r1/benches/scalar.rs @@ -0,0 +1,115 @@ +use std::hint::black_box; + +use criterion::{Criterion, criterion_group, criterion_main}; +use openssl::bn::{BigNum, BigNumContext}; +use p256::{Scalar as P256Scalar, elliptic_curve::ff::PrimeField}; +use secp256r1::scalar::Scalar; + +const A: [u8; 32] = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, +]; + +const B: [u8; 32] = [ + 0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x80, 0x70, 0x60, 0x50, 0x40, 0x30, 0x20, 0x10, +]; + +const ORDER: [u8; 32] = [ + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xbc, 0xe6, 0xfa, 0xad, 0xa7, 0x17, 0x9e, 0x84, 0xf3, 0xb9, 0xca, 0xc2, 0xfc, 0x63, 0x25, 0x51, +]; + +struct Fixture { + rust_a: Scalar, + rust_b: Scalar, + p256_a: P256Scalar, + p256_b: P256Scalar, + openssl_a: BigNum, + openssl_order: BigNum, +} + +impl Fixture { + fn new() -> Self { + Self { + rust_a: Scalar::from_be_bytes(A).unwrap(), + rust_b: Scalar::from_be_bytes(B).unwrap(), + p256_a: p256_scalar(A), + p256_b: p256_scalar(B), + openssl_a: BigNum::from_slice(&A).unwrap(), + openssl_order: BigNum::from_slice(&ORDER).unwrap(), + } + } +} + +fn p256_scalar(bytes: [u8; 32]) -> P256Scalar { + Option::from(P256Scalar::from_repr(bytes.into())).unwrap() +} + +fn bench_scalar_mul(c: &mut Criterion) { + let fixture = Fixture::new(); + let mut group = c.benchmark_group("secp256r1_scalar_mul"); + + group.bench_function("rust", |b| { + b.iter(|| black_box(fixture.rust_a) * black_box(fixture.rust_b)); + }); + + group.bench_function("p256", |b| { + b.iter(|| black_box(fixture.p256_a) * black_box(fixture.p256_b)); + }); + + group.finish(); +} + +fn bench_scalar_square(c: &mut Criterion) { + let fixture = Fixture::new(); + let mut group = c.benchmark_group("secp256r1_scalar_square"); + + group.bench_function("rust", |b| { + b.iter(|| black_box(fixture.rust_a).square()); + }); + + group.bench_function("p256", |b| { + b.iter(|| black_box(fixture.p256_a).square()); + }); + + group.finish(); +} + +fn bench_scalar_invert(c: &mut Criterion) { + let fixture = Fixture::new(); + let mut context = BigNumContext::new().unwrap(); + let mut openssl_out = BigNum::new().unwrap(); + let mut group = c.benchmark_group("secp256r1_scalar_invert"); + + group.bench_function("rust", |b| { + b.iter(|| black_box(fixture.rust_a).invert().unwrap()); + }); + + group.bench_function("p256", |b| { + b.iter(|| black_box(fixture.p256_a).invert().unwrap()); + }); + + group.bench_function("openssl_bn_mod_inverse", |b| { + b.iter(|| { + openssl_out + .mod_inverse( + black_box(&fixture.openssl_a), + black_box(&fixture.openssl_order), + &mut context, + ) + .unwrap(); + black_box(&openssl_out); + }); + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_scalar_mul, + bench_scalar_square, + bench_scalar_invert +); +criterion_main!(benches); diff --git a/secp256r1/src/field.rs b/secp256r1/src/field.rs new file mode 100644 index 0000000..01a79b8 --- /dev/null +++ b/secp256r1/src/field.rs @@ -0,0 +1,665 @@ +//! P-256 base field arithmetic. +//! +//! [`FieldElement`] represents an element of GF(p) where +//! `p = 2^256 - 2^224 + 2^192 + 2^96 - 1` (the P-256 field prime). +//! Internally elements are stored in Montgomery form; use +//! [`from_be_bytes`][FieldElement::from_be_bytes] and +//! [`to_be_bytes`][FieldElement::to_be_bytes] to convert from/to canonical +//! big-endian representation. + +use core::ops::{Add, Mul, Neg, Sub}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FieldElement { + limbs: [u64; 4], +} + +const MODULUS: [u64; 4] = [ + 0xffff_ffff_ffff_ffff, + 0x0000_0000_ffff_ffff, + 0x0000_0000_0000_0000, + 0xffff_ffff_0000_0001, +]; + +const R2: [u64; 4] = [ + 0x0000_0000_0000_0003, + 0xffff_fffb_ffff_ffff, + 0xffff_ffff_ffff_fffe, + 0x0000_0004_ffff_fffd, +]; + +const P_PLUS_ONE_DIV_4: [u8; 32] = [ + 0x3f, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +impl FieldElement { + pub const ZERO: Self = Self { limbs: [0; 4] }; + pub const ONE: Self = Self { + limbs: [ + 0x0000_0000_0000_0001, + 0xffff_ffff_0000_0000, + 0xffff_ffff_ffff_ffff, + 0x0000_0000_ffff_fffe, + ], + }; + + #[inline] + pub fn from_u64(value: u64) -> Self { + Self::from_canonical_limbs([value, 0, 0, 0]).expect("u64 is canonical") + } + + #[inline] + pub fn from_be_bytes(bytes: [u8; 32]) -> Option { + Self::from_canonical_limbs(limbs_from_be_bytes(bytes)) + } + + #[inline] + pub fn to_be_bytes(self) -> [u8; 32] { + be_bytes_from_limbs(from_montgomery(self.limbs)) + } + + #[inline] + pub fn is_zero(self) -> bool { + self == Self::ZERO + } + + #[inline] + pub fn square(self) -> Self { + Self { + limbs: montgomery_square(self.limbs), + } + } + + #[inline] + pub fn invert(self) -> Option { + if self.is_zero() { + return None; + } + + Some(Self { + limbs: montgomery_mul(invert_canonical(from_montgomery(self.limbs)), R2), + }) + } + + #[inline] + pub(crate) fn sqrt(self) -> Option { + let candidate = self.pow(P_PLUS_ONE_DIV_4); + (candidate.square() == self).then_some(candidate) + } + + #[inline] + fn pow(self, exponent: [u8; 32]) -> Self { + let mut out = Self::ONE; + + for byte in exponent { + for bit in (0..8).rev() { + out = out.square(); + if ((byte >> bit) & 1) == 1 { + out = out * self; + } + } + } + + out + } + + #[inline] + pub fn montgomery_limbs(self) -> [u64; 4] { + self.limbs + } + + #[inline] + pub(crate) const fn from_montgomery_limbs(limbs: [u64; 4]) -> Self { + Self { limbs } + } + + #[inline] + fn from_canonical_limbs(limbs: [u64; 4]) -> Option { + if ge_limbs(limbs, MODULUS) { + None + } else { + Some(Self { + limbs: montgomery_mul(limbs, R2), + }) + } + } +} + +impl Add for FieldElement { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self::Output { + let (sum, carry) = add_limbs(self.limbs, rhs.limbs); + Self { + limbs: reduce_sum(sum, carry), + } + } +} + +impl Sub for FieldElement { + type Output = Self; + + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + let (difference, borrow) = sub_limbs(self.limbs, rhs.limbs); + let (corrected, _) = add_limbs(difference, MODULUS); + + Self { + limbs: if borrow == 0 { difference } else { corrected }, + } + } +} + +impl Mul for FieldElement { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Self { + limbs: montgomery_mul(self.limbs, rhs.limbs), + } + } +} + +impl Neg for FieldElement { + type Output = Self; + + #[inline] + fn neg(self) -> Self::Output { + if self.is_zero() { + self + } else { + Self::ZERO - self + } + } +} + +#[inline(always)] +fn add_limbs(a: [u64; 4], b: [u64; 4]) -> ([u64; 4], u64) { + let mut out = [0; 4]; + let mut carry = 0u64; + + for i in 0..4 { + let (sum, carry1) = a[i].overflowing_add(b[i]); + let (sum, carry2) = sum.overflowing_add(carry); + out[i] = sum; + carry = u64::from(carry1 | carry2); + } + + (out, carry) +} + +#[inline(always)] +fn sub_limbs(a: [u64; 4], b: [u64; 4]) -> ([u64; 4], u64) { + let mut out = [0; 4]; + let mut borrow = 0u64; + + for i in 0..4 { + let (difference, borrow1) = a[i].overflowing_sub(b[i]); + let (difference, borrow2) = difference.overflowing_sub(borrow); + out[i] = difference; + borrow = u64::from(borrow1 | borrow2); + } + + (out, borrow) +} + +#[inline(always)] +fn reduce_sum(sum: [u64; 4], carry: u64) -> [u64; 4] { + let (reduced, borrow) = sub_limbs(sum, MODULUS); + + if carry != 0 || borrow == 0 { + reduced + } else { + sum + } +} + +#[inline(always)] +fn ge_limbs(a: [u64; 4], b: [u64; 4]) -> bool { + sub_limbs(a, b).1 == 0 +} + +#[inline(always)] +fn is_one_limbs(a: [u64; 4]) -> bool { + a == [1, 0, 0, 0] +} + +#[inline(always)] +fn is_even_limbs(a: [u64; 4]) -> bool { + (a[0] & 1) == 0 +} + +#[inline(always)] +fn shr1_limbs(a: [u64; 4]) -> [u64; 4] { + shr1_limbs_with_carry(a, 0) +} + +#[inline(always)] +fn shr1_limbs_with_carry(a: [u64; 4], carry: u64) -> [u64; 4] { + [ + (a[0] >> 1) | (a[1] << 63), + (a[1] >> 1) | (a[2] << 63), + (a[2] >> 1) | (a[3] << 63), + (a[3] >> 1) | (carry << 63), + ] +} + +#[inline(always)] +fn half_mod(a: [u64; 4]) -> [u64; 4] { + if is_even_limbs(a) { + shr1_limbs(a) + } else { + let (sum, carry) = add_limbs(a, MODULUS); + shr1_limbs_with_carry(sum, carry) + } +} + +#[inline(always)] +fn sub_mod(a: [u64; 4], b: [u64; 4]) -> [u64; 4] { + let (difference, borrow) = sub_limbs(a, b); + + if borrow == 0 { + difference + } else { + add_limbs(difference, MODULUS).0 + } +} + +fn invert_canonical(value: [u64; 4]) -> [u64; 4] { + let mut u = value; + let mut v = MODULUS; + let mut x1 = [1, 0, 0, 0]; + let mut x2 = [0; 4]; + + while !is_one_limbs(u) && !is_one_limbs(v) { + while is_even_limbs(u) { + u = shr1_limbs(u); + x1 = half_mod(x1); + } + + while is_even_limbs(v) { + v = shr1_limbs(v); + x2 = half_mod(x2); + } + + if ge_limbs(u, v) { + u = sub_limbs(u, v).0; + x1 = sub_mod(x1, x2); + } else { + v = sub_limbs(v, u).0; + x2 = sub_mod(x2, x1); + } + } + + if is_one_limbs(u) { x1 } else { x2 } +} + +#[inline(always)] +fn mul_wide(a: [u64; 4], b: [u64; 4]) -> [u64; 8] { + let (w0, carry) = mac(0, a[0], b[0], 0); + let (w1, carry) = mac(0, a[0], b[1], carry); + let (w2, carry) = mac(0, a[0], b[2], carry); + let (w3, w4) = mac(0, a[0], b[3], carry); + + let (w1, carry) = mac(w1, a[1], b[0], 0); + let (w2, carry) = mac(w2, a[1], b[1], carry); + let (w3, carry) = mac(w3, a[1], b[2], carry); + let (w4, w5) = mac(w4, a[1], b[3], carry); + + let (w2, carry) = mac(w2, a[2], b[0], 0); + let (w3, carry) = mac(w3, a[2], b[1], carry); + let (w4, carry) = mac(w4, a[2], b[2], carry); + let (w5, w6) = mac(w5, a[2], b[3], carry); + + let (w3, carry) = mac(w3, a[3], b[0], 0); + let (w4, carry) = mac(w4, a[3], b[1], carry); + let (w5, carry) = mac(w5, a[3], b[2], carry); + let (w6, w7) = mac(w6, a[3], b[3], carry); + + [w0, w1, w2, w3, w4, w5, w6, w7] +} + +#[inline(always)] +fn montgomery_mul(a: [u64; 4], b: [u64; 4]) -> [u64; 4] { + montgomery_reduce(mul_wide(a, b)) +} + +#[inline(always)] +fn montgomery_square(a: [u64; 4]) -> [u64; 4] { + let a0 = a[0] as u128; + let a1 = a[1] as u128; + let a2 = a[2] as u128; + let a3 = a[3] as u128; + + let p00 = a0 * a0; + let w0 = p00 as u64; + let mut acc = p00 >> 64; + let mut top = 0u64; + + let p01 = a0 * a1; + top += (p01 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p01 << 1); + let w1 = sum as u64; + acc = sum >> 64; + top += u64::from(overflow); + + acc |= (top as u128) << 64; + top = 0; + let p02 = a0 * a2; + top += (p02 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p02 << 1); + acc = sum; + top += u64::from(overflow); + let (sum, overflow) = acc.overflowing_add(a1 * a1); + let w2 = sum as u64; + acc = sum >> 64; + top += u64::from(overflow); + + acc |= (top as u128) << 64; + top = 0; + let p03 = a0 * a3; + top += (p03 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p03 << 1); + acc = sum; + top += u64::from(overflow); + let p12 = a1 * a2; + top += (p12 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p12 << 1); + let w3 = sum as u64; + acc = sum >> 64; + top += u64::from(overflow); + + acc |= (top as u128) << 64; + top = 0; + let p13 = a1 * a3; + top += (p13 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p13 << 1); + acc = sum; + top += u64::from(overflow); + let (sum, overflow) = acc.overflowing_add(a2 * a2); + let w4 = sum as u64; + acc = sum >> 64; + top += u64::from(overflow); + + acc |= (top as u128) << 64; + top = 0; + let p23 = a2 * a3; + top += (p23 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p23 << 1); + let w5 = sum as u64; + acc = sum >> 64; + top += u64::from(overflow); + + acc |= (top as u128) << 64; + let (sum, overflow) = acc.overflowing_add(a3 * a3); + let w6 = sum as u64; + let w7 = (sum >> 64) as u64; + debug_assert!(!overflow); + + montgomery_reduce_words(w0, w1, w2, w3, w4, w5, w6, w7) +} + +#[inline(always)] +fn from_montgomery(a: [u64; 4]) -> [u64; 4] { + montgomery_reduce([a[0], a[1], a[2], a[3], 0, 0, 0, 0]) +} + +#[inline(always)] +fn montgomery_reduce(input: [u64; 8]) -> [u64; 4] { + montgomery_reduce_words( + input[0], input[1], input[2], input[3], input[4], input[5], input[6], input[7], + ) +} + +#[inline(always)] +#[allow(clippy::too_many_arguments)] +fn montgomery_reduce_words( + r0: u64, + r1: u64, + r2: u64, + r3: u64, + r4: u64, + r5: u64, + r6: u64, + r7: u64, +) -> [u64; 4] { + let (r1, carry) = mac(r1, r0, MODULUS[1], r0); + let (r2, carry) = adc(r2, 0, carry); + let (r3, carry) = mac(r3, r0, MODULUS[3], carry); + let (r4, carry2) = adc(r4, 0, carry); + + let (r2, carry) = mac(r2, r1, MODULUS[1], r1); + let (r3, carry) = adc(r3, 0, carry); + let (r4, carry) = mac(r4, r1, MODULUS[3], carry); + let (r5, carry2) = adc(r5, carry2, carry); + + let (r3, carry) = mac(r3, r2, MODULUS[1], r2); + let (r4, carry) = adc(r4, 0, carry); + let (r5, carry) = mac(r5, r2, MODULUS[3], carry); + let (r6, carry2) = adc(r6, carry2, carry); + + let (r4, carry) = mac(r4, r3, MODULUS[1], r3); + let (r5, carry) = adc(r5, 0, carry); + let (r6, carry) = mac(r6, r3, MODULUS[3], carry); + let (r7, r8) = adc(r7, carry2, carry); + + reduce_wide([r4, r5, r6, r7, r8]) +} + +#[inline(always)] +fn reduce_wide(value: [u64; 5]) -> [u64; 4] { + let (w0, borrow) = sbb(value[0], MODULUS[0], 0); + let (w1, borrow) = sbb(value[1], MODULUS[1], borrow); + let (w2, borrow) = sbb(value[2], MODULUS[2], borrow); + let (w3, borrow) = sbb(value[3], MODULUS[3], borrow); + let (_, borrow) = sbb(value[4], 0, borrow); + + if borrow == 0 { + [w0, w1, w2, w3] + } else { + [value[0], value[1], value[2], value[3]] + } +} + +#[inline(always)] +fn adc(a: u64, b: u64, carry: u64) -> (u64, u64) { + let sum = (a as u128) + (b as u128) + (carry as u128); + (sum as u64, (sum >> 64) as u64) +} + +#[inline(always)] +fn sbb(a: u64, b: u64, borrow: u64) -> (u64, u64) { + let difference = (a as u128).wrapping_sub((b as u128) + (borrow as u128)); + (difference as u64, u64::from((difference >> 127) != 0)) +} + +#[inline(always)] +fn mac(a: u64, b: u64, c: u64, carry: u64) -> (u64, u64) { + let product = (a as u128) + (b as u128) * (c as u128) + (carry as u128); + (product as u64, (product >> 64) as u64) +} + +#[inline] +fn limbs_from_be_bytes(bytes: [u8; 32]) -> [u64; 4] { + let mut limbs = [0u64; 4]; + + for (i, chunk) in bytes.chunks_exact(8).rev().enumerate() { + limbs[i] = u64::from_be_bytes(chunk.try_into().expect("chunk length is 8")); + } + + limbs +} + +#[inline] +fn be_bytes_from_limbs(limbs: [u64; 4]) -> [u8; 32] { + let mut bytes = [0u8; 32]; + + for (i, limb) in limbs.iter().rev().enumerate() { + bytes[i * 8..(i + 1) * 8].copy_from_slice(&limb.to_be_bytes()); + } + + bytes +} + +#[cfg(test)] +mod tests { + use super::FieldElement; + use p256::{FieldElement as P256FieldElement, elliptic_curve::ff::PrimeField}; + + const A: [u8; 32] = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, + 0x70, 0x80, + ]; + const B: [u8; 32] = [ + 0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, + 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x80, 0x70, 0x60, 0x50, 0x40, 0x30, + 0x20, 0x10, + ]; + const P_MINUS_ONE: [u8; 32] = [ + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfe, + ]; + const P: [u8; 32] = [ + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, + ]; + + fn p256_field(bytes: [u8; 32]) -> P256FieldElement { + Option::from(P256FieldElement::from_repr(bytes.into())).unwrap() + } + + fn assert_matches_p256(rust: FieldElement, p256: P256FieldElement, operation: &'static str) { + let p256_bytes: [u8; 32] = p256.to_repr().into(); + assert_eq!(rust.to_be_bytes(), p256_bytes, "{operation}"); + } + + fn sample(mut seed: u64) -> [u8; 32] { + let mut bytes = [0u8; 32]; + + for chunk in bytes.chunks_exact_mut(8) { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + chunk.copy_from_slice(&seed.to_be_bytes()); + } + + // Keep samples well below p so generation cannot accidentally produce + // a non-canonical field encoding. + bytes[0] &= 0x7f; + bytes + } + + #[test] + fn rejects_non_canonical_values() { + assert!(FieldElement::from_be_bytes(P).is_none()); + } + + #[test] + fn round_trips_canonical_values() { + for bytes in [[0u8; 32], A, B, P_MINUS_ONE] { + let element = FieldElement::from_be_bytes(bytes).unwrap(); + assert_eq!(element.to_be_bytes(), bytes); + } + } + + #[test] + fn add_matches_p256() { + assert_matches_p256( + FieldElement::from_be_bytes(A).unwrap() + FieldElement::from_be_bytes(B).unwrap(), + p256_field(A) + p256_field(B), + "add", + ); + } + + #[test] + fn sub_matches_p256() { + assert_matches_p256( + FieldElement::from_be_bytes(A).unwrap() - FieldElement::from_be_bytes(B).unwrap(), + p256_field(A) - p256_field(B), + "sub", + ); + } + + #[test] + fn mul_matches_p256() { + assert_matches_p256( + FieldElement::from_be_bytes(A).unwrap() * FieldElement::from_be_bytes(B).unwrap(), + p256_field(A) * p256_field(B), + "mul", + ); + } + + #[test] + fn square_matches_p256() { + assert_matches_p256( + FieldElement::from_be_bytes(A).unwrap().square(), + p256_field(A).square(), + "square", + ); + } + + #[test] + fn invert_matches_p256() { + assert!(FieldElement::ZERO.invert().is_none()); + + for bytes in [A, B, P_MINUS_ONE] { + let rust = FieldElement::from_be_bytes(bytes).unwrap(); + let p256 = p256_field(bytes); + + assert_matches_p256( + rust.invert().unwrap(), + Option::from(p256.invert()).unwrap(), + "invert", + ); + assert_eq!( + (rust * rust.invert().unwrap()).to_be_bytes(), + FieldElement::ONE.to_be_bytes() + ); + } + } + + #[test] + fn edge_values_match_p256() { + let rust = FieldElement::from_be_bytes(P_MINUS_ONE).unwrap(); + let p256 = p256_field(P_MINUS_ONE); + let mut one_bytes = [0u8; 32]; + one_bytes[31] = 1; + let rust_one = FieldElement::ONE; + let p256_one = p256_field(one_bytes); + + assert_matches_p256(rust + rust, p256 + p256, "p_minus_one add"); + assert_matches_p256(rust - rust_one, p256 - p256_one, "p_minus_one sub"); + assert_matches_p256(rust * rust, p256 * p256, "p_minus_one mul"); + assert_matches_p256(rust.square(), p256.square(), "p_minus_one square"); + } + + #[test] + fn arithmetic_matches_p256_for_many_samples() { + for i in 0..256 { + let a = sample(i); + let b = sample(i ^ 0xa5a5_a5a5_a5a5_a5a5); + let rust_a = FieldElement::from_be_bytes(a).unwrap(); + let rust_b = FieldElement::from_be_bytes(b).unwrap(); + let p256_a = p256_field(a); + let p256_b = p256_field(b); + + assert_matches_p256(rust_a + rust_b, p256_a + p256_b, "add"); + assert_matches_p256(rust_a - rust_b, p256_a - p256_b, "sub"); + assert_matches_p256(rust_a * rust_b, p256_a * p256_b, "mul"); + assert_matches_p256(rust_a.square(), p256_a.square(), "square"); + if !rust_a.is_zero() { + assert_matches_p256( + rust_a.invert().unwrap(), + Option::from(p256_a.invert()).unwrap(), + "invert", + ); + } + } + } +} diff --git a/secp256r1/src/group.rs b/secp256r1/src/group.rs new file mode 100644 index 0000000..4838088 --- /dev/null +++ b/secp256r1/src/group.rs @@ -0,0 +1,746 @@ +//! P-256 elliptic curve group operations. +//! +//! Points are represented in two forms: +//! +//! - [`AffinePoint`] — standard `(x, y)` coordinates, used for storage and +//! table entries. Includes an `infinity` flag for the identity element. +//! - [`ProjectivePoint`] — Jacobian `(X : Y : Z)` coordinates, used during +//! multi-step scalar multiplication to avoid per-step field inversions. +//! +//! Use [`ProjectivePoint::to_affine`] to convert back and pay the single +//! field inversion, or [`batch_normalize`][`ProjectivePoint`] implicitly via +//! the precomputed table builders. + +use core::ops::{Add, Neg, Sub}; +use std::sync::OnceLock; + +use crate::field::FieldElement; + +const BASE_WINDOWS: usize = 32; +const BASE_WINDOW_POINTS: usize = 256; +const SHAMIR_WINDOW_POINTS: usize = 16; + +const CURVE_B: FieldElement = FieldElement::from_montgomery_limbs([ + 0xd89c_df62_29c4_bddf, + 0xacf0_05cd_7884_3090, + 0xe5a2_20ab_f721_2ed6, + 0xdc30_061d_0487_4834, +]); + +const GENERATOR_X: FieldElement = FieldElement::from_montgomery_limbs([ + 0x79e7_30d4_18a9_143c, + 0x75ba_95fc_5fed_b601, + 0x79fb_732b_7762_2510, + 0x1890_5f76_a537_55c6, +]); + +const GENERATOR_Y: FieldElement = FieldElement::from_montgomery_limbs([ + 0xddf2_5357_ce95_560a, + 0x8b4a_b8e4_ba19_e45c, + 0xd2e8_8688_dd21_f325, + 0x8571_ff18_2588_5d85, +]); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AffinePoint { + x: FieldElement, + y: FieldElement, + infinity: bool, +} + +impl AffinePoint { + pub const IDENTITY: Self = Self { + x: FieldElement::ZERO, + y: FieldElement::ZERO, + infinity: true, + }; + + pub const GENERATOR: Self = Self { + x: GENERATOR_X, + y: GENERATOR_Y, + infinity: false, + }; + + #[inline] + pub fn identity() -> Self { + Self::IDENTITY + } + + #[inline] + pub fn generator() -> Self { + Self::GENERATOR + } + + #[inline] + pub fn new(x: FieldElement, y: FieldElement) -> Option { + let point = Self { + x, + y, + infinity: false, + }; + + point.is_on_curve().then_some(point) + } + + #[inline] + pub fn from_uncompressed(bytes: [u8; 65]) -> Option { + if bytes[0] != 0x04 { + return None; + } + + let mut x = [0u8; 32]; + let mut y = [0u8; 32]; + x.copy_from_slice(&bytes[1..33]); + y.copy_from_slice(&bytes[33..65]); + + Self::new( + FieldElement::from_be_bytes(x)?, + FieldElement::from_be_bytes(y)?, + ) + } + + #[inline] + pub fn from_compressed(bytes: [u8; 33]) -> Option { + if bytes[0] != 0x02 && bytes[0] != 0x03 { + return None; + } + + let mut x_bytes = [0u8; 32]; + x_bytes.copy_from_slice(&bytes[1..33]); + let x = FieldElement::from_be_bytes(x_bytes)?; + let rhs = x.square() * x - triple(x) + CURVE_B; + let mut y = rhs.sqrt()?; + + if (y.to_be_bytes()[31] & 1) != (bytes[0] & 1) { + y = -y; + } + + ((y.to_be_bytes()[31] & 1) == (bytes[0] & 1)).then_some(Self { + x, + y, + infinity: false, + }) + } + + #[inline] + pub fn to_projective(self) -> ProjectivePoint { + if self.infinity { + ProjectivePoint::IDENTITY + } else { + ProjectivePoint { + x: self.x, + y: self.y, + z: FieldElement::ONE, + } + } + } + + #[inline] + pub fn to_uncompressed(self) -> Option<[u8; 65]> { + if self.infinity { + return None; + } + + let mut out = [0u8; 65]; + out[0] = 0x04; + out[1..33].copy_from_slice(&self.x.to_be_bytes()); + out[33..65].copy_from_slice(&self.y.to_be_bytes()); + Some(out) + } + + #[inline] + pub fn is_identity(self) -> bool { + self.infinity + } + + #[inline] + pub fn x(self) -> Option { + (!self.infinity).then_some(self.x) + } + + #[inline] + pub fn y(self) -> Option { + (!self.infinity).then_some(self.y) + } + + #[inline] + fn is_on_curve(self) -> bool { + if self.infinity { + return true; + } + + let y2 = self.y.square(); + let x2 = self.x.square(); + let x3 = x2 * self.x; + let three_x = triple(self.x); + y2 == x3 - three_x + CURVE_B + } +} + +impl Neg for AffinePoint { + type Output = Self; + + #[inline] + fn neg(self) -> Self::Output { + if self.infinity { + self + } else { + Self { + x: self.x, + y: -self.y, + infinity: false, + } + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct ProjectivePoint { + x: FieldElement, + y: FieldElement, + z: FieldElement, +} + +impl PartialEq for ProjectivePoint { + #[inline] + fn eq(&self, other: &Self) -> bool { + let self_is_identity = self.z.is_zero(); + let other_is_identity = other.z.is_zero(); + + if self_is_identity || other_is_identity { + return self_is_identity && other_is_identity; + } + + let self_z2 = self.z.square(); + let other_z2 = other.z.square(); + let self_z3 = self_z2 * self.z; + let other_z3 = other_z2 * other.z; + + // x1 * z2^2 == x2 * z1^2 + // y1 * z2^3 == y2 * z1^3 + + self.x * other_z2 == other.x * self_z2 && self.y * other_z3 == other.y * self_z3 + } +} + +impl Eq for ProjectivePoint {} + +impl ProjectivePoint { + pub const IDENTITY: Self = Self { + x: FieldElement::ZERO, + y: FieldElement::ONE, + z: FieldElement::ZERO, + }; + + pub const GENERATOR: Self = Self { + x: GENERATOR_X, + y: GENERATOR_Y, + z: FieldElement::ONE, + }; + + #[inline] + pub fn identity() -> Self { + Self::IDENTITY + } + + #[inline] + pub fn generator() -> Self { + Self::GENERATOR + } + + #[inline] + pub fn from_affine(point: AffinePoint) -> Self { + point.to_projective() + } + + #[inline] + pub fn to_affine(self) -> AffinePoint { + match self.z.invert() { + Some(zinv) => { + let zinv2 = zinv.square(); + AffinePoint { + x: self.x * zinv2, + y: self.y * zinv2 * zinv, + infinity: false, + } + } + None => AffinePoint::IDENTITY, + } + } + + #[inline] + pub fn to_uncompressed(self) -> Option<[u8; 65]> { + self.to_affine().to_uncompressed() + } + + #[inline] + pub fn is_identity(self) -> bool { + self.z.is_zero() + } + + #[inline] + pub fn has_affine_x(self, x: FieldElement) -> bool { + !self.is_identity() && self.x == x * self.z.square() + } + + #[inline] + pub fn double(self) -> Self { + if self.is_identity() || self.y.is_zero() { + return Self::IDENTITY; + } + + let xx = self.x.square(); + let yy = self.y.square(); + let yyyy = yy.square(); + let zz = self.z.square(); + let s = double((self.x + yy).square() - xx - yyyy); + let m = triple(xx - zz.square()); + let x = m.square() - double(s); + let y = m * (s - x) - double(double(double(yyyy))); + let z = (self.y + self.z).square() - yy - zz; + + Self { x, y, z } + } + + #[inline] + pub fn add_mixed(self, rhs: AffinePoint) -> Self { + if rhs.infinity { + return self; + } + if self.is_identity() { + return rhs.to_projective(); + } + + let z1z1 = self.z.square(); + let u2 = rhs.x * z1z1; + let s2 = rhs.y * self.z * z1z1; + let h = u2 - self.x; + let slope_num = double(s2 - self.y); + + if h.is_zero() { + return if slope_num.is_zero() { + self.double() + } else { + Self::IDENTITY + }; + } + + let hh = h.square(); + let i = double(double(hh)); + let j = h * i; + let v = self.x * i; + let x = slope_num.square() - j - double(v); + let y = slope_num * (v - x) - double(self.y * j); + let z = (self.z + h).square() - z1z1 - hh; + + Self { x, y, z } + } + + #[inline] + pub fn mul_scalar_vartime(self, scalar: [u8; 32]) -> Self { + let mut table = [Self::IDENTITY; 16]; + table[1] = self; + + for i in 2..16 { + table[i] = table[i - 1] + self; + } + let affine_table = batch_normalize(table); + + let mut out = Self::IDENTITY; + + for &byte in scalar.iter() { + out = out.double().double().double().double(); + out = out.add_mixed(affine_table[(byte >> 4) as usize]); + out = out.double().double().double().double(); + out = out.add_mixed(affine_table[(byte & 0x0f) as usize]); + } + + out + } + + #[inline] + pub fn fixed_base_scalar_mul_vartime(scalar: [u8; 32]) -> Self { + mul_window8_vartime(generator_window8_table(), &scalar) + } + + #[inline] + pub fn double_scalar_mul_vartime( + generator_scalar: [u8; 32], + point: AffinePoint, + point_scalar: [u8; 32], + ) -> Self { + let generator_table = generator_window4_table(); + let point_table = window4_table(point); + let mut out = Self::IDENTITY; + + for (&generator_byte, &point_byte) in generator_scalar.iter().zip(point_scalar.iter()) { + out = double_n(out, 4) + .add_mixed(generator_table[(generator_byte >> 4) as usize]) + .add_mixed(point_table[(point_byte >> 4) as usize]); + out = double_n(out, 4) + .add_mixed(generator_table[(generator_byte & 0x0f) as usize]) + .add_mixed(point_table[(point_byte & 0x0f) as usize]); + } + + out + } + + /// Computes `sum(scalars[i] * points[i])` using variable-time table + /// lookups. + /// + /// Returns `None` when `points` and `scalars` have different lengths. + /// This routine is intended for public inputs, such as syscall MSM + /// plumbing; do not use it with secret scalars. + #[inline] + pub fn multi_scalar_mul_vartime(points: &[AffinePoint], scalars: &[[u8; 32]]) -> Option { + if points.len() != scalars.len() { + return None; + } + + let tables: Vec<_> = points.iter().copied().map(window4_table).collect(); + let mut out = Self::IDENTITY; + + for byte_index in 0..32 { + out = double_n(out, 4); + for (table, scalar) in tables.iter().zip(scalars) { + out = out.add_mixed(table[(scalar[byte_index] >> 4) as usize]); + } + + out = double_n(out, 4); + for (table, scalar) in tables.iter().zip(scalars) { + out = out.add_mixed(table[(scalar[byte_index] & 0x0f) as usize]); + } + } + + Some(out) + } +} + +impl Add for ProjectivePoint { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self::Output { + if self.is_identity() { + return rhs; + } + if rhs.is_identity() { + return self; + } + + let z1z1 = self.z.square(); + let z2z2 = rhs.z.square(); + let u1 = self.x * z2z2; + let u2 = rhs.x * z1z1; + let s1 = self.y * rhs.z * z2z2; + let s2 = rhs.y * self.z * z1z1; + + if u1 == u2 { + return if s1 == s2 { + self.double() + } else { + Self::IDENTITY + }; + } + + let h = u2 - u1; + let i = double(h).square(); + let j = h * i; + let slope_num = double(s2 - s1); + let v = u1 * i; + let x = slope_num.square() - j - double(v); + let y = slope_num * (v - x) - double(s1 * j); + let z = ((self.z + rhs.z).square() - z1z1 - z2z2) * h; + + Self { x, y, z } + } +} + +impl Sub for ProjectivePoint { + type Output = Self; + + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + self + (-rhs) + } +} + +impl Neg for ProjectivePoint { + type Output = Self; + + #[inline] + fn neg(self) -> Self::Output { + Self { + x: self.x, + y: -self.y, + z: self.z, + } + } +} + +#[inline] +fn double(x: FieldElement) -> FieldElement { + x + x +} + +#[inline] +fn triple(x: FieldElement) -> FieldElement { + x + x + x +} + +fn generator_window8_table() -> &'static [[AffinePoint; BASE_WINDOW_POINTS]; BASE_WINDOWS] { + static TABLE: OnceLock> = + OnceLock::new(); + + TABLE + .get_or_init(|| build_window8_table(ProjectivePoint::GENERATOR)) + .as_ref() +} + +fn generator_window4_table() -> &'static [AffinePoint; SHAMIR_WINDOW_POINTS] { + static TABLE: OnceLock<[AffinePoint; SHAMIR_WINDOW_POINTS]> = OnceLock::new(); + + TABLE.get_or_init(|| window4_table(AffinePoint::GENERATOR)) +} + +fn window4_table(base: AffinePoint) -> [AffinePoint; SHAMIR_WINDOW_POINTS] { + let mut projective = [ProjectivePoint::IDENTITY; SHAMIR_WINDOW_POINTS]; + + for i in 1..SHAMIR_WINDOW_POINTS { + projective[i] = projective[i - 1].add_mixed(base); + } + + batch_normalize(projective) +} + +fn batch_normalize(points: [ProjectivePoint; N]) -> [AffinePoint; N] { + let mut products = [FieldElement::ONE; N]; + let mut acc = FieldElement::ONE; + + for (i, point) in points.iter().enumerate() { + products[i] = acc; + if !point.is_identity() { + acc = acc * point.z; + } + } + + let Some(mut acc_inverse) = acc.invert() else { + return [AffinePoint::IDENTITY; N]; + }; + let mut out = [AffinePoint::IDENTITY; N]; + + for i in (0..N).rev() { + let point = points[i]; + if point.is_identity() { + continue; + } + + let z_inverse = acc_inverse * products[i]; + acc_inverse = acc_inverse * point.z; + let z_inverse2 = z_inverse.square(); + out[i] = AffinePoint { + x: point.x * z_inverse2, + y: point.y * z_inverse2 * z_inverse, + infinity: false, + }; + } + + out +} + +#[inline] +fn double_n(mut point: ProjectivePoint, count: usize) -> ProjectivePoint { + for _ in 0..count { + point = point.double(); + } + + point +} + +fn build_window8_table( + mut base: ProjectivePoint, +) -> Box<[[AffinePoint; BASE_WINDOW_POINTS]; BASE_WINDOWS]> { + let mut rows = Vec::with_capacity(BASE_WINDOWS); + + for _ in 0..BASE_WINDOWS { + rows.push(projective_window8_table(base)); + + for _ in 0..8 { + base = base.double(); + } + } + + rows.into_boxed_slice() + .try_into() + .expect("fixed-point table has the expected number of rows") +} + +fn projective_window8_table(base: ProjectivePoint) -> [AffinePoint; BASE_WINDOW_POINTS] { + let mut projective = [ProjectivePoint::IDENTITY; BASE_WINDOW_POINTS]; + let mut multiple = ProjectivePoint::IDENTITY; + + for entry in projective.iter_mut().skip(1) { + multiple = multiple + base; + *entry = multiple; + } + + batch_normalize(projective) +} + +#[inline] +fn mul_window8_vartime( + table: &[[AffinePoint; BASE_WINDOW_POINTS]; BASE_WINDOWS], + scalar: &[u8; 32], +) -> ProjectivePoint { + let mut out = ProjectivePoint::IDENTITY; + + for (window, byte) in scalar.iter().rev().enumerate() { + out = out.add_mixed(table[window][*byte as usize]); + } + + out +} + +#[cfg(test)] +mod tests { + use super::{AffinePoint, ProjectivePoint}; + use crate::field::FieldElement; + use p256::{ + ProjectivePoint as P256ProjectivePoint, Scalar, + elliptic_curve::{ff::PrimeField, group::Group, sec1::ToEncodedPoint}, + }; + + const SCALAR: [u8; 32] = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, + 0x77, 0x88, + ]; + const SMALL_SCALAR: [u8; 32] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 5, + ]; + + fn assert_matches_p256(rust: ProjectivePoint, p256: P256ProjectivePoint) { + let rust_bytes = rust.to_uncompressed().unwrap(); + let p256_bytes = p256.to_affine().to_encoded_point(false); + assert_eq!(rust_bytes.as_slice(), p256_bytes.as_bytes()); + } + + #[test] + fn generator_matches_p256() { + assert_matches_p256( + ProjectivePoint::generator(), + P256ProjectivePoint::generator(), + ); + } + + #[test] + fn parses_and_serializes_generator() { + let bytes = ProjectivePoint::generator().to_uncompressed().unwrap(); + assert_eq!( + AffinePoint::from_uncompressed(bytes).unwrap(), + AffinePoint::generator() + ); + } + + #[test] + fn projective_equality_ignores_jacobian_scale() { + let point = ProjectivePoint::generator().double(); + let normalized = ProjectivePoint::from_affine(point.to_affine()); + assert_eq!(point, normalized); + + let z = FieldElement::from_u64(7); + let z2 = z.square(); + let scaled = ProjectivePoint { + x: point.x * z2, + y: point.y * z2 * z, + z: point.z * z, + }; + + assert_eq!(point, scaled); + assert_ne!(point, -point); + } + + #[test] + fn double_matches_p256() { + let p256 = P256ProjectivePoint::generator().double(); + assert_matches_p256(ProjectivePoint::generator().double(), p256); + } + + #[test] + fn add_matches_p256() { + let rust_g = ProjectivePoint::generator(); + let rust_2g = rust_g.double(); + let p256_g = P256ProjectivePoint::generator(); + let p256_2g = p256_g.double(); + + assert_matches_p256(rust_2g + rust_g, p256_2g + p256_g); + } + + #[test] + fn mixed_add_matches_p256() { + let rust_g = ProjectivePoint::generator(); + let rust_2g = rust_g.double(); + let p256_g = P256ProjectivePoint::generator(); + let p256_2g = p256_g.double(); + + assert_matches_p256( + rust_2g.add_mixed(AffinePoint::generator()), + p256_2g + p256_g, + ); + } + + #[test] + fn scalar_mul_matches_p256() { + let scalar = Option::::from(Scalar::from_repr(SCALAR.into())).unwrap(); + assert_matches_p256( + ProjectivePoint::generator().mul_scalar_vartime(SCALAR), + P256ProjectivePoint::generator() * scalar, + ); + } + + #[test] + fn fixed_base_scalar_mul_matches_p256() { + let scalar = Option::::from(Scalar::from_repr(SCALAR.into())).unwrap(); + assert_matches_p256( + ProjectivePoint::fixed_base_scalar_mul_vartime(SCALAR), + P256ProjectivePoint::generator() * scalar, + ); + } + + #[test] + fn double_scalar_mul_matches_p256() { + let scalar = Option::::from(Scalar::from_repr(SCALAR.into())).unwrap(); + let point = ProjectivePoint::generator().double().to_affine(); + let p256_point = P256ProjectivePoint::generator().double(); + + assert_matches_p256( + ProjectivePoint::double_scalar_mul_vartime(SCALAR, point, SCALAR), + (P256ProjectivePoint::generator() * scalar) + (p256_point * scalar), + ); + } + + #[test] + fn multi_scalar_mul_matches_p256() { + let scalar = Option::::from(Scalar::from_repr(SCALAR.into())).unwrap(); + let small_scalar = Option::::from(Scalar::from_repr(SMALL_SCALAR.into())).unwrap(); + let point = ProjectivePoint::generator().double().to_affine(); + let p256_point = P256ProjectivePoint::generator().double(); + + assert_matches_p256( + ProjectivePoint::multi_scalar_mul_vartime( + &[AffinePoint::generator(), point], + &[SCALAR, SMALL_SCALAR], + ) + .unwrap(), + (P256ProjectivePoint::generator() * scalar) + (p256_point * small_scalar), + ); + } + + #[test] + fn multi_scalar_mul_rejects_length_mismatch() { + assert!( + ProjectivePoint::multi_scalar_mul_vartime(&[AffinePoint::generator()], &[]).is_none() + ); + } +} diff --git a/secp256r1/src/lib.rs b/secp256r1/src/lib.rs new file mode 100644 index 0000000..0d1711e --- /dev/null +++ b/secp256r1/src/lib.rs @@ -0,0 +1,29 @@ +//! secp256r1/P-256 field, scalar, and group operations. +//! +//! This crate implements low-level arithmetic for the NIST P-256 +//! (secp256r1) curve in pure Rust, with no C dependencies in the library. +//! It is designed for benchmarking, experimentation, and public-input syscall +//! plumbing. +//! +//! # Scope +//! +//! The public modules are: +//! +//! - [`field`] for arithmetic modulo the P-256 base field. +//! - [`scalar`] for arithmetic modulo the P-256 group order. +//! - [`group`] for affine/projective points, fixed-length point parsing, scalar +//! multiplication, double-scalar multiplication, and variable-time +//! multiscalar multiplication. +//! +//! # Security +//! +//! This crate is experimental and has not been audited. Group scalar +//! multiplication APIs are variable time and intended for public inputs. Do not +//! use them with secret scalars in environments where local timing/cache side +//! channels are in scope. + +#![forbid(unsafe_code)] + +pub mod field; +pub mod group; +pub mod scalar; diff --git a/secp256r1/src/scalar.rs b/secp256r1/src/scalar.rs new file mode 100644 index 0000000..43cfaa1 --- /dev/null +++ b/secp256r1/src/scalar.rs @@ -0,0 +1,561 @@ +//! P-256 scalar field arithmetic. +//! +//! [`Scalar`] represents an element of GF(n) where +//! `n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551` +//! is the P-256 group order. Internally scalars are stored in Montgomery +//! form; use [`from_be_bytes`][Scalar::from_be_bytes] and +//! [`to_be_bytes`][Scalar::to_be_bytes] to convert from/to canonical +//! big-endian representation. + +use core::ops::{Add, Mul, Neg, Sub}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Scalar { + limbs: [u64; 4], +} + +const MODULUS: [u64; 4] = [ + 0xf3b9_cac2_fc63_2551, + 0xbce6_faad_a717_9e84, + 0xffff_ffff_ffff_ffff, + 0xffff_ffff_0000_0000, +]; + +const MODULUS_INV: u64 = 0xccd1_c8aa_ee00_bc4f; + +const R2: [u64; 4] = [ + 0x8324_4c95_be79_eea2, + 0x4699_799c_49bd_6fa6, + 0x2845_b239_2b6b_ec59, + 0x66e1_2d94_f3d9_5620, +]; + +const MODULUS_MINUS_TWO: [u8; 32] = [ + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xbc, 0xe6, 0xfa, 0xad, 0xa7, 0x17, 0x9e, 0x84, 0xf3, 0xb9, 0xca, 0xc2, 0xfc, 0x63, 0x25, 0x4f, +]; +const INVERT_WINDOW: usize = 5; +const INVERT_TABLE_POINTS: usize = 1 << (INVERT_WINDOW - 1); + +impl Scalar { + pub const ZERO: Self = Self { limbs: [0; 4] }; + pub const ONE: Self = Self { + limbs: [ + 0x0c46_353d_039c_daaf, + 0x4319_0552_58e8_617b, + 0x0000_0000_0000_0000, + 0x0000_0000_ffff_ffff, + ], + }; + + #[inline] + pub fn from_be_bytes(bytes: [u8; 32]) -> Option { + Self::from_canonical_limbs(limbs_from_be_bytes(bytes)) + } + + #[inline] + pub fn from_be_bytes_reduced(bytes: [u8; 32]) -> Self { + let mut limbs = limbs_from_be_bytes(bytes); + + if ge_limbs(limbs, MODULUS) { + limbs = sub_limbs(limbs, MODULUS).0; + } + + Self { + limbs: montgomery_mul(limbs, R2), + } + } + + #[inline] + pub fn to_be_bytes(self) -> [u8; 32] { + be_bytes_from_limbs(from_montgomery(self.limbs)) + } + + #[inline] + pub fn is_zero(self) -> bool { + self == Self::ZERO + } + + #[inline] + pub fn square(self) -> Self { + Self { + limbs: montgomery_square(self.limbs), + } + } + + #[inline] + pub fn invert(self) -> Option { + if self.is_zero() { + return None; + } + + let table = odd_powers(self); + let mut out = Self::ONE; + let mut bit = 255isize; + + while bit >= 0 { + if !modulus_minus_two_bit(bit as usize) { + out = out.square(); + bit -= 1; + continue; + } + + let mut width = INVERT_WINDOW.min(bit as usize + 1); + while width > 1 && !modulus_minus_two_bit(bit as usize + 1 - width) { + width -= 1; + } + + let low = bit as usize + 1 - width; + let mut value = 0usize; + for i in (low..=bit as usize).rev() { + value = (value << 1) | usize::from(modulus_minus_two_bit(i)); + } + + for _ in 0..width { + out = out.square(); + } + out = out * table[value >> 1]; + bit -= width as isize; + } + + Some(out) + } + + #[inline] + fn from_canonical_limbs(limbs: [u64; 4]) -> Option { + if ge_limbs(limbs, MODULUS) { + None + } else { + Some(Self { + limbs: montgomery_mul(limbs, R2), + }) + } + } +} + +impl Add for Scalar { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self::Output { + let (sum, carry) = add_limbs(self.limbs, rhs.limbs); + Self { + limbs: reduce_sum(sum, carry), + } + } +} + +impl Sub for Scalar { + type Output = Self; + + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + let (difference, borrow) = sub_limbs(self.limbs, rhs.limbs); + let (corrected, _) = add_limbs(difference, MODULUS); + + Self { + limbs: if borrow == 0 { difference } else { corrected }, + } + } +} + +impl Mul for Scalar { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Self { + limbs: montgomery_mul(self.limbs, rhs.limbs), + } + } +} + +impl Neg for Scalar { + type Output = Self; + + #[inline] + fn neg(self) -> Self::Output { + Self::ZERO - self + } +} + +#[inline] +fn odd_powers(value: Scalar) -> [Scalar; INVERT_TABLE_POINTS] { + let mut table = [Scalar::ZERO; INVERT_TABLE_POINTS]; + table[0] = value; + + let value_squared = value.square(); + for i in 1..INVERT_TABLE_POINTS { + table[i] = table[i - 1] * value_squared; + } + + table +} + +#[inline(always)] +fn modulus_minus_two_bit(bit: usize) -> bool { + ((MODULUS_MINUS_TWO[31 - bit / 8] >> (bit % 8)) & 1) == 1 +} + +#[inline(always)] +fn add_limbs(a: [u64; 4], b: [u64; 4]) -> ([u64; 4], u64) { + let mut out = [0; 4]; + let mut carry = 0u64; + + for i in 0..4 { + let (sum, carry1) = a[i].overflowing_add(b[i]); + let (sum, carry2) = sum.overflowing_add(carry); + out[i] = sum; + carry = u64::from(carry1 | carry2); + } + + (out, carry) +} + +#[inline(always)] +fn sub_limbs(a: [u64; 4], b: [u64; 4]) -> ([u64; 4], u64) { + let mut out = [0; 4]; + let mut borrow = 0u64; + + for i in 0..4 { + let (difference, borrow1) = a[i].overflowing_sub(b[i]); + let (difference, borrow2) = difference.overflowing_sub(borrow); + out[i] = difference; + borrow = u64::from(borrow1 | borrow2); + } + + (out, borrow) +} + +#[inline(always)] +fn reduce_sum(sum: [u64; 4], carry: u64) -> [u64; 4] { + let (reduced, borrow) = sub_limbs(sum, MODULUS); + + if carry != 0 || borrow == 0 { + reduced + } else { + sum + } +} + +#[inline(always)] +fn ge_limbs(a: [u64; 4], b: [u64; 4]) -> bool { + sub_limbs(a, b).1 == 0 +} + +#[inline(always)] +fn mul_wide(a: [u64; 4], b: [u64; 4]) -> [u64; 8] { + let (w0, carry) = mac(0, a[0], b[0], 0); + let (w1, carry) = mac(0, a[0], b[1], carry); + let (w2, carry) = mac(0, a[0], b[2], carry); + let (w3, w4) = mac(0, a[0], b[3], carry); + + let (w1, carry) = mac(w1, a[1], b[0], 0); + let (w2, carry) = mac(w2, a[1], b[1], carry); + let (w3, carry) = mac(w3, a[1], b[2], carry); + let (w4, w5) = mac(w4, a[1], b[3], carry); + + let (w2, carry) = mac(w2, a[2], b[0], 0); + let (w3, carry) = mac(w3, a[2], b[1], carry); + let (w4, carry) = mac(w4, a[2], b[2], carry); + let (w5, w6) = mac(w5, a[2], b[3], carry); + + let (w3, carry) = mac(w3, a[3], b[0], 0); + let (w4, carry) = mac(w4, a[3], b[1], carry); + let (w5, carry) = mac(w5, a[3], b[2], carry); + let (w6, w7) = mac(w6, a[3], b[3], carry); + + [w0, w1, w2, w3, w4, w5, w6, w7] +} + +#[inline(always)] +fn montgomery_mul(a: [u64; 4], b: [u64; 4]) -> [u64; 4] { + montgomery_reduce(mul_wide(a, b)) +} + +#[inline(always)] +fn montgomery_square(a: [u64; 4]) -> [u64; 4] { + let a0 = a[0] as u128; + let a1 = a[1] as u128; + let a2 = a[2] as u128; + let a3 = a[3] as u128; + + let p00 = a0 * a0; + let w0 = p00 as u64; + let mut acc = p00 >> 64; + let mut top = 0u64; + + let p01 = a0 * a1; + top += (p01 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p01 << 1); + let w1 = sum as u64; + acc = sum >> 64; + top += u64::from(overflow); + + acc |= (top as u128) << 64; + top = 0; + let p02 = a0 * a2; + top += (p02 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p02 << 1); + acc = sum; + top += u64::from(overflow); + let (sum, overflow) = acc.overflowing_add(a1 * a1); + let w2 = sum as u64; + acc = sum >> 64; + top += u64::from(overflow); + + acc |= (top as u128) << 64; + top = 0; + let p03 = a0 * a3; + top += (p03 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p03 << 1); + acc = sum; + top += u64::from(overflow); + let p12 = a1 * a2; + top += (p12 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p12 << 1); + let w3 = sum as u64; + acc = sum >> 64; + top += u64::from(overflow); + + acc |= (top as u128) << 64; + top = 0; + let p13 = a1 * a3; + top += (p13 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p13 << 1); + acc = sum; + top += u64::from(overflow); + let (sum, overflow) = acc.overflowing_add(a2 * a2); + let w4 = sum as u64; + acc = sum >> 64; + top += u64::from(overflow); + + acc |= (top as u128) << 64; + top = 0; + let p23 = a2 * a3; + top += (p23 >> 127) as u64; + let (sum, overflow) = acc.overflowing_add(p23 << 1); + let w5 = sum as u64; + acc = sum >> 64; + top += u64::from(overflow); + + acc |= (top as u128) << 64; + let (sum, overflow) = acc.overflowing_add(a3 * a3); + let w6 = sum as u64; + let w7 = (sum >> 64) as u64; + debug_assert!(!overflow); + + montgomery_reduce_words(w0, w1, w2, w3, w4, w5, w6, w7) +} + +#[inline(always)] +fn from_montgomery(a: [u64; 4]) -> [u64; 4] { + montgomery_reduce([a[0], a[1], a[2], a[3], 0, 0, 0, 0]) +} + +#[inline(always)] +fn montgomery_reduce(input: [u64; 8]) -> [u64; 4] { + montgomery_reduce_words( + input[0], input[1], input[2], input[3], input[4], input[5], input[6], input[7], + ) +} + +#[inline(always)] +#[allow(clippy::too_many_arguments)] +fn montgomery_reduce_words( + r0: u64, + r1: u64, + r2: u64, + r3: u64, + r4: u64, + r5: u64, + r6: u64, + r7: u64, +) -> [u64; 4] { + let k = r0.wrapping_mul(MODULUS_INV); + let (_, carry) = mac(r0, k, MODULUS[0], 0); + let (r1, carry) = mac(r1, k, MODULUS[1], carry); + let (r2, carry) = mac(r2, k, MODULUS[2], carry); + let (r3, carry) = mac(r3, k, MODULUS[3], carry); + let (r4, carry2) = adc(r4, 0, carry); + + let k = r1.wrapping_mul(MODULUS_INV); + let (_, carry) = mac(r1, k, MODULUS[0], 0); + let (r2, carry) = mac(r2, k, MODULUS[1], carry); + let (r3, carry) = mac(r3, k, MODULUS[2], carry); + let (r4, carry) = mac(r4, k, MODULUS[3], carry); + let (r5, carry2) = adc(r5, carry2, carry); + + let k = r2.wrapping_mul(MODULUS_INV); + let (_, carry) = mac(r2, k, MODULUS[0], 0); + let (r3, carry) = mac(r3, k, MODULUS[1], carry); + let (r4, carry) = mac(r4, k, MODULUS[2], carry); + let (r5, carry) = mac(r5, k, MODULUS[3], carry); + let (r6, carry2) = adc(r6, carry2, carry); + + let k = r3.wrapping_mul(MODULUS_INV); + let (_, carry) = mac(r3, k, MODULUS[0], 0); + let (r4, carry) = mac(r4, k, MODULUS[1], carry); + let (r5, carry) = mac(r5, k, MODULUS[2], carry); + let (r6, carry) = mac(r6, k, MODULUS[3], carry); + let (r7, r8) = adc(r7, carry2, carry); + + reduce_wide([r4, r5, r6, r7, r8]) +} + +#[inline(always)] +fn reduce_wide(value: [u64; 5]) -> [u64; 4] { + let (w0, borrow) = sbb(value[0], MODULUS[0], 0); + let (w1, borrow) = sbb(value[1], MODULUS[1], borrow); + let (w2, borrow) = sbb(value[2], MODULUS[2], borrow); + let (w3, borrow) = sbb(value[3], MODULUS[3], borrow); + let (_, borrow) = sbb(value[4], 0, borrow); + + if borrow == 0 { + [w0, w1, w2, w3] + } else { + [value[0], value[1], value[2], value[3]] + } +} + +#[inline(always)] +fn adc(a: u64, b: u64, carry: u64) -> (u64, u64) { + let sum = (a as u128) + (b as u128) + (carry as u128); + (sum as u64, (sum >> 64) as u64) +} + +#[inline(always)] +fn sbb(a: u64, b: u64, borrow: u64) -> (u64, u64) { + let difference = (a as u128).wrapping_sub((b as u128) + (borrow as u128)); + (difference as u64, u64::from((difference >> 127) != 0)) +} + +#[inline(always)] +fn mac(a: u64, b: u64, c: u64, carry: u64) -> (u64, u64) { + let product = (a as u128) + (b as u128) * (c as u128) + (carry as u128); + (product as u64, (product >> 64) as u64) +} + +#[inline] +fn limbs_from_be_bytes(bytes: [u8; 32]) -> [u64; 4] { + let mut limbs = [0u64; 4]; + + for (i, chunk) in bytes.chunks_exact(8).rev().enumerate() { + limbs[i] = u64::from_be_bytes(chunk.try_into().expect("chunk length is 8")); + } + + limbs +} + +#[inline] +fn be_bytes_from_limbs(limbs: [u64; 4]) -> [u8; 32] { + let mut bytes = [0u8; 32]; + + for (i, limb) in limbs.iter().rev().enumerate() { + bytes[i * 8..(i + 1) * 8].copy_from_slice(&limb.to_be_bytes()); + } + + bytes +} + +#[cfg(test)] +mod tests { + use super::Scalar; + use p256::{ + Scalar as P256Scalar, + elliptic_curve::{bigint::U256, ff::PrimeField, ops::Reduce}, + }; + + const A: [u8; 32] = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, + 0x77, 0x88, + ]; + const B: [u8; 32] = [ + 0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21, 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, + 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x80, 0x70, 0x60, 0x50, 0x40, 0x30, + 0x20, 0x10, + ]; + const N: [u8; 32] = [ + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xbc, 0xe6, 0xfa, 0xad, 0xa7, 0x17, 0x9e, 0x84, 0xf3, 0xb9, 0xca, 0xc2, 0xfc, 0x63, + 0x25, 0x51, + ]; + + fn p256_scalar(bytes: [u8; 32]) -> P256Scalar { + Option::from(P256Scalar::from_repr(bytes.into())).unwrap() + } + + fn assert_matches_p256(rust: Scalar, p256: P256Scalar) { + let p256_bytes: [u8; 32] = p256.to_repr().into(); + assert_eq!(rust.to_be_bytes(), p256_bytes); + } + + fn sample(mut seed: u64) -> [u8; 32] { + let mut bytes = [0u8; 32]; + + for chunk in bytes.chunks_exact_mut(8) { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + chunk.copy_from_slice(&seed.to_be_bytes()); + } + + bytes[0] &= 0x7f; + bytes + } + + #[test] + fn rejects_non_canonical_order() { + assert!(Scalar::from_be_bytes(N).is_none()); + } + + #[test] + fn round_trips() { + for bytes in [[0u8; 32], A, B] { + assert_eq!(Scalar::from_be_bytes(bytes).unwrap().to_be_bytes(), bytes); + } + } + + #[test] + fn arithmetic_matches_p256() { + let a = Scalar::from_be_bytes(A).unwrap(); + let b = Scalar::from_be_bytes(B).unwrap(); + let p256_a = p256_scalar(A); + let p256_b = p256_scalar(B); + + assert_matches_p256(a + b, p256_a + p256_b); + assert_matches_p256(a - b, p256_a - p256_b); + assert_matches_p256(a * b, p256_a * p256_b); + assert_matches_p256(a.square(), p256_a.square()); + assert_matches_p256(a.invert().unwrap(), Option::from(p256_a.invert()).unwrap()); + } + + #[test] + fn reduced_bytes_match_p256() { + let rust = Scalar::from_be_bytes_reduced(N); + let p256 = P256Scalar::reduce(U256::from_be_slice(&N)); + assert_matches_p256(rust, p256); + } + + #[test] + fn arithmetic_matches_p256_for_many_samples() { + for i in 1..128 { + let a = sample(i); + let b = sample(i ^ 0xa5a5_a5a5_a5a5_a5a5); + let rust_a = Scalar::from_be_bytes(a).unwrap(); + let rust_b = Scalar::from_be_bytes(b).unwrap(); + let p256_a = p256_scalar(a); + let p256_b = p256_scalar(b); + + assert_matches_p256(rust_a + rust_b, p256_a + p256_b); + assert_matches_p256(rust_a - rust_b, p256_a - p256_b); + assert_matches_p256(rust_a * rust_b, p256_a * p256_b); + assert_matches_p256(rust_a.square(), p256_a.square()); + assert_matches_p256( + rust_a.invert().unwrap(), + Option::from(p256_a.invert()).unwrap(), + ); + } + } +}