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
This commit is contained in:
zz-sol 2026-06-17 09:45:41 -04:00 committed by GitHub
parent 06cc7111db
commit dd4164eefe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 2923 additions and 0 deletions

141
Cargo.lock generated
View file

@ -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"

View file

@ -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"

27
secp256r1/Cargo.toml Normal file
View file

@ -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

125
secp256r1/README.md Normal file
View file

@ -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.

179
secp256r1/benches/field.rs Normal file
View file

@ -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);

331
secp256r1/benches/group.rs Normal file
View file

@ -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::<Scalar>::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<AffinePoint>, 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<P256ProjectivePoint>, Vec<Scalar>) {
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::<Scalar>::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);

115
secp256r1/benches/scalar.rs Normal file
View file

@ -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);

665
secp256r1/src/field.rs Normal file
View file

@ -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> {
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<Self> {
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<Self> {
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<Self> {
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",
);
}
}
}
}

746
secp256r1/src/group.rs Normal file
View file

@ -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<Self> {
let point = Self {
x,
y,
infinity: false,
};
point.is_on_curve().then_some(point)
}
#[inline]
pub fn from_uncompressed(bytes: [u8; 65]) -> Option<Self> {
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<Self> {
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<FieldElement> {
(!self.infinity).then_some(self.x)
}
#[inline]
pub fn y(self) -> Option<FieldElement> {
(!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<Self> {
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<Box<[[AffinePoint; BASE_WINDOW_POINTS]; BASE_WINDOWS]>> =
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<const N: usize>(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::<Scalar>::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::<Scalar>::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::<Scalar>::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::<Scalar>::from(Scalar::from_repr(SCALAR.into())).unwrap();
let small_scalar = Option::<Scalar>::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()
);
}
}

29
secp256r1/src/lib.rs Normal file
View file

@ -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;

561
secp256r1/src/scalar.rs Normal file
View file

@ -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> {
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<Self> {
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<Self> {
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(),
);
}
}
}