From 1554844e9bde7e6821288b22402ebec0dfab8291 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Thu, 11 Jun 2026 09:32:51 -0400 Subject: [PATCH 1/2] impl secp256r1 --- Cargo.lock | 187 ++++++++ Cargo.toml | 4 + secp256r1/Cargo.toml | 41 ++ secp256r1/README.md | 204 +++++++++ secp256r1/benches/field.rs | 189 ++++++++ secp256r1/benches/group.rs | 341 ++++++++++++++ secp256r1/benches/scalar.rs | 113 +++++ secp256r1/benches/sign.rs | 186 ++++++++ secp256r1/benches/verify.rs | 280 ++++++++++++ secp256r1/src/constants.rs | 8 + secp256r1/src/ecdsa.rs | 647 +++++++++++++++++++++++++++ secp256r1/src/error.rs | 32 ++ secp256r1/src/field.rs | 569 ++++++++++++++++++++++++ secp256r1/src/group.rs | 864 ++++++++++++++++++++++++++++++++++++ secp256r1/src/lib.rs | 105 +++++ secp256r1/src/scalar.rs | 569 ++++++++++++++++++++++++ secp256r1/tests/ecdsa.rs | 513 +++++++++++++++++++++ 17 files changed, 4852 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/benches/sign.rs create mode 100644 secp256r1/benches/verify.rs create mode 100644 secp256r1/src/constants.rs create mode 100644 secp256r1/src/ecdsa.rs create mode 100644 secp256r1/src/error.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 create mode 100644 secp256r1/tests/ecdsa.rs diff --git a/Cargo.lock b/Cargo.lock index 05a8cd6..31eaa5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,6 +213,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" @@ -531,6 +537,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" @@ -595,7 +613,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common 0.1.7", + "subtle", ] [[package]] @@ -608,6 +628,20 @@ dependencies = [ "crypto-common 0.2.1", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -673,6 +707,26 @@ 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", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -760,6 +814,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" @@ -798,6 +867,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -913,6 +983,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "home" version = "0.5.12" @@ -1130,12 +1209,61 @@ 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 = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "p3-air" version = "0.4.3" @@ -1438,6 +1566,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" @@ -1485,6 +1619,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" @@ -1664,6 +1807,16 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -1738,6 +1891,33 @@ 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", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256r1" +version = "0.1.0" +dependencies = [ + "criterion", + "hmac", + "openssl", + "p256", + "rand_core 0.6.4", + "sha2 0.10.9", + "zeroize", +] + [[package]] name = "semver" version = "1.0.28" @@ -1830,6 +2010,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest 0.10.7", "rand_core 0.6.4", ] @@ -2116,6 +2297,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 897c768..5957925 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", features = ["ecdsa", "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..eafd68c --- /dev/null +++ b/secp256r1/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "secp256r1" +description = "Pure-Rust secp256r1/P-256 ECDSA keys, signatures, signing, and verification" +version = "0.1.0" +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = "2024" +readme = "README.md" + +[dependencies] +hmac.workspace = true +rand_core = { workspace = true, features = ["getrandom"] } +sha2 = "0.10" +zeroize.workspace = true + +[dev-dependencies] +criterion.workspace = true +openssl.workspace = true +p256.workspace = true + +[[bench]] +name = "verify" +harness = false + +[[bench]] +name = "field" +harness = false + +[[bench]] +name = "group" +harness = false + +[[bench]] +name = "scalar" +harness = false + +[[bench]] +name = "sign" +harness = false diff --git a/secp256r1/README.md b/secp256r1/README.md new file mode 100644 index 0000000..2eb049d --- /dev/null +++ b/secp256r1/README.md @@ -0,0 +1,204 @@ +# secp256r1 + +Pure-Rust secp256r1/P-256 ECDSA keys, signatures, signing, and verification. + +The public API is intentionally close to RustCrypto `p256` for the common flows: +`SigningKey`, `VerifyingKey`, `Signature`, SEC1 public keys, fixed-width +signatures, DER signatures, message signing with SHA-256, and prehashed +verification. + +## Status + +This crate is performance-oriented and experimental. It has not been audited. +Do not treat it as production cryptography without independent review. +Signing and signing-key import use variable-time scalar multiplication for +secret-dependent values. This is acceptable for local benchmarking, but not for +production signing or side-channel-exposed environments. +`SigningKey` and RFC6979 nonce state are zeroized on drop, but callers are +responsible for clearing secret byte arrays returned by APIs such as +`SigningKey::to_bytes`. + +Current scope: + +- secp256r1/P-256 ECDSA +- SHA-256 message signing and verification +- 32-byte prehash signing and verification +- deterministic RFC6979/SHA-256 signing nonces +- compressed and uncompressed SEC1 public-key input +- compressed and uncompressed SEC1 public-key output +- fixed-width `r || s` and DER signature encodings + +OpenSSL and `p256` are used only as dev/benchmark comparison dependencies. + +## Installation + +```toml +[dependencies] +secp256r1 = { path = "." } +``` + +For key generation examples using `OsRng`, add: + +```toml +rand_core = { version = "0.6", features = ["getrandom"] } +``` + +For the prehash examples below, add: + +```toml +sha2 = "0.10" +``` + +## API + +```rust +use secp256r1::{Signature, SigningKey, VerifyingKey}; +``` + +### Generate a key and sign + +```rust +use rand_core::OsRng; +use secp256r1::SigningKey; + +let mut rng = OsRng; +let signing_key = SigningKey::random(&mut rng); +let verifying_key = signing_key.verifying_key(); + +let message = b"message"; +let signature = signing_key.sign(message); + +verifying_key.verify(message, &signature).unwrap(); +``` + +### Load a signing key from bytes + +```rust +use secp256r1::SigningKey; + +let secret = [7u8; 32]; +let signing_key = SigningKey::from_slice(&secret).unwrap(); +let signature = signing_key.sign(b"message"); + +assert!(signing_key.verifying_key().verify(b"message", &signature).is_ok()); +``` + +### SEC1 public keys + +```rust +use secp256r1::{SigningKey, VerifyingKey}; + +let signing_key = SigningKey::from_slice(&[7u8; 32]).unwrap(); +let public_key_sec1: Vec = signing_key + .verifying_key() + .to_encoded_point(false) + .as_bytes() + .to_vec(); + +let verifying_key = VerifyingKey::from_sec1_bytes(&public_key_sec1).unwrap(); +``` + +`from_sec1_bytes` accepts compressed and uncompressed SEC1 public keys. Use +`to_encoded_point(true)` to emit compressed SEC1 public keys. + +### Fixed-width and DER signatures + +```rust +use secp256r1::{Signature, SigningKey}; + +let signing_key = SigningKey::from_slice(&[7u8; 32]).unwrap(); +let signature = signing_key.sign(b"message"); + +let fixed = signature.to_bytes(); // 64 bytes: r || s +let reparsed = Signature::from_slice(&fixed).unwrap(); + +let der = signature.to_der(); +let reparsed_der = Signature::from_der(der.as_bytes()).unwrap(); + +assert_eq!(signature, reparsed); +assert_eq!(signature, reparsed_der); +``` + +### Prehashed signing and verification + +```rust +use secp256r1::SigningKey; +use sha2::{Digest as _, Sha256}; + +let signing_key = SigningKey::from_slice(&[7u8; 32]).unwrap(); +let digest = Sha256::digest(b"message"); + +let signature = signing_key.sign_prehash(&digest).unwrap(); +signing_key + .verifying_key() + .verify_prehash(&digest, &signature) + .unwrap(); +``` + +Prehash APIs require exactly 32 bytes. + +## Benchmarks + +Run all benchmarks: + +```sh +cargo bench +``` + +Focused benchmark groups: + +```sh +cargo bench --bench verify +cargo bench --bench sign +cargo bench --bench field +cargo bench --bench scalar +cargo bench --bench group +``` + +Representative local results from this workspace: + +### Verify + +| Benchmark | rust | p256 | OpenSSL | +|---|---:|---:|---:| +| prehashed, parsed sig | 35.842 us | 154.61 us | 30.896 us | +| prehashed, DER sig | 36.149 us | 153.55 us | 31.326 us | +| message SHA-256, parsed sig | 36.450 us | 154.69 us | 31.344 us | +| message SHA-256, DER sig | 36.622 us | 155.57 us | 31.842 us | + +### Sign + +| Benchmark | rust | p256 | OpenSSL | +|---|---:|---:|---:| +| keygen | 8.423 us | 78.101 us | 4.762 us | +| sign prehashed | 12.674 us | 94.367 us | 10.476 us | +| sign message SHA-256 | 12.869 us | 94.662 us | 10.753 us | + +### Group Ops + +| Benchmark | rust | p256 | OpenSSL | +|---|---:|---:|---:| +| point double | 89.851 ns | 210.85 ns | 57.582 ns nistz | +| point add | 141.95 ns | 229.71 ns | 99.978 ns nistz | +| mixed add | 101.93 ns | 205.46 ns | 75.010 ns nistz | +| base scalar mul | 3.236 us fixed-base window8 | 76.277 us | 3.543 us | +| double scalar mul | 32.682 us separate wNAF6 | 151.99 us | 25.704 us | + +Benchmark numbers are machine- and compiler-dependent. Re-run locally before +making performance decisions. + +## Design Notes + +- `SigningKey::sign` hashes messages with SHA-256. +- `SigningKey::sign_prehash` signs a caller-provided 32-byte digest. +- `VerifyingKey::verify` hashes messages with SHA-256. +- `VerifyingKey::verify_prehash` verifies a caller-provided 32-byte digest. +- `Signature::from_der` enforces strict/minimal DER encodings. +- `field`, `scalar`, and `group` are exposed for low-level benchmarking and + experimentation. The stable public surface should be considered the ECDSA key + and signature API. + +## Safety + +The crate forbids `unsafe` in library code. Benchmark code links to OpenSSL +internal/public routines 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..65d8d66 --- /dev/null +++ b/secp256r1/benches/field.rs @@ -0,0 +1,189 @@ +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +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, +]; + +#[link(name = "crypto")] +unsafe extern "C" { + fn ecp_nistz256_to_mont(res: *mut u64, input: *const u64); + fn ecp_nistz256_add(res: *mut u64, a: *const u64, b: *const u64); + fn ecp_nistz256_sub(res: *mut u64, a: *const u64, b: *const u64); + fn ecp_nistz256_mul_mont(res: *mut u64, a: *const u64, b: *const u64); + fn ecp_nistz256_sqr_mont(res: *mut u64, a: *const u64); +} + +#[derive(Clone, Copy)] +struct Fixture { + rust_a: FieldElement, + rust_b: FieldElement, + openssl_a: [u64; 4], + openssl_b: [u64; 4], + p256_a: P256FieldElement, + p256_b: P256FieldElement, +} + +impl Fixture { + fn new() -> Self { + let raw_a = limbs_from_be_bytes(A); + let raw_b = limbs_from_be_bytes(B); + let mut openssl_a = [0u64; 4]; + let mut openssl_b = [0u64; 4]; + + unsafe { + ecp_nistz256_to_mont(openssl_a.as_mut_ptr(), raw_a.as_ptr()); + ecp_nistz256_to_mont(openssl_b.as_mut_ptr(), raw_b.as_ptr()); + } + + Self { + rust_a: FieldElement::from_be_bytes(A).unwrap(), + rust_b: FieldElement::from_be_bytes(B).unwrap(), + openssl_a, + openssl_b, + 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 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().unwrap()); + } + + limbs +} + +fn bench_field_add(c: &mut Criterion) { + let fixture = Fixture::new(); + 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_ecp_nistz256", |b| { + b.iter(|| { + let mut out = [0u64; 4]; + unsafe { + ecp_nistz256_add( + out.as_mut_ptr(), + black_box(fixture.openssl_a).as_ptr(), + black_box(fixture.openssl_b).as_ptr(), + ); + } + black_box(out) + }); + }); + + group.finish(); +} + +fn bench_field_sub(c: &mut Criterion) { + let fixture = Fixture::new(); + 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_ecp_nistz256", |b| { + b.iter(|| { + let mut out = [0u64; 4]; + unsafe { + ecp_nistz256_sub( + out.as_mut_ptr(), + black_box(fixture.openssl_a).as_ptr(), + black_box(fixture.openssl_b).as_ptr(), + ); + } + black_box(out) + }); + }); + + group.finish(); +} + +fn bench_field_mul(c: &mut Criterion) { + let fixture = Fixture::new(); + 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_ecp_nistz256", |b| { + b.iter(|| { + let mut out = [0u64; 4]; + unsafe { + ecp_nistz256_mul_mont( + out.as_mut_ptr(), + black_box(fixture.openssl_a).as_ptr(), + black_box(fixture.openssl_b).as_ptr(), + ); + } + black_box(out) + }); + }); + + group.finish(); +} + +fn bench_field_square(c: &mut Criterion) { + let fixture = Fixture::new(); + 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_ecp_nistz256", |b| { + b.iter(|| { + let mut out = [0u64; 4]; + unsafe { + ecp_nistz256_sqr_mont(out.as_mut_ptr(), black_box(fixture.openssl_a).as_ptr()); + } + black_box(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..b89efde --- /dev/null +++ b/secp256r1/benches/group.rs @@ -0,0 +1,341 @@ +use criterion::{Criterion, black_box, 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::{ + field::FieldElement, + group::{AffinePoint, ProjectivePoint}, +}; + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct NistzPoint { + x: [u64; 4], + y: [u64; 4], + z: [u64; 4], +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct NistzAffinePoint { + x: [u64; 4], + y: [u64; 4], +} + +#[link(name = "crypto")] +unsafe extern "C" { + fn ecp_nistz256_point_double(res: *mut NistzPoint, a: *const NistzPoint); + fn ecp_nistz256_point_add(res: *mut NistzPoint, a: *const NistzPoint, b: *const NistzPoint); + fn ecp_nistz256_point_add_affine( + res: *mut NistzPoint, + a: *const NistzPoint, + b: *const NistzAffinePoint, + ); +} + +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, +]; + +#[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 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 openssl_nistz_fixture() -> (NistzPoint, NistzPoint, NistzAffinePoint) { + let affine = AffinePoint::generator(); + let generator = NistzPoint { + x: affine.x().unwrap().montgomery_limbs(), + y: affine.y().unwrap().montgomery_limbs(), + z: FieldElement::ONE.montgomery_limbs(), + }; + let affine_generator = NistzAffinePoint { + x: generator.x, + y: generator.y, + }; + let mut double_generator = NistzPoint::default(); + + unsafe { + ecp_nistz256_point_double(&mut double_generator, &generator); + } + + (generator, double_generator, affine_generator) +} + +fn bench_group_double(c: &mut Criterion) { + let fixture = Fixture::new(); + let (openssl_group, mut openssl_context, openssl_g, _) = openssl_fixture(); + let (nistz_g, _, _) = openssl_nistz_fixture(); + let mut openssl_out = EcPoint::new(&openssl_group).unwrap(); + let mut nistz_out = NistzPoint::default(); + 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.bench_function("openssl_nistz256_point_double", |b| { + b.iter(|| unsafe { + ecp_nistz256_point_double(&mut nistz_out, black_box(&nistz_g)); + black_box(nistz_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 (nistz_g, nistz_2g, _) = openssl_nistz_fixture(); + let mut openssl_out = EcPoint::new(&openssl_group).unwrap(); + let mut nistz_out = NistzPoint::default(); + 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.bench_function("openssl_nistz256_point_add", |b| { + b.iter(|| unsafe { + ecp_nistz256_point_add(&mut nistz_out, black_box(&nistz_2g), black_box(&nistz_g)); + black_box(nistz_out); + }); + }); + + group.finish(); +} + +fn bench_group_mixed_add(c: &mut Criterion) { + let fixture = Fixture::new(); + let (_, nistz_2g, nistz_affine_g) = openssl_nistz_fixture(); + let mut nistz_out = NistzPoint::default(); + 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.bench_function("openssl_nistz256_point_add_affine", |b| { + b.iter(|| unsafe { + ecp_nistz256_point_add_affine( + &mut nistz_out, + black_box(&nistz_2g), + black_box(&nistz_affine_g), + ); + black_box(nistz_out); + }); + }); + + 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(); + black_box(ProjectivePoint::mul_generator_vartime(SCALAR)); + let mut group = c.benchmark_group("secp256r1_group_base_scalar_mul"); + + group.bench_function("rust_vartime_window4", |b| { + b.iter(|| black_box(fixture.rust_g).mul_scalar_vartime(black_box(SCALAR))); + }); + + group.bench_function("rust_wnaf5_projective", |b| { + b.iter(|| black_box(fixture.rust_g).mul_scalar_wnaf5_vartime(black_box(SCALAR))); + }); + + group.bench_function("rust_wnaf6_projective", |b| { + b.iter(|| black_box(fixture.rust_g).mul_scalar_wnaf6_vartime(black_box(SCALAR))); + }); + + group.bench_function("rust_fixed_base_window8", |b| { + b.iter(|| ProjectivePoint::mul_generator_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 (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::mul_generator_vartime(black_box(SCALAR)) + + ProjectivePoint::from_affine(black_box(rust_q)) + .mul_scalar_vartime(black_box(SCALAR)) + }); + }); + + group.bench_function("rust_separate_wnaf5_q", |b| { + b.iter(|| { + ProjectivePoint::mul_generator_vartime(black_box(SCALAR)) + + ProjectivePoint::from_affine(black_box(rust_q)) + .mul_scalar_wnaf5_vartime(black_box(SCALAR)) + }); + }); + + group.bench_function("rust_separate_wnaf6_q", |b| { + b.iter(|| { + ProjectivePoint::mul_generator_vartime(black_box(SCALAR)) + + ProjectivePoint::from_affine(black_box(rust_q)) + .mul_scalar_wnaf6_vartime(black_box(SCALAR)) + }); + }); + + group.bench_function("rust_separate_mixed_q", |b| { + b.iter(|| { + ProjectivePoint::mul_generator_vartime(black_box(SCALAR)) + + ProjectivePoint::mul_affine_scalar_vartime(black_box(rust_q), black_box(SCALAR)) + }); + }); + + group.bench_function("rust_shamir_window4", |b| { + b.iter(|| { + ProjectivePoint::double_scalar_mul_shamir_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(); +} + +criterion_group!( + benches, + bench_group_double, + bench_group_add, + bench_group_mixed_add, + bench_group_base_scalar_mul, + bench_group_double_scalar_mul +); +criterion_main!(benches); diff --git a/secp256r1/benches/scalar.rs b/secp256r1/benches/scalar.rs new file mode 100644 index 0000000..e20b991 --- /dev/null +++ b/secp256r1/benches/scalar.rs @@ -0,0 +1,113 @@ +use criterion::{Criterion, black_box, 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/benches/sign.rs b/secp256r1/benches/sign.rs new file mode 100644 index 0000000..7b540ad --- /dev/null +++ b/secp256r1/benches/sign.rs @@ -0,0 +1,186 @@ +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use openssl::{ + bn::{BigNum, BigNumContext}, + ec::{EcGroup, EcKey, EcPoint}, + ecdsa::EcdsaSig, + nid::Nid, + pkey::Private, +}; +use p256::ecdsa::{ + Signature as P256Signature, SigningKey as P256SigningKey, + signature::{Signer as _, hazmat::PrehashSigner}, +}; +use rand_core::{CryptoRng, Error as RngError, RngCore}; +use secp256r1::SigningKey; +use sha2::{Digest as _, Sha256}; + +const MESSAGE: &[u8] = b"secp256r1 verification benchmark message"; +const SECRET: [u8; 32] = [7u8; 32]; + +struct Fixture { + digest: [u8; 32], + rust_key: SigningKey, + p256_key: P256SigningKey, + openssl_key: EcKey, +} + +impl Fixture { + fn new() -> Self { + Self { + digest: Sha256::digest(MESSAGE).into(), + rust_key: SigningKey::from_slice(&SECRET).unwrap(), + p256_key: P256SigningKey::from_slice(&SECRET).unwrap(), + openssl_key: openssl_key_from_secret(), + } + } +} + +#[derive(Clone, Copy)] +struct BenchRng { + state: u64, +} + +impl BenchRng { + fn new() -> Self { + Self { + state: 0x243f_6a88_85a3_08d3, + } + } + + fn next(&mut self) -> u64 { + let mut x = self.state; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.state = x; + x + } +} + +impl RngCore for BenchRng { + fn next_u32(&mut self) -> u32 { + self.next() as u32 + } + + fn next_u64(&mut self) -> u64 { + self.next() + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + for chunk in dest.chunks_mut(8) { + let word = self.next().to_be_bytes(); + chunk.copy_from_slice(&word[..chunk.len()]); + } + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), RngError> { + self.fill_bytes(dest); + Ok(()) + } +} + +impl CryptoRng for BenchRng {} + +fn openssl_key_from_secret() -> EcKey { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let private_key = BigNum::from_slice(&SECRET).unwrap(); + let mut context = BigNumContext::new().unwrap(); + let mut public_key = EcPoint::new(&group).unwrap(); + public_key + .mul_generator2(&group, &private_key, &mut context) + .unwrap(); + + EcKey::from_private_components(&group, &private_key, &public_key).unwrap() +} + +fn bench_keygen(c: &mut Criterion) { + let openssl_group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let mut rust_rng = BenchRng::new(); + let mut p256_rng = BenchRng::new(); + let mut group = c.benchmark_group("secp256r1_keygen"); + + group.bench_function("rust", |b| { + b.iter(|| SigningKey::random(black_box(&mut rust_rng))); + }); + + group.bench_function("p256", |b| { + b.iter(|| P256SigningKey::random(black_box(&mut p256_rng))); + }); + + group.bench_function("openssl", |b| { + b.iter(|| EcKey::generate(black_box(&openssl_group)).unwrap()); + }); + + group.finish(); +} + +fn bench_sign_prehashed(c: &mut Criterion) { + let fixture = Fixture::new(); + let mut group = c.benchmark_group("secp256r1_sign_prehashed"); + + group.bench_function("rust", |b| { + b.iter(|| { + let signature = fixture + .rust_key + .sign_prehash(black_box(&fixture.digest)) + .unwrap(); + black_box(signature); + }); + }); + + group.bench_function("p256", |b| { + b.iter(|| { + let signature: P256Signature = fixture + .p256_key + .sign_prehash(black_box(&fixture.digest)) + .unwrap(); + black_box(signature); + }); + }); + + group.bench_function("openssl", |b| { + b.iter(|| { + let signature = + EcdsaSig::sign(black_box(&fixture.digest), black_box(&fixture.openssl_key)) + .unwrap(); + black_box(signature); + }); + }); + + group.finish(); +} + +fn bench_sign_message_sha256(c: &mut Criterion) { + let fixture = Fixture::new(); + let mut group = c.benchmark_group("secp256r1_sign_message_sha256"); + + group.bench_function("rust", |b| { + b.iter(|| black_box(fixture.rust_key.sign(black_box(MESSAGE)))); + }); + + group.bench_function("p256", |b| { + b.iter(|| { + let signature: P256Signature = fixture.p256_key.sign(black_box(MESSAGE)); + black_box(signature); + }); + }); + + group.bench_function("openssl", |b| { + b.iter(|| { + let digest: [u8; 32] = Sha256::digest(black_box(MESSAGE)).into(); + let signature = + EcdsaSig::sign(black_box(&digest), black_box(&fixture.openssl_key)).unwrap(); + black_box(signature); + }); + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_keygen, + bench_sign_prehashed, + bench_sign_message_sha256 +); +criterion_main!(benches); diff --git a/secp256r1/benches/verify.rs b/secp256r1/benches/verify.rs new file mode 100644 index 0000000..857e983 --- /dev/null +++ b/secp256r1/benches/verify.rs @@ -0,0 +1,280 @@ +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use openssl::{ + bn::BigNumContext, + ec::{EcGroup, EcKey, EcPoint}, + ecdsa::EcdsaSig, + nid::Nid, + pkey::Public, +}; +use p256::ecdsa::{ + Signature as P256Signature, SigningKey, VerifyingKey, + signature::{Signer as _, hazmat::PrehashVerifier}, +}; +use secp256r1::{Signature, VerifyingKey as RustVerifyingKey}; +use sha2::{Digest as _, Sha256}; + +const MESSAGE: &[u8] = b"secp256r1 verification benchmark message"; + +struct BenchFixture { + digest: [u8; 32], + message: &'static [u8], + rust_signature: Signature, + openssl_signature: EcdsaSig, + p256_signature: P256Signature, + public_key_sec1: Vec, + signature_der: Vec, +} + +impl BenchFixture { + fn new() -> Self { + let secret = [7u8; 32]; + let signing_key = SigningKey::from_slice(&secret).unwrap(); + let verifying_key = signing_key.verifying_key(); + let p256_signature: P256Signature = signing_key.sign(MESSAGE); + let signature_der = p256_signature.to_der().as_bytes().to_vec(); + let signature_bytes = p256_signature.to_bytes(); + let rust_signature = Signature::from_scalars( + signature_bytes[..32].try_into().unwrap(), + signature_bytes[32..].try_into().unwrap(), + ) + .unwrap(); + let digest = Sha256::digest(MESSAGE).into(); + let openssl_signature = EcdsaSig::from_der(&signature_der).unwrap(); + + Self { + digest, + message: MESSAGE, + rust_signature, + openssl_signature, + p256_signature, + public_key_sec1: verifying_key.to_encoded_point(false).as_bytes().to_vec(), + signature_der, + } + } +} + +fn sha256(message: &[u8]) -> [u8; 32] { + Sha256::digest(message).into() +} + +struct P256Comparison { + verifying_key: VerifyingKey, +} + +impl P256Comparison { + fn from_sec1_public_key(public_key_sec1: &[u8]) -> Self { + Self { + verifying_key: VerifyingKey::from_sec1_bytes(public_key_sec1).unwrap(), + } + } + + fn verify_prehashed_signature(&self, digest: &[u8], signature: &P256Signature) -> bool { + self.verifying_key.verify_prehash(digest, signature).is_ok() + } + + fn verify_prehashed_der(&self, digest: &[u8], signature_der: &[u8]) -> bool { + let signature = P256Signature::from_der(signature_der).unwrap(); + self.verify_prehashed_signature(digest, &signature) + } +} + +struct OpenSslComparison { + ec_key: EcKey, +} + +impl OpenSslComparison { + fn from_sec1_public_key(public_key_sec1: &[u8]) -> Self { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let mut context = BigNumContext::new().unwrap(); + let point = EcPoint::from_bytes(&group, public_key_sec1, &mut context).unwrap(); + let ec_key = EcKey::from_public_key(&group, &point).unwrap(); + + Self { ec_key } + } + + fn verify_prehashed_signature(&self, digest: &[u8], signature: &EcdsaSig) -> bool { + signature.verify(digest, &self.ec_key).unwrap() + } + + fn verify_prehashed_der(&self, digest: &[u8], signature_der: &[u8]) -> bool { + let signature = EcdsaSig::from_der(signature_der).unwrap(); + self.verify_prehashed_signature(digest, &signature) + } +} + +fn bench_prehashed_preparsed(c: &mut Criterion) { + let fixture = BenchFixture::new(); + let p256_verifier = P256Comparison::from_sec1_public_key(&fixture.public_key_sec1); + let rust_verifier = RustVerifyingKey::from_sec1_bytes(&fixture.public_key_sec1).unwrap(); + let openssl_verifier = OpenSslComparison::from_sec1_public_key(&fixture.public_key_sec1); + + let mut group = c.benchmark_group("secp256r1_verify_prehashed_preparsed"); + + group.bench_function("p256", |b| { + b.iter(|| { + assert!(p256_verifier.verify_prehashed_signature( + black_box(&fixture.digest), + black_box(&fixture.p256_signature) + )); + }); + }); + + group.bench_function("rust", |b| { + b.iter(|| { + assert!( + rust_verifier + .verify_prehash( + black_box(&fixture.digest), + black_box(&fixture.rust_signature) + ) + .is_ok() + ); + }); + }); + + group.bench_function("openssl", |b| { + b.iter(|| { + assert!(openssl_verifier.verify_prehashed_signature( + black_box(&fixture.digest), + black_box(&fixture.openssl_signature) + )); + }); + }); + + group.finish(); +} + +fn bench_prehashed_der(c: &mut Criterion) { + let fixture = BenchFixture::new(); + let p256_verifier = P256Comparison::from_sec1_public_key(&fixture.public_key_sec1); + let rust_verifier = RustVerifyingKey::from_sec1_bytes(&fixture.public_key_sec1).unwrap(); + let openssl_verifier = OpenSslComparison::from_sec1_public_key(&fixture.public_key_sec1); + + let mut group = c.benchmark_group("secp256r1_verify_prehashed_der"); + + group.bench_function("p256", |b| { + b.iter(|| { + assert!(p256_verifier.verify_prehashed_der( + black_box(&fixture.digest), + black_box(&fixture.signature_der) + )); + }); + }); + + group.bench_function("rust", |b| { + b.iter(|| { + assert!( + rust_verifier + .verify_prehashed_der( + black_box(&fixture.digest), + black_box(&fixture.signature_der) + ) + .is_ok() + ); + }); + }); + + group.bench_function("openssl", |b| { + b.iter(|| { + assert!(openssl_verifier.verify_prehashed_der( + black_box(&fixture.digest), + black_box(&fixture.signature_der) + )); + }); + }); + + group.finish(); +} + +fn bench_message_sha256_preparsed(c: &mut Criterion) { + let fixture = BenchFixture::new(); + let p256_verifier = P256Comparison::from_sec1_public_key(&fixture.public_key_sec1); + let rust_verifier = RustVerifyingKey::from_sec1_bytes(&fixture.public_key_sec1).unwrap(); + let openssl_verifier = OpenSslComparison::from_sec1_public_key(&fixture.public_key_sec1); + + let mut group = c.benchmark_group("secp256r1_verify_message_sha256_preparsed"); + + group.bench_function("p256", |b| { + b.iter(|| { + let digest = sha256(black_box(fixture.message)); + assert!(p256_verifier.verify_prehashed_signature( + black_box(&digest), + black_box(&fixture.p256_signature) + )); + }); + }); + + group.bench_function("rust", |b| { + b.iter(|| { + let digest = sha256(black_box(fixture.message)); + assert!( + rust_verifier + .verify_prehash(black_box(&digest), black_box(&fixture.rust_signature)) + .is_ok() + ); + }); + }); + + group.bench_function("openssl", |b| { + b.iter(|| { + let digest = sha256(black_box(fixture.message)); + assert!(openssl_verifier.verify_prehashed_signature( + black_box(&digest), + black_box(&fixture.openssl_signature) + )); + }); + }); + + group.finish(); +} + +fn bench_message_sha256_der(c: &mut Criterion) { + let fixture = BenchFixture::new(); + let p256_verifier = P256Comparison::from_sec1_public_key(&fixture.public_key_sec1); + let rust_verifier = RustVerifyingKey::from_sec1_bytes(&fixture.public_key_sec1).unwrap(); + let openssl_verifier = OpenSslComparison::from_sec1_public_key(&fixture.public_key_sec1); + + let mut group = c.benchmark_group("secp256r1_verify_message_sha256_der"); + + group.bench_function("p256", |b| { + b.iter(|| { + let digest = sha256(black_box(fixture.message)); + assert!( + p256_verifier + .verify_prehashed_der(black_box(&digest), black_box(&fixture.signature_der)) + ); + }); + }); + + group.bench_function("rust", |b| { + b.iter(|| { + let digest = sha256(black_box(fixture.message)); + assert!( + rust_verifier + .verify_prehashed_der(black_box(&digest), black_box(&fixture.signature_der)) + .is_ok() + ); + }); + }); + + group.bench_function("openssl", |b| { + b.iter(|| { + let digest = sha256(black_box(fixture.message)); + assert!( + openssl_verifier + .verify_prehashed_der(black_box(&digest), black_box(&fixture.signature_der)) + ); + }); + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_prehashed_preparsed, + bench_prehashed_der, + bench_message_sha256_preparsed, + bench_message_sha256_der +); +criterion_main!(benches); diff --git a/secp256r1/src/constants.rs b/secp256r1/src/constants.rs new file mode 100644 index 0000000..1de8ed3 --- /dev/null +++ b/secp256r1/src/constants.rs @@ -0,0 +1,8 @@ +pub(crate) const DIGEST_LEN: usize = 32; +pub(crate) const COMPRESSED_SEC1_PUBLIC_KEY_LEN: usize = 33; +pub(crate) const UNCOMPRESSED_SEC1_PUBLIC_KEY_LEN: usize = 65; + +pub(crate) const ORDER_BYTES: [u8; DIGEST_LEN] = [ + 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, +]; diff --git a/secp256r1/src/ecdsa.rs b/secp256r1/src/ecdsa.rs new file mode 100644 index 0000000..224bd5b --- /dev/null +++ b/secp256r1/src/ecdsa.rs @@ -0,0 +1,647 @@ +use crate::{ + Error, Result, + constants::{ + COMPRESSED_SEC1_PUBLIC_KEY_LEN, DIGEST_LEN, ORDER_BYTES, UNCOMPRESSED_SEC1_PUBLIC_KEY_LEN, + }, + field::FieldElement, + group::{AffinePoint, ProjectivePoint}, + scalar::Scalar, +}; +use core::fmt; +use hmac::{Hmac, Mac}; +use rand_core::CryptoRngCore; +use sha2::{Digest as _, Sha256}; +use zeroize::Zeroize; + +type HmacSha256 = Hmac; + +/// An ECDSA signature over the P-256 curve. +/// +/// A signature consists of two scalars, `r` and `s`, each in `[1, n-1]` +/// where `n` is the group order. Use [`from_der`][Self::from_der] or +/// [`from_slice`][Self::from_slice] to parse, and [`to_der`][Self::to_der] +/// or [`to_bytes`][Self::to_bytes] to serialize. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Signature { + r: Scalar, + s: Scalar, +} + +impl Signature { + /// Builds a signature from canonical big-endian 32-byte `r` and `s` scalars. + /// + /// Returns an error if either scalar is zero or not in canonical form + /// (i.e. ≥ the group order `n`). + pub fn from_scalars(r: [u8; DIGEST_LEN], s: [u8; DIGEST_LEN]) -> Result { + let r = Scalar::from_be_bytes(r) + .filter(|r| !r.is_zero()) + .ok_or_else(|| Error::InvalidInput("invalid ECDSA r scalar".to_owned()))?; + let s = Scalar::from_be_bytes(s) + .filter(|s| !s.is_zero()) + .ok_or_else(|| Error::InvalidInput("invalid ECDSA s scalar".to_owned()))?; + + Ok(Self { r, s }) + } + + /// Parses a DER-encoded ECDSA signature. + /// + /// Enforces strict/minimal DER: no trailing bytes, no leading zero bytes + /// in integers beyond the required sign byte, no negative integers. + pub fn from_der(signature_der: &[u8]) -> Result { + let mut parser = DerParser::new(signature_der); + parser.expect_byte(0x30)?; + let sequence_len = parser.read_len()?; + let sequence_end = parser + .position + .checked_add(sequence_len) + .ok_or_else(|| Error::InvalidInput("invalid DER sequence length".to_owned()))?; + + if sequence_end != signature_der.len() { + return Err(Error::InvalidInput( + "DER signature has trailing data".to_owned(), + )); + } + + let r = parser.read_integer_32()?; + let s = parser.read_integer_32()?; + + if parser.position != sequence_end { + return Err(Error::InvalidInput( + "DER signature has trailing sequence data".to_owned(), + )); + } + + Self::from_scalars(r, s) + } + + /// Parses a fixed-width `r || s` ECDSA signature. + pub fn from_slice(bytes: &[u8]) -> Result { + let bytes: &[u8; 64] = bytes + .try_into() + .map_err(|_| Error::InvalidInput("expected 64-byte signature".to_owned()))?; + let mut r = [0u8; DIGEST_LEN]; + let mut s = [0u8; DIGEST_LEN]; + r.copy_from_slice(&bytes[..DIGEST_LEN]); + s.copy_from_slice(&bytes[DIGEST_LEN..]); + Self::from_scalars(r, s) + } + + /// Returns the fixed-width `r || s` encoding. + pub fn to_bytes(self) -> [u8; 64] { + let mut out = [0u8; 64]; + out[..DIGEST_LEN].copy_from_slice(&self.r.to_be_bytes()); + out[DIGEST_LEN..].copy_from_slice(&self.s.to_be_bytes()); + out + } + + /// Returns the DER encoding of this signature. + pub fn to_der(self) -> DerSignature { + let mut out = [0u8; 72]; + let r = self.r.to_be_bytes(); + let s = self.s.to_be_bytes(); + let r_len = write_der_integer(&mut out[2..], r); + let s_len = write_der_integer(&mut out[2 + r_len..], s); + out[0] = 0x30; + out[1] = (r_len + s_len) as u8; + + DerSignature { + bytes: out, + len: 2 + r_len + s_len, + } + } + + /// Returns the `r` scalar. + #[inline] + pub fn r(self) -> Scalar { + self.r + } + + /// Returns the `s` scalar. + #[inline] + pub fn s(self) -> Scalar { + self.s + } +} + +/// A DER-encoded ECDSA signature, at most 72 bytes. +/// +/// Produced by [`Signature::to_der`]. Use [`as_bytes`][Self::as_bytes] to +/// obtain the encoded bytes. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct DerSignature { + bytes: [u8; 72], + len: usize, +} + +impl DerSignature { + /// Returns this DER signature as bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.bytes[..self.len] + } +} + +impl AsRef<[u8]> for DerSignature { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +/// A SEC1-encoded public key, either compressed (33 bytes) or uncompressed (65 bytes). +/// +/// Produced by [`VerifyingKey::to_encoded_point`]. Use +/// [`as_bytes`][Self::as_bytes] to obtain the encoded bytes. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EncodedPoint { + bytes: [u8; UNCOMPRESSED_SEC1_PUBLIC_KEY_LEN], + len: usize, +} + +impl EncodedPoint { + /// Returns this encoded point as bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.bytes[..self.len] + } +} + +impl AsRef<[u8]> for EncodedPoint { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +/// ECDSA signing key for secp256r1/P-256. +/// +/// # Security +/// +/// Public-key derivation and signing currently use variable-time scalar +/// multiplication with secret-dependent values. This type is intended for +/// benchmarking and experimentation, not production signing in side-channel +/// exposed environments. +#[derive(Clone)] +pub struct SigningKey { + secret: Scalar, + verifying_key: VerifyingKey, +} + +impl fmt::Debug for SigningKey { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("SigningKey") + .field("secret", &"") + .field("verifying_key", &self.verifying_key) + .finish() + } +} + +impl SigningKey { + /// Generates a signing key using rejection sampling. + pub fn random(rng: &mut impl CryptoRngCore) -> Self { + loop { + let mut bytes = [0u8; DIGEST_LEN]; + rng.fill_bytes(&mut bytes); + let key = Self::from_bytes(bytes); + bytes.zeroize(); + if let Ok(key) = key { + return key; + } + } + } + + /// Builds a signing key from a canonical 32-byte scalar. + /// + /// This derives the corresponding verifying key using variable-time scalar + /// multiplication. + pub fn from_slice(bytes: &[u8]) -> Result { + let mut bytes: [u8; DIGEST_LEN] = bytes + .try_into() + .map_err(|_| Error::InvalidInput("expected 32-byte signing key".to_owned()))?; + let key = Self::from_bytes(bytes); + bytes.zeroize(); + key + } + + /// Builds a signing key from a canonical 32-byte scalar. + /// + /// This derives the corresponding verifying key using variable-time scalar + /// multiplication. + pub fn from_bytes(mut bytes: [u8; DIGEST_LEN]) -> Result { + let Some(secret) = Scalar::from_be_bytes(bytes).filter(|secret| !secret.is_zero()) else { + bytes.zeroize(); + return Err(Error::InvalidInput("invalid signing key scalar".to_owned())); + }; + bytes.zeroize(); + let mut scalar_bytes = secret.to_be_bytes(); + let public_key = ProjectivePoint::mul_generator_vartime(scalar_bytes).to_affine(); + scalar_bytes.zeroize(); + + Ok(Self { + secret, + verifying_key: VerifyingKey { public_key }, + }) + } + + /// Returns the verifying key corresponding to this signing key. + pub fn verifying_key(&self) -> &VerifyingKey { + &self.verifying_key + } + + /// Returns this signing key as a canonical big-endian 32-byte scalar. + /// + /// The caller is responsible for clearing the returned bytes when they are + /// no longer needed. + pub fn to_bytes(&self) -> [u8; DIGEST_LEN] { + self.secret.to_be_bytes() + } + + /// Signs a message using SHA-256 and deterministic RFC6979 nonces. + /// + /// Signing uses variable-time scalar multiplication for the secret nonce. + pub fn sign(&self, message: &[u8]) -> Signature { + self.sign_prehash(&Sha256::digest(message)) + .expect("SHA-256 output has the expected length") + } + + /// Signs a 32-byte message digest using deterministic RFC6979 nonces. + /// + /// Signing uses variable-time scalar multiplication for the secret nonce. + pub fn sign_prehash(&self, digest: &[u8]) -> Result { + let digest = digest_32(digest)?; + let z = Scalar::from_be_bytes_reduced(digest); + let mut secret = self.secret.to_be_bytes(); + let mut nonce = Rfc6979::new(secret, digest); + secret.zeroize(); + + loop { + let mut k = nonce.next(); + let mut k_bytes = k.to_be_bytes(); + let r = scalar_from_x_coordinate( + ProjectivePoint::mul_generator_vartime(k_bytes).to_affine(), + ); + k_bytes.zeroize(); + + if r.is_zero() { + k.zeroize(); + nonce.retry(); + continue; + } + + let Some(mut k_inv) = k.invert() else { + k.zeroize(); + nonce.retry(); + continue; + }; + let s = k_inv * (z + r * self.secret); + + if !s.is_zero() { + k.zeroize(); + k_inv.zeroize(); + return Ok(Signature { r, s }); + } + + k.zeroize(); + k_inv.zeroize(); + nonce.retry(); + } + } +} + +impl Drop for SigningKey { + fn drop(&mut self) { + self.secret.zeroize(); + } +} + +/// ECDSA verifying key for secp256r1/P-256. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct VerifyingKey { + public_key: AffinePoint, +} + +impl VerifyingKey { + /// Builds a verifying key from compressed or uncompressed SEC1 public key bytes. + pub fn from_sec1_bytes(public_key_sec1: &[u8]) -> Result { + let public_key = match public_key_sec1.len() { + UNCOMPRESSED_SEC1_PUBLIC_KEY_LEN => { + let bytes = public_key_sec1 + .try_into() + .expect("length checked above for uncompressed SEC1 key"); + AffinePoint::from_sec1_uncompressed(bytes) + } + COMPRESSED_SEC1_PUBLIC_KEY_LEN => { + let bytes = public_key_sec1 + .try_into() + .expect("length checked above for compressed SEC1 key"); + AffinePoint::from_sec1_compressed(bytes) + } + _ => None, + } + .ok_or_else(|| Error::InvalidInput("invalid secp256r1 public key".to_owned()))?; + + Ok(Self { public_key }) + } + + /// Returns this public key as a SEC1-encoded point. + /// + /// Pass `compress = false` for the uncompressed 65-byte `0x04 || X || Y` + /// encoding, or `compress = true` for the compressed 33-byte + /// `0x02/0x03 || X` encoding. + pub fn to_encoded_point(&self, compress: bool) -> EncodedPoint { + let uncompressed = self + .public_key + .to_sec1_uncompressed() + .expect("verifying keys are never identity"); + + if !compress { + return EncodedPoint { + bytes: uncompressed, + len: UNCOMPRESSED_SEC1_PUBLIC_KEY_LEN, + }; + } + + let mut bytes = [0u8; UNCOMPRESSED_SEC1_PUBLIC_KEY_LEN]; + let y = self + .public_key + .y() + .expect("verifying keys are never identity"); + bytes[0] = if y.to_be_bytes()[DIGEST_LEN - 1] & 1 == 0 { + 0x02 + } else { + 0x03 + }; + bytes[1..33].copy_from_slice(&uncompressed[1..33]); + EncodedPoint { bytes, len: 33 } + } + + /// Verifies an ECDSA signature over a message using SHA-256. + pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<()> { + self.verify_prehash(&Sha256::digest(message), signature) + } + + /// Verifies an ECDSA signature over a 32-byte message digest. + pub fn verify_prehash(&self, digest: &[u8], signature: &Signature) -> Result<()> { + let digest = digest_32(digest)?; + if self.verify_digest_signature(digest, signature) { + Ok(()) + } else { + Err(Error::InvalidSignature) + } + } + + /// Verifies a DER-encoded ECDSA signature over a 32-byte message digest. + pub fn verify_prehashed_der(&self, digest: &[u8], signature_der: &[u8]) -> Result<()> { + let signature = Signature::from_der(signature_der)?; + self.verify_prehash(digest, &signature) + } + + fn verify_digest_signature(&self, digest: [u8; DIGEST_LEN], signature: &Signature) -> bool { + let z = Scalar::from_be_bytes_reduced(digest); + let Some(w) = signature.s.invert() else { + return false; + }; + let u1 = z * w; + let u2 = signature.r * w; + let point = ProjectivePoint::mul_generator_vartime(u1.to_be_bytes()) + + ProjectivePoint::from_affine(self.public_key) + .mul_scalar_wnaf6_vartime(u2.to_be_bytes()); + + !point.is_identity() && projective_x_matches_signature_r(point, signature.r) + } +} + +fn digest_32(digest: &[u8]) -> Result<[u8; DIGEST_LEN]> { + digest.try_into().map_err(|_| Error::InvalidDigestLength { + expected: DIGEST_LEN, + actual: digest.len(), + }) +} + +fn scalar_from_x_coordinate(point: AffinePoint) -> Scalar { + let x = point.x().expect("nonzero scalar multiple of generator"); + Scalar::from_be_bytes_reduced(x.to_be_bytes()) +} + +fn write_der_integer(out: &mut [u8], bytes: [u8; DIGEST_LEN]) -> usize { + let first_nonzero = bytes + .iter() + .position(|byte| *byte != 0) + .unwrap_or(DIGEST_LEN - 1); + let integer = &bytes[first_nonzero..]; + + out[0] = 0x02; + if integer[0] & 0x80 != 0 { + out[1] = (integer.len() + 1) as u8; + out[2] = 0; + out[3..3 + integer.len()].copy_from_slice(integer); + 3 + integer.len() + } else { + out[1] = integer.len() as u8; + out[2..2 + integer.len()].copy_from_slice(integer); + 2 + integer.len() + } +} + +struct Rfc6979 { + key: [u8; DIGEST_LEN], + value: [u8; DIGEST_LEN], +} + +impl Rfc6979 { + fn new(mut secret: [u8; DIGEST_LEN], digest: [u8; DIGEST_LEN]) -> Self { + let mut digest = Scalar::from_be_bytes_reduced(digest).to_be_bytes(); + let mut out = Self { + key: [0u8; DIGEST_LEN], + value: [1u8; DIGEST_LEN], + }; + + out.rekey(&[&[0x00], &secret, &digest]); + out.update_value(); + out.rekey(&[&[0x01], &secret, &digest]); + out.update_value(); + secret.zeroize(); + digest.zeroize(); + out + } + + fn next(&mut self) -> Scalar { + loop { + self.update_value(); + + if let Some(scalar) = + Scalar::from_be_bytes(self.value).filter(|scalar| !scalar.is_zero()) + { + return scalar; + } + + self.retry(); + } + } + + fn retry(&mut self) { + self.rekey(&[&[0x00]]); + self.update_value(); + } + + fn rekey(&mut self, parts: &[&[u8]]) { + let mut mac = + ::new_from_slice(&self.key).expect("HMAC accepts keys of any size"); + mac.update(&self.value); + for part in parts { + mac.update(part); + } + let mut key = [0u8; DIGEST_LEN]; + key.copy_from_slice(&mac.finalize().into_bytes()); + self.key.copy_from_slice(&key); + key.zeroize(); + } + + fn update_value(&mut self) { + let mut mac = + ::new_from_slice(&self.key).expect("HMAC accepts keys of any size"); + mac.update(&self.value); + let mut value = [0u8; DIGEST_LEN]; + value.copy_from_slice(&mac.finalize().into_bytes()); + self.value.copy_from_slice(&value); + value.zeroize(); + } +} + +impl Drop for Rfc6979 { + fn drop(&mut self) { + self.key.zeroize(); + self.value.zeroize(); + } +} + +#[inline] +fn projective_x_matches_signature_r(point: ProjectivePoint, r: Scalar) -> bool { + let r_bytes = r.to_be_bytes(); + let r_field = FieldElement::from_be_bytes(r_bytes).expect("r < group order < field prime"); + + point.has_affine_x(r_field) + || r_plus_order_field(r_bytes).is_some_and(|candidate| point.has_affine_x(candidate)) +} + +fn r_plus_order_field(r: [u8; DIGEST_LEN]) -> Option { + let mut out = [0u8; DIGEST_LEN]; + let mut carry = 0u16; + + for i in (0..DIGEST_LEN).rev() { + let sum = r[i] as u16 + ORDER_BYTES[i] as u16 + carry; + out[i] = sum as u8; + carry = sum >> 8; + } + + if carry == 0 { + FieldElement::from_be_bytes(out) + } else { + None + } +} + +struct DerParser<'a> { + input: &'a [u8], + position: usize, +} + +impl<'a> DerParser<'a> { + fn new(input: &'a [u8]) -> Self { + Self { input, position: 0 } + } + + fn expect_byte(&mut self, expected: u8) -> Result<()> { + let actual = self.read_byte()?; + if actual == expected { + Ok(()) + } else { + Err(Error::InvalidInput("unexpected DER tag".to_owned())) + } + } + + fn read_len(&mut self) -> Result { + let first = self.read_byte()?; + if first & 0x80 == 0 { + return Ok(first as usize); + } + + let bytes = (first & 0x7f) as usize; + if bytes == 0 || bytes > 2 { + return Err(Error::InvalidInput( + "unsupported DER length encoding".to_owned(), + )); + } + + let mut len = 0usize; + for i in 0..bytes { + let byte = self.read_byte()?; + if i == 0 && byte == 0 { + return Err(Error::InvalidInput( + "non-minimal DER length encoding".to_owned(), + )); + } + len = (len << 8) | byte as usize; + } + + if len < 128 { + return Err(Error::InvalidInput( + "non-minimal DER length encoding".to_owned(), + )); + } + + Ok(len) + } + + fn read_integer_32(&mut self) -> Result<[u8; DIGEST_LEN]> { + self.expect_byte(0x02)?; + let len = self.read_len()?; + if len == 0 { + return Err(Error::InvalidInput("empty DER integer".to_owned())); + } + + let integer = self.read_bytes(len)?; + if integer[0] & 0x80 != 0 { + return Err(Error::InvalidInput("negative DER integer".to_owned())); + } + + let integer = if integer.len() > 1 && integer[0] == 0 { + if integer[1] & 0x80 == 0 { + return Err(Error::InvalidInput( + "non-minimal DER integer encoding".to_owned(), + )); + } + &integer[1..] + } else { + integer + }; + + if integer.len() > DIGEST_LEN { + return Err(Error::InvalidInput("oversized DER integer".to_owned())); + } + + let mut out = [0u8; DIGEST_LEN]; + out[DIGEST_LEN - integer.len()..].copy_from_slice(integer); + Ok(out) + } + + fn read_byte(&mut self) -> Result { + let byte = self + .input + .get(self.position) + .copied() + .ok_or_else(|| Error::InvalidInput("truncated DER input".to_owned()))?; + self.position += 1; + Ok(byte) + } + + fn read_bytes(&mut self, len: usize) -> Result<&'a [u8]> { + let end = self + .position + .checked_add(len) + .ok_or_else(|| Error::InvalidInput("invalid DER length".to_owned()))?; + let bytes = self + .input + .get(self.position..end) + .ok_or_else(|| Error::InvalidInput("truncated DER input".to_owned()))?; + self.position = end; + Ok(bytes) + } +} diff --git a/secp256r1/src/error.rs b/secp256r1/src/error.rs new file mode 100644 index 0000000..9db0b2e --- /dev/null +++ b/secp256r1/src/error.rs @@ -0,0 +1,32 @@ +use std::fmt; + +/// Errors returned by key construction, signature parsing, signing, and verification. +#[derive(Debug)] +pub enum Error { + /// A prehashed verification API received a digest with the wrong length. + InvalidDigestLength { expected: usize, actual: usize }, + /// The API rejected malformed input. + InvalidInput(String), + /// A syntactically valid ECDSA signature did not verify. + InvalidSignature, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidDigestLength { expected, actual } => { + write!( + f, + "invalid digest length: expected {expected}, got {actual}" + ) + } + Self::InvalidInput(error) => write!(f, "secp256r1 error: {error}"), + Self::InvalidSignature => write!(f, "invalid ECDSA signature"), + } + } +} + +impl std::error::Error for Error {} + +/// Result type used by this crate. +pub type Result = std::result::Result; diff --git a/secp256r1/src/field.rs b/secp256r1/src/field.rs new file mode 100644 index 0000000..2d10112 --- /dev/null +++ b/secp256r1/src/field.rs @@ -0,0 +1,569 @@ +//! 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_MINUS_TWO: [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, 0xfd, +]; +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.pow(P_MINUS_TWO)) + } + + #[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 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 { + let (w0, carry) = adc(w0, MODULUS[0], 0); + let (w1, carry) = adc(w1, MODULUS[1], carry); + let (w2, carry) = adc(w2, MODULUS[2], carry); + let (w3, _) = adc(w3, MODULUS[3], carry); + [w0, w1, w2, w3] + } +} + +#[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 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"); + } + } +} diff --git a/secp256r1/src/group.rs b/secp256r1/src/group.rs new file mode 100644 index 0000000..a789395 --- /dev/null +++ b/secp256r1/src/group.rs @@ -0,0 +1,864 @@ +//! 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; +use zeroize::Zeroize; + +const BASE_WINDOWS: usize = 32; +const BASE_WINDOW_POINTS: usize = 256; +const SHAMIR_WINDOW_POINTS: usize = 16; +const WNAF_DIGITS: usize = 257; + +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_sec1_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_sec1_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_sec1_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, Eq, PartialEq)] +pub struct ProjectivePoint { + x: FieldElement, + y: FieldElement, + z: FieldElement, +} + +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_sec1_uncompressed(self) -> Option<[u8; 65]> { + self.to_affine().to_sec1_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_generator_vartime(mut scalar: [u8; 32]) -> Self { + let out = mul_window8_vartime(generator_window8_table(), &scalar); + scalar.zeroize(); + out + } + + #[inline] + pub fn mul_affine_scalar_vartime(base: AffinePoint, mut scalar: [u8; 32]) -> Self { + let out = mul_window4_vartime(&window4_table(base), &scalar); + scalar.zeroize(); + out + } + + #[inline] + pub fn double_scalar_mul_shamir_vartime( + mut generator_scalar: [u8; 32], + point: AffinePoint, + mut 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]); + } + + generator_scalar.zeroize(); + point_scalar.zeroize(); + out + } + + #[inline] + pub fn double_scalar_mul_shamir_window8_vartime( + mut generator_scalar: [u8; 32], + point: AffinePoint, + mut point_scalar: [u8; 32], + ) -> Self { + let generator_table = generator_shamir_window8_table(); + let point_table = projective_window8_table(ProjectivePoint::from_affine(point)); + let mut out = Self::IDENTITY; + + for (&generator_byte, &point_byte) in generator_scalar.iter().zip(point_scalar.iter()) { + out = double_n(out, 8) + .add_mixed(generator_table[generator_byte as usize]) + .add_mixed(point_table[point_byte as usize]); + } + + generator_scalar.zeroize(); + point_scalar.zeroize(); + out + } + + #[inline] + pub fn mul_scalar_vartime(self, mut 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 mut out = Self::IDENTITY; + + for &byte in scalar.iter() { + out = out.double().double().double().double(); + out = out + table[(byte >> 4) as usize]; + out = out.double().double().double().double(); + out = out + table[(byte & 0x0f) as usize]; + } + + scalar.zeroize(); + out + } + + #[inline] + pub fn mul_scalar_wnaf5_vartime(self, mut scalar: [u8; 32]) -> Self { + let out = mul_wnaf_projective_vartime::<8>(self, &scalar, 5); + scalar.zeroize(); + out + } + + #[inline] + pub fn mul_scalar_wnaf6_vartime(self, mut scalar: [u8; 32]) -> Self { + let out = mul_wnaf_projective_vartime::<16>(self, &scalar, 6); + scalar.zeroize(); + 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 generator_shamir_window8_table() -> &'static [AffinePoint; BASE_WINDOW_POINTS] { + static TABLE: OnceLock<[AffinePoint; BASE_WINDOW_POINTS]> = OnceLock::new(); + + TABLE.get_or_init(|| projective_window8_table(ProjectivePoint::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 mul_window4_vartime( + table: &[AffinePoint; SHAMIR_WINDOW_POINTS], + scalar: &[u8; 32], +) -> ProjectivePoint { + let mut out = ProjectivePoint::IDENTITY; + + for &byte in scalar { + out = double_n(out, 4).add_mixed(table[(byte >> 4) as usize]); + out = double_n(out, 4).add_mixed(table[(byte & 0x0f) as usize]); + } + + out +} + +#[inline] +fn mul_wnaf_projective_vartime( + base: ProjectivePoint, + scalar: &[u8; 32], + width: usize, +) -> ProjectivePoint { + let table = odd_projective_table::(base); + let (mut digits, len) = wnaf_digits(scalar, width); + let mut out = ProjectivePoint::IDENTITY; + + for i in (0..len).rev() { + out = out.double(); + + let digit = digits[i]; + if digit > 0 { + out = out + table[(digit as usize) >> 1]; + } else if digit < 0 { + out = out - table[((-digit) as usize) >> 1]; + } + } + + digits.zeroize(); + out +} + +#[inline] +fn odd_projective_table( + base: ProjectivePoint, +) -> [ProjectivePoint; TABLE_POINTS] { + let mut table = [ProjectivePoint::IDENTITY; TABLE_POINTS]; + table[0] = base; + + let two_base = base.double(); + for i in 1..TABLE_POINTS { + table[i] = table[i - 1] + two_base; + } + + table +} + +fn wnaf_digits(scalar: &[u8; 32], width: usize) -> ([i8; WNAF_DIGITS], usize) { + let mut k = scalar_limbs(scalar); + let mut digits = [0i8; WNAF_DIGITS]; + let mask = (1u64 << width) - 1; + let cutoff = 1i64 << (width - 1); + let radix = 1i64 << width; + let mut len = 0; + + for (i, digit) in digits.iter_mut().enumerate() { + if scalar_limbs_are_zero(k) { + break; + } + + if k[0] & 1 == 1 { + let residue = (k[0] & mask) as i64; + let signed_digit = if residue >= cutoff { + residue - radix + } else { + residue + }; + + *digit = signed_digit as i8; + if signed_digit > 0 { + sub_small(&mut k, signed_digit as u64); + } else { + add_small(&mut k, (-signed_digit) as u64); + } + } + + shift_right_one(&mut k); + len = i + 1; + } + + k.zeroize(); + (digits, len) +} + +#[inline] +fn scalar_limbs(scalar: &[u8; 32]) -> [u64; 5] { + let mut limbs = [0u64; 5]; + + for (i, chunk) in scalar.chunks_exact(8).rev().enumerate() { + limbs[i] = u64::from_be_bytes(chunk.try_into().expect("chunk length is 8")); + } + + limbs +} + +#[inline] +fn scalar_limbs_are_zero(limbs: [u64; 5]) -> bool { + limbs.into_iter().all(|limb| limb == 0) +} + +#[inline] +fn add_small(limbs: &mut [u64; 5], value: u64) { + let (sum, carry) = limbs[0].overflowing_add(value); + limbs[0] = sum; + + let mut carry = u64::from(carry); + for limb in limbs.iter_mut().skip(1) { + if carry == 0 { + break; + } + + let (sum, next_carry) = limb.overflowing_add(carry); + *limb = sum; + carry = u64::from(next_carry); + } +} + +#[inline] +fn sub_small(limbs: &mut [u64; 5], value: u64) { + let (difference, borrow) = limbs[0].overflowing_sub(value); + limbs[0] = difference; + + let mut borrow = u64::from(borrow); + for limb in limbs.iter_mut().skip(1) { + if borrow == 0 { + break; + } + + let (difference, next_borrow) = limb.overflowing_sub(borrow); + *limb = difference; + borrow = u64::from(next_borrow); + } +} + +#[inline] +fn shift_right_one(limbs: &mut [u64; 5]) { + let mut carry = 0; + + for limb in limbs.iter_mut().rev() { + let next_carry = *limb << 63; + *limb = (*limb >> 1) | carry; + carry = next_carry; + } +} + +#[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 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, + ]; + + fn assert_matches_p256(rust: ProjectivePoint, p256: P256ProjectivePoint) { + let rust_bytes = rust.to_sec1_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_sec1_uncompressed().unwrap(); + assert_eq!( + AffinePoint::from_sec1_uncompressed(bytes).unwrap(), + AffinePoint::generator() + ); + } + + #[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, + ); + assert_matches_p256( + ProjectivePoint::generator().mul_scalar_wnaf5_vartime(SCALAR), + P256ProjectivePoint::generator() * scalar, + ); + assert_matches_p256( + ProjectivePoint::generator().mul_scalar_wnaf6_vartime(SCALAR), + P256ProjectivePoint::generator() * scalar, + ); + assert_matches_p256( + ProjectivePoint::mul_affine_scalar_vartime(AffinePoint::generator(), SCALAR), + P256ProjectivePoint::generator() * scalar, + ); + } + + #[test] + fn base_scalar_mul_matches_p256() { + let scalar = Option::::from(Scalar::from_repr(SCALAR.into())).unwrap(); + assert_matches_p256( + ProjectivePoint::mul_generator_vartime(SCALAR), + P256ProjectivePoint::generator() * scalar, + ); + } + + #[test] + fn shamir_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_shamir_vartime(SCALAR, point, SCALAR), + (P256ProjectivePoint::generator() * scalar) + (p256_point * scalar), + ); + } +} diff --git a/secp256r1/src/lib.rs b/secp256r1/src/lib.rs new file mode 100644 index 0000000..ed2fb27 --- /dev/null +++ b/secp256r1/src/lib.rs @@ -0,0 +1,105 @@ +//! secp256r1/P-256 ECDSA keys, signatures, signing, and verification. +//! +//! This crate implements ECDSA signing and verification over the NIST P-256 +//! (secp256r1) curve in pure Rust, with no C dependencies in the library. +//! It is designed for benchmarking and experimentation; see the [Security] +//! section before using it. +//! +//! [Security]: #security +//! +//! # Quick start +//! +//! ```rust +//! use secp256r1::{SigningKey, VerifyingKey}; +//! +//! // Generate a key pair. +//! # let signing_key = SigningKey::from_slice(&[7u8; 32]).unwrap(); +//! # let verifying_key = *signing_key.verifying_key(); +//! // let signing_key = SigningKey::random(&mut rand_core::OsRng); +//! // let verifying_key = *signing_key.verifying_key(); +//! +//! // Sign a message (SHA-256 is applied internally). +//! let signature = signing_key.sign(b"hello"); +//! +//! // Verify. +//! verifying_key.verify(b"hello", &signature).unwrap(); +//! ``` +//! +//! # Types +//! +//! | Type | Description | +//! |---|---| +//! | [`SigningKey`] | Private key; produces [`Signature`]s | +//! | [`VerifyingKey`] | Public key; verifies [`Signature`]s | +//! | [`Signature`] | ECDSA signature; converts to/from fixed-width and DER | +//! | [`DerSignature`] | Borrowed view of a DER-encoded signature (up to 72 bytes) | +//! | [`EncodedPoint`] | Borrowed view of a SEC1-encoded public key (33 or 65 bytes) | +//! | [`Error`] | Error type returned by parsing and verification | +//! +//! # Signature formats +//! +//! Both fixed-width (`r || s`, 64 bytes) and DER formats are supported. +//! +//! ```rust +//! # use secp256r1::{Signature, SigningKey}; +//! # let signing_key = SigningKey::from_slice(&[7u8; 32]).unwrap(); +//! let signature = signing_key.sign(b"hello"); +//! +//! // Fixed-width round-trip. +//! let bytes = signature.to_bytes(); +//! let reparsed = Signature::from_slice(&bytes).unwrap(); +//! assert_eq!(signature, reparsed); +//! +//! // DER round-trip. +//! let der = signature.to_der(); +//! let reparsed_der = Signature::from_der(der.as_bytes()).unwrap(); +//! assert_eq!(signature, reparsed_der); +//! ``` +//! +//! # Public key encoding +//! +//! [`VerifyingKey`] accepts both compressed (33-byte) and uncompressed +//! (65-byte) SEC1 keys via [`VerifyingKey::from_sec1_bytes`]. +//! +//! ```rust +//! # use secp256r1::{SigningKey, VerifyingKey}; +//! # let signing_key = SigningKey::from_slice(&[7u8; 32]).unwrap(); +//! // Uncompressed (0x04 prefix, 65 bytes). +//! let uncompressed = signing_key.verifying_key().to_encoded_point(false); +//! // Compressed (0x02/0x03 prefix, 33 bytes). +//! let compressed = signing_key.verifying_key().to_encoded_point(true); +//! +//! let key1 = VerifyingKey::from_sec1_bytes(uncompressed.as_bytes()).unwrap(); +//! let key2 = VerifyingKey::from_sec1_bytes(compressed.as_bytes()).unwrap(); +//! assert_eq!(key1, key2); +//! ``` +//! +//! # Prehashed signing +//! +//! Use [`SigningKey::sign_prehash`] and [`VerifyingKey::verify_prehash`] when +//! the message digest has already been computed. Both require exactly 32 bytes. +//! +//! # Low-level modules +//! +//! [`field`], [`scalar`], and [`group`] are public for benchmarking and +//! experimentation. Their APIs are not considered stable. +//! +//! # Security +//! +//! This crate is experimental and has not been audited. Signing and +//! signing-key import currently use variable-time scalar multiplication for +//! secret-dependent values. Do not use this crate for production signing or in +//! environments where local timing/cache side channels are in scope. + +#![forbid(unsafe_code)] + +mod constants; +mod ecdsa; +mod error; + +pub mod field; +pub mod group; +pub mod scalar; + +pub use ecdsa::{DerSignature, EncodedPoint, Signature, SigningKey, VerifyingKey}; +pub use error::{Error, Result}; diff --git a/secp256r1/src/scalar.rs b/secp256r1/src/scalar.rs new file mode 100644 index 0000000..b59aa83 --- /dev/null +++ b/secp256r1/src/scalar.rs @@ -0,0 +1,569 @@ +//! 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}; +use zeroize::Zeroize; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Scalar { + limbs: [u64; 4], +} + +impl Zeroize for Scalar { + fn zeroize(&mut self) { + self.limbs.zeroize(); + } +} + +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 mut 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; + } + + table.zeroize(); + 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(), + ); + } + } +} diff --git a/secp256r1/tests/ecdsa.rs b/secp256r1/tests/ecdsa.rs new file mode 100644 index 0000000..7ff2bbb --- /dev/null +++ b/secp256r1/tests/ecdsa.rs @@ -0,0 +1,513 @@ +use openssl::{ + bn::{BigNum, BigNumContext}, + ec::{EcGroup, EcKey, EcPoint}, + ecdsa::EcdsaSig, + hash::MessageDigest, + nid::Nid, + pkey::{PKey, Private}, + sign::Verifier as OpenSslVerifier, +}; +use p256::ecdsa::{ + Signature as P256Signature, SigningKey, VerifyingKey, signature::Signer as _, + signature::Verifier as _, signature::hazmat::PrehashVerifier, +}; +use secp256r1::{ + Error, Signature, SigningKey as RustSigningKey, VerifyingKey as RustVerifyingKey, + group::{AffinePoint, ProjectivePoint}, + scalar::Scalar, +}; +use sha2::{Digest as _, Sha256}; + +const MESSAGE: &[u8] = b"secp256r1 verification benchmark message"; +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, +]; + +fn fixture() -> (Vec, Vec) { + let secret = [7u8; 32]; + let signing_key = SigningKey::from_slice(&secret).unwrap(); + let verifying_key = signing_key.verifying_key(); + let signature: P256Signature = signing_key.sign(MESSAGE); + + ( + verifying_key.to_encoded_point(false).as_bytes().to_vec(), + signature.to_der().as_bytes().to_vec(), + ) +} + +fn sample_bytes(mut seed: u64) -> [u8; N] { + let mut bytes = [0u8; N]; + + for chunk in bytes.chunks_mut(8) { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + chunk.copy_from_slice(&seed.to_be_bytes()[..chunk.len()]); + } + + bytes +} + +fn sample_secret(seed: u64) -> [u8; 32] { + let mut secret = sample_bytes(seed); + + // Keep the sample strictly below the group order without rejection. + secret[0] &= 0x7f; + if secret.iter().all(|byte| *byte == 0) { + secret[31] = 1; + } + + secret +} + +fn openssl_private_key(secret: &[u8; 32]) -> EcKey { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let private_key = BigNum::from_slice(secret).unwrap(); + let mut context = BigNumContext::new().unwrap(); + let mut public_key = EcPoint::new(&group).unwrap(); + public_key + .mul_generator2(&group, &private_key, &mut context) + .unwrap(); + + EcKey::from_private_components(&group, &private_key, &public_key).unwrap() +} + +fn openssl_verifies(public_key: &[u8], message: &[u8], signature_der: &[u8]) -> bool { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let mut context = BigNumContext::new().unwrap(); + let point = EcPoint::from_bytes(&group, public_key, &mut context).unwrap(); + let ec_key = EcKey::from_public_key(&group, &point).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + let mut verifier = OpenSslVerifier::new(MessageDigest::sha256(), &pkey).unwrap(); + + verifier.update(message).unwrap(); + verifier.verify(signature_der).unwrap() +} + +fn small_signature_der() -> Vec { + let mut r = [0u8; 32]; + let mut s = [0u8; 32]; + r[31] = 1; + s[31] = 2; + + Signature::from_scalars(r, s) + .unwrap() + .to_der() + .as_bytes() + .to_vec() +} + +fn order_plus_u32(value: u32) -> ([u8; 32], [u8; 32]) { + let mut scalar = [0u8; 32]; + scalar[28..32].copy_from_slice(&value.to_be_bytes()); + + let mut x = ORDER; + let mut carry = value as u64; + for byte in x.iter_mut().rev() { + let sum = *byte as u64 + (carry & 0xff); + *byte = sum as u8; + carry = (carry >> 8) + (sum >> 8); + } + + assert_eq!(carry, 0); + (scalar, x) +} + +#[test] +fn p256_verifies_der_signature() { + let (public_key, signature_der) = fixture(); + let verifying_key = VerifyingKey::from_sec1_bytes(&public_key).unwrap(); + let signature = P256Signature::from_der(&signature_der).unwrap(); + + assert!(verifying_key.verify(MESSAGE, &signature).is_ok()); +} + +#[test] +fn openssl_verifies_der_signature() { + let (public_key, signature_der) = fixture(); + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let mut context = BigNumContext::new().unwrap(); + let point = EcPoint::from_bytes(&group, &public_key, &mut context).unwrap(); + let ec_key = EcKey::from_public_key(&group, &point).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + let mut verifier = OpenSslVerifier::new(MessageDigest::sha256(), &pkey).unwrap(); + + verifier.update(MESSAGE).unwrap(); + + assert!(verifier.verify(&signature_der).unwrap()); +} + +#[test] +fn verifying_key_verifies_der_signature() { + let (public_key, signature) = fixture(); + let verifying_key = RustVerifyingKey::from_sec1_bytes(&public_key).unwrap(); + let digest = Sha256::digest(MESSAGE); + + assert!( + verifying_key + .verify_prehashed_der(&digest, &signature) + .is_ok() + ); +} + +#[test] +fn top_level_verifying_key_verifies_preparsed_signature() { + let (public_key, signature_der) = fixture(); + let signature = Signature::from_der(&signature_der).unwrap(); + let verifying_key = RustVerifyingKey::from_sec1_bytes(&public_key).unwrap(); + let digest = Sha256::digest(MESSAGE); + + assert!(verifying_key.verify_prehash(&digest, &signature).is_ok()); +} + +#[test] +fn signing_key_derives_same_public_key_as_p256() { + let secret = [7u8; 32]; + let signing_key = RustSigningKey::from_slice(&secret).unwrap(); + let p256_signing_key = SigningKey::from_slice(&secret).unwrap(); + + assert_eq!( + signing_key + .verifying_key() + .to_encoded_point(false) + .as_bytes(), + p256_signing_key + .verifying_key() + .to_encoded_point(false) + .as_bytes() + ); +} + +#[test] +fn signing_key_debug_redacts_secret() { + let signing_key = RustSigningKey::from_slice(&[7u8; 32]).unwrap(); + let debug = format!("{signing_key:?}"); + + assert!(debug.contains("")); + assert!(!debug.contains("secret: Scalar")); +} + +#[test] +fn signing_key_signs_and_verifies_message() { + let secret = [7u8; 32]; + let signing_key = RustSigningKey::from_slice(&secret).unwrap(); + let signature = signing_key.sign(MESSAGE); + + assert!( + signing_key + .verifying_key() + .verify(MESSAGE, &signature) + .is_ok() + ); +} + +#[test] +fn signing_key_signs_and_verifies_prehash() { + let secret = [7u8; 32]; + let signing_key = RustSigningKey::from_slice(&secret).unwrap(); + let digest = Sha256::digest(MESSAGE); + let signature = signing_key.sign_prehash(&digest).unwrap(); + + assert!( + signing_key + .verifying_key() + .verify_prehash(&digest, &signature) + .is_ok() + ); +} + +#[test] +fn compressed_sec1_public_key_round_trips() { + let signing_key = RustSigningKey::from_slice(&[7u8; 32]).unwrap(); + let verifying_key = signing_key.verifying_key(); + let compressed = verifying_key.to_encoded_point(true); + let reparsed = RustVerifyingKey::from_sec1_bytes(compressed.as_bytes()).unwrap(); + + assert_eq!( + reparsed.to_encoded_point(false).as_bytes(), + verifying_key.to_encoded_point(false).as_bytes() + ); + + let p256_verifying_key = VerifyingKey::from_sec1_bytes(compressed.as_bytes()).unwrap(); + assert_eq!( + p256_verifying_key.to_encoded_point(false).as_bytes(), + verifying_key.to_encoded_point(false).as_bytes() + ); +} + +#[test] +fn malformed_compressed_sec1_public_key_is_rejected() { + let signing_key = RustSigningKey::from_slice(&[7u8; 32]).unwrap(); + let mut compressed = signing_key + .verifying_key() + .to_encoded_point(true) + .as_bytes() + .to_vec(); + compressed[0] = 0x05; + + assert!(RustVerifyingKey::from_sec1_bytes(&compressed).is_err()); +} + +#[test] +fn p256_verifies_signature() { + let secret = [7u8; 32]; + let signing_key = RustSigningKey::from_slice(&secret).unwrap(); + let p256_signing_key = SigningKey::from_slice(&secret).unwrap(); + let signature = signing_key.sign(MESSAGE); + let p256_signature = P256Signature::from_slice(&signature.to_bytes()).unwrap(); + + assert!( + p256_signing_key + .verifying_key() + .verify(MESSAGE, &p256_signature) + .is_ok() + ); +} + +#[test] +fn strict_der_accepts_required_integer_leading_zero() { + let mut r = [0u8; 32]; + let mut s = [0u8; 32]; + r[0] = 0x80; + s[31] = 1; + let signature = Signature::from_scalars(r, s).unwrap(); + let der = signature.to_der(); + + assert_eq!(der.as_bytes()[3], 33); + assert_eq!(Signature::from_der(der.as_bytes()).unwrap(), signature); +} + +#[test] +fn strict_der_rejects_long_form_short_sequence_length() { + let der = small_signature_der(); + let mut encoded = Vec::with_capacity(der.len() + 1); + encoded.extend_from_slice(&[0x30, 0x81, der[1]]); + encoded.extend_from_slice(&der[2..]); + + assert!(Signature::from_der(&encoded).is_err()); +} + +#[test] +fn strict_der_rejects_long_form_short_integer_length() { + let encoded = [0x30, 0x07, 0x02, 0x81, 0x01, 0x01, 0x02, 0x01, 0x02]; + + assert!(Signature::from_der(&encoded).is_err()); +} + +#[test] +fn strict_der_rejects_unnecessary_integer_leading_zero() { + let encoded = [0x30, 0x07, 0x02, 0x02, 0x00, 0x01, 0x02, 0x01, 0x02]; + + assert!(Signature::from_der(&encoded).is_err()); +} + +#[test] +fn strict_der_rejects_negative_integer_encoding() { + let mut encoded = vec![0x30, 0x25, 0x02, 0x20, 0x80]; + encoded.extend_from_slice(&[0u8; 31]); + encoded.extend_from_slice(&[0x02, 0x01, 0x01]); + + assert!(Signature::from_der(&encoded).is_err()); +} + +#[test] +fn strict_der_rejects_oversized_integer() { + let mut encoded = vec![0x30, 0x26, 0x02, 0x21, 0x01]; + encoded.extend_from_slice(&[0u8; 32]); + encoded.extend_from_slice(&[0x02, 0x01, 0x01]); + + assert!(Signature::from_der(&encoded).is_err()); +} + +#[test] +fn strict_der_rejects_wrong_integer_tag_order() { + let encoded = [0x30, 0x06, 0x02, 0x01, 0x01, 0x03, 0x01, 0x02]; + + assert!(Signature::from_der(&encoded).is_err()); +} + +#[test] +fn strict_der_rejects_trailing_sequence_data() { + let encoded = [0x30, 0x07, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02, 0x00]; + + assert!(Signature::from_der(&encoded).is_err()); +} + +#[test] +fn strict_der_rejects_outer_trailing_data() { + let mut encoded = small_signature_der(); + encoded.push(0); + + assert!(Signature::from_der(&encoded).is_err()); +} + +#[test] +fn invalid_signature_returns_invalid_signature_error() { + let signing_key = RustSigningKey::from_slice(&[7u8; 32]).unwrap(); + let digest = Sha256::digest(MESSAGE); + let mut signature_bytes = signing_key.sign_prehash(&digest).unwrap().to_bytes(); + signature_bytes[63] ^= 1; + let signature = Signature::from_slice(&signature_bytes).unwrap(); + let signature_der = signature.to_der(); + let verifying_key = signing_key.verifying_key(); + + assert!(matches!( + verifying_key.verify_prehash(&digest, &signature), + Err(Error::InvalidSignature) + )); + assert!(matches!( + verifying_key.verify_prehashed_der(&digest, signature_der.as_bytes()), + Err(Error::InvalidSignature) + )); +} + +#[test] +fn verification_accepts_signature_when_x_coordinate_exceeds_order() { + let (r, point) = (1u32..=1024) + .find_map(|candidate| { + let (r, x) = order_plus_u32(candidate); + let mut compressed = [0u8; 33]; + compressed[0] = 0x02; + compressed[1..].copy_from_slice(&x); + AffinePoint::from_sec1_compressed(compressed).map(|point| (r, point)) + }) + .expect("test range contains a curve point with x >= n"); + let r_inverse = Scalar::from_be_bytes(r).unwrap().invert().unwrap(); + let public_key = ProjectivePoint::mul_affine_scalar_vartime(point, r_inverse.to_be_bytes()) + .to_affine() + .to_sec1_uncompressed() + .unwrap(); + let verifying_key = RustVerifyingKey::from_sec1_bytes(&public_key).unwrap(); + let mut s = [0u8; 32]; + s[31] = 1; + let signature = Signature::from_scalars(r, s).unwrap(); + + assert!(verifying_key.verify_prehash(&[0u8; 32], &signature).is_ok()); +} + +#[test] +fn openssl_verifies_signature_der() { + let secret = [7u8; 32]; + let signing_key = RustSigningKey::from_slice(&secret).unwrap(); + let signature = signing_key.sign(MESSAGE); + let public_key = signing_key.verifying_key().to_encoded_point(false); + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let mut context = BigNumContext::new().unwrap(); + let point = EcPoint::from_bytes(&group, public_key.as_bytes(), &mut context).unwrap(); + let ec_key = EcKey::from_public_key(&group, &point).unwrap(); + let pkey = PKey::from_ec_key(ec_key).unwrap(); + let mut verifier = OpenSslVerifier::new(MessageDigest::sha256(), &pkey).unwrap(); + + verifier.update(MESSAGE).unwrap(); + + assert!(verifier.verify(signature.to_der().as_bytes()).unwrap()); +} + +#[test] +fn random_signing_key_signs_and_verifies() { + let mut rng = rand_core::OsRng; + let signing_key = RustSigningKey::random(&mut rng); + let signature = signing_key.sign(MESSAGE); + + assert!( + signing_key + .verifying_key() + .verify(MESSAGE, &signature) + .is_ok() + ); +} + +#[test] +fn randomized_public_keys_match_p256() { + for seed in 1..=64 { + let secret = sample_secret(seed); + let signing_key = RustSigningKey::from_slice(&secret).unwrap(); + let p256_signing_key = SigningKey::from_slice(&secret).unwrap(); + + assert_eq!( + signing_key + .verifying_key() + .to_encoded_point(false) + .as_bytes(), + p256_signing_key + .verifying_key() + .to_encoded_point(false) + .as_bytes(), + "seed {seed}" + ); + } +} + +#[test] +fn randomized_signatures_verify_across_implementations() { + for seed in 1..=32 { + let secret = sample_secret(seed); + let message = sample_bytes::<113>(seed ^ 0xa5a5_a5a5_a5a5_a5a5); + let signing_key = RustSigningKey::from_slice(&secret).unwrap(); + let verifying_key = signing_key.verifying_key(); + let public_key = verifying_key.to_encoded_point(false); + let p256_signing_key = SigningKey::from_slice(&secret).unwrap(); + let p256_verifying_key = p256_signing_key.verifying_key(); + + let signature = signing_key.sign(&message); + let p256_signature = P256Signature::from_slice(&signature.to_bytes()).unwrap(); + assert!( + p256_verifying_key.verify(&message, &p256_signature).is_ok(), + "p256 rejected our signature for seed {seed}" + ); + assert!( + openssl_verifies( + public_key.as_bytes(), + &message, + signature.to_der().as_bytes() + ), + "OpenSSL rejected our signature for seed {seed}" + ); + + let p256_signature: P256Signature = p256_signing_key.sign(&message); + let signature = Signature::from_slice(&p256_signature.to_bytes()).unwrap(); + assert!( + verifying_key.verify(&message, &signature).is_ok(), + "we rejected p256 signature for seed {seed}" + ); + assert!( + openssl_verifies( + public_key.as_bytes(), + &message, + p256_signature.to_der().as_bytes(), + ), + "OpenSSL rejected p256 signature for seed {seed}" + ); + } +} + +#[test] +fn randomized_openssl_prehash_signatures_verify_with_rust_and_p256() { + for seed in 1..=16 { + let secret = sample_secret(seed); + let digest = Sha256::digest(sample_bytes::<97>(seed ^ 0x5a5a_5a5a_5a5a_5a5a)); + let signing_key = RustSigningKey::from_slice(&secret).unwrap(); + let verifying_key = signing_key.verifying_key(); + let p256_signing_key = SigningKey::from_slice(&secret).unwrap(); + let openssl_key = openssl_private_key(&secret); + let openssl_signature = EcdsaSig::sign(&digest, &openssl_key).unwrap(); + let signature_der = openssl_signature.to_der().unwrap(); + + assert!( + verifying_key + .verify_prehashed_der(&digest, &signature_der) + .is_ok(), + "we rejected OpenSSL signature for seed {seed}" + ); + + let p256_signature = P256Signature::from_der(&signature_der).unwrap(); + assert!( + p256_signing_key + .verifying_key() + .verify_prehash(&digest, &p256_signature) + .is_ok(), + "p256 rejected OpenSSL signature for seed {seed}" + ); + } +} From 7015be5efb094abbf28df3b96d5c2831ed163d81 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Thu, 11 Jun 2026 09:50:16 -0400 Subject: [PATCH 2/2] CI --- secp256r1/README.md | 24 +++---- secp256r1/benches/field.rs | 128 +++++++++++++++++-------------------- secp256r1/benches/group.rs | 82 +----------------------- 3 files changed, 71 insertions(+), 163 deletions(-) diff --git a/secp256r1/README.md b/secp256r1/README.md index 2eb049d..4091c46 100644 --- a/secp256r1/README.md +++ b/secp256r1/README.md @@ -148,11 +148,11 @@ cargo bench Focused benchmark groups: ```sh -cargo bench --bench verify -cargo bench --bench sign -cargo bench --bench field -cargo bench --bench scalar -cargo bench --bench group +cargo bench -p secp256r1 --bench verify +cargo bench -p secp256r1 --bench sign +cargo bench -p secp256r1 --bench field +cargo bench -p secp256r1 --bench scalar +cargo bench -p secp256r1 --bench group ``` Representative local results from this workspace: @@ -178,11 +178,11 @@ Representative local results from this workspace: | Benchmark | rust | p256 | OpenSSL | |---|---:|---:|---:| -| point double | 89.851 ns | 210.85 ns | 57.582 ns nistz | -| point add | 141.95 ns | 229.71 ns | 99.978 ns nistz | -| mixed add | 101.93 ns | 205.46 ns | 75.010 ns nistz | -| base scalar mul | 3.236 us fixed-base window8 | 76.277 us | 3.543 us | -| double scalar mul | 32.682 us separate wNAF6 | 151.99 us | 25.704 us | +| point double | 81.611 ns | 195.83 ns | 213.29 ns public EC | +| point add | 133.17 ns | 213.19 ns | 210.09 ns public EC | +| mixed add | 97.422 ns | 193.13 ns | n/a | +| base scalar mul | 3.033 us fixed-base window8 | 71.555 us | 3.415 us | +| double scalar mul | 31.197 us separate wNAF6 | 143.96 us | 24.258 us | Benchmark numbers are machine- and compiler-dependent. Re-run locally before making performance decisions. @@ -200,5 +200,5 @@ making performance decisions. ## Safety -The crate forbids `unsafe` in library code. Benchmark code links to OpenSSL -internal/public routines for comparison and is not part of the library. +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 index 65d8d66..214fff6 100644 --- a/secp256r1/benches/field.rs +++ b/secp256r1/benches/field.rs @@ -1,4 +1,5 @@ use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use openssl::bn::{BigNum, BigNumContext}; use p256::{FieldElement as P256FieldElement, elliptic_curve::ff::PrimeField}; use secp256r1::field::FieldElement; @@ -10,43 +11,29 @@ 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, +]; -#[link(name = "crypto")] -unsafe extern "C" { - fn ecp_nistz256_to_mont(res: *mut u64, input: *const u64); - fn ecp_nistz256_add(res: *mut u64, a: *const u64, b: *const u64); - fn ecp_nistz256_sub(res: *mut u64, a: *const u64, b: *const u64); - fn ecp_nistz256_mul_mont(res: *mut u64, a: *const u64, b: *const u64); - fn ecp_nistz256_sqr_mont(res: *mut u64, a: *const u64); -} - -#[derive(Clone, Copy)] struct Fixture { rust_a: FieldElement, rust_b: FieldElement, - openssl_a: [u64; 4], - openssl_b: [u64; 4], + openssl_a: BigNum, + openssl_b: BigNum, + openssl_p: BigNum, p256_a: P256FieldElement, p256_b: P256FieldElement, } impl Fixture { fn new() -> Self { - let raw_a = limbs_from_be_bytes(A); - let raw_b = limbs_from_be_bytes(B); - let mut openssl_a = [0u64; 4]; - let mut openssl_b = [0u64; 4]; - - unsafe { - ecp_nistz256_to_mont(openssl_a.as_mut_ptr(), raw_a.as_ptr()); - ecp_nistz256_to_mont(openssl_b.as_mut_ptr(), raw_b.as_ptr()); - } - Self { rust_a: FieldElement::from_be_bytes(A).unwrap(), rust_b: FieldElement::from_be_bytes(B).unwrap(), - openssl_a, - openssl_b, + 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), } @@ -57,18 +44,10 @@ fn p256_field(bytes: [u8; 32]) -> P256FieldElement { Option::from(P256FieldElement::from_repr(bytes.into())).unwrap() } -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().unwrap()); - } - - limbs -} - 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| { @@ -79,17 +58,17 @@ fn bench_field_add(c: &mut Criterion) { b.iter(|| black_box(fixture.p256_a) + black_box(fixture.p256_b)); }); - group.bench_function("openssl_ecp_nistz256", |b| { + group.bench_function("openssl_bn_mod_add", |b| { b.iter(|| { - let mut out = [0u64; 4]; - unsafe { - ecp_nistz256_add( - out.as_mut_ptr(), - black_box(fixture.openssl_a).as_ptr(), - black_box(fixture.openssl_b).as_ptr(), - ); - } - black_box(out) + 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); }); }); @@ -98,6 +77,8 @@ fn bench_field_add(c: &mut Criterion) { 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| { @@ -108,17 +89,17 @@ fn bench_field_sub(c: &mut Criterion) { b.iter(|| black_box(fixture.p256_a) - black_box(fixture.p256_b)); }); - group.bench_function("openssl_ecp_nistz256", |b| { + group.bench_function("openssl_bn_mod_sub", |b| { b.iter(|| { - let mut out = [0u64; 4]; - unsafe { - ecp_nistz256_sub( - out.as_mut_ptr(), - black_box(fixture.openssl_a).as_ptr(), - black_box(fixture.openssl_b).as_ptr(), - ); - } - black_box(out) + 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); }); }); @@ -127,6 +108,8 @@ fn bench_field_sub(c: &mut Criterion) { 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| { @@ -137,17 +120,17 @@ fn bench_field_mul(c: &mut Criterion) { b.iter(|| black_box(fixture.p256_a) * black_box(fixture.p256_b)); }); - group.bench_function("openssl_ecp_nistz256", |b| { + group.bench_function("openssl_bn_mod_mul", |b| { b.iter(|| { - let mut out = [0u64; 4]; - unsafe { - ecp_nistz256_mul_mont( - out.as_mut_ptr(), - black_box(fixture.openssl_a).as_ptr(), - black_box(fixture.openssl_b).as_ptr(), - ); - } - black_box(out) + 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); }); }); @@ -156,6 +139,8 @@ fn bench_field_mul(c: &mut Criterion) { 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| { @@ -166,13 +151,16 @@ fn bench_field_square(c: &mut Criterion) { b.iter(|| black_box(fixture.p256_a).square()); }); - group.bench_function("openssl_ecp_nistz256", |b| { + group.bench_function("openssl_bn_mod_sqr", |b| { b.iter(|| { - let mut out = [0u64; 4]; - unsafe { - ecp_nistz256_sqr_mont(out.as_mut_ptr(), black_box(fixture.openssl_a).as_ptr()); - } - black_box(out) + openssl_out + .mod_sqr( + black_box(&fixture.openssl_a), + black_box(&fixture.openssl_p), + &mut context, + ) + .unwrap(); + black_box(&openssl_out); }); }); diff --git a/secp256r1/benches/group.rs b/secp256r1/benches/group.rs index b89efde..ef184a2 100644 --- a/secp256r1/benches/group.rs +++ b/secp256r1/benches/group.rs @@ -8,36 +8,7 @@ use p256::{ AffinePoint as P256AffinePoint, ProjectivePoint as P256ProjectivePoint, Scalar, elliptic_curve::{ff::PrimeField, group::Group}, }; -use secp256r1::{ - field::FieldElement, - group::{AffinePoint, ProjectivePoint}, -}; - -#[repr(C)] -#[derive(Clone, Copy, Default)] -struct NistzPoint { - x: [u64; 4], - y: [u64; 4], - z: [u64; 4], -} - -#[repr(C)] -#[derive(Clone, Copy, Default)] -struct NistzAffinePoint { - x: [u64; 4], - y: [u64; 4], -} - -#[link(name = "crypto")] -unsafe extern "C" { - fn ecp_nistz256_point_double(res: *mut NistzPoint, a: *const NistzPoint); - fn ecp_nistz256_point_add(res: *mut NistzPoint, a: *const NistzPoint, b: *const NistzPoint); - fn ecp_nistz256_point_add_affine( - res: *mut NistzPoint, - a: *const NistzPoint, - b: *const NistzAffinePoint, - ); -} +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, @@ -84,32 +55,10 @@ fn openssl_fixture() -> (EcGroup, BigNumContext, EcPoint, EcPoint) { (group, context, generator, double_generator) } -fn openssl_nistz_fixture() -> (NistzPoint, NistzPoint, NistzAffinePoint) { - let affine = AffinePoint::generator(); - let generator = NistzPoint { - x: affine.x().unwrap().montgomery_limbs(), - y: affine.y().unwrap().montgomery_limbs(), - z: FieldElement::ONE.montgomery_limbs(), - }; - let affine_generator = NistzAffinePoint { - x: generator.x, - y: generator.y, - }; - let mut double_generator = NistzPoint::default(); - - unsafe { - ecp_nistz256_point_double(&mut double_generator, &generator); - } - - (generator, double_generator, affine_generator) -} - fn bench_group_double(c: &mut Criterion) { let fixture = Fixture::new(); let (openssl_group, mut openssl_context, openssl_g, _) = openssl_fixture(); - let (nistz_g, _, _) = openssl_nistz_fixture(); let mut openssl_out = EcPoint::new(&openssl_group).unwrap(); - let mut nistz_out = NistzPoint::default(); let mut group = c.benchmark_group("secp256r1_group_double"); group.bench_function("rust", |b| { @@ -134,22 +83,13 @@ fn bench_group_double(c: &mut Criterion) { }); }); - group.bench_function("openssl_nistz256_point_double", |b| { - b.iter(|| unsafe { - ecp_nistz256_point_double(&mut nistz_out, black_box(&nistz_g)); - black_box(nistz_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 (nistz_g, nistz_2g, _) = openssl_nistz_fixture(); let mut openssl_out = EcPoint::new(&openssl_group).unwrap(); - let mut nistz_out = NistzPoint::default(); let mut group = c.benchmark_group("secp256r1_group_add"); group.bench_function("rust", |b| { @@ -174,20 +114,11 @@ fn bench_group_add(c: &mut Criterion) { }); }); - group.bench_function("openssl_nistz256_point_add", |b| { - b.iter(|| unsafe { - ecp_nistz256_point_add(&mut nistz_out, black_box(&nistz_2g), black_box(&nistz_g)); - black_box(nistz_out); - }); - }); - group.finish(); } fn bench_group_mixed_add(c: &mut Criterion) { let fixture = Fixture::new(); - let (_, nistz_2g, nistz_affine_g) = openssl_nistz_fixture(); - let mut nistz_out = NistzPoint::default(); let mut group = c.benchmark_group("secp256r1_group_mixed_add"); group.bench_function("rust", |b| { @@ -198,17 +129,6 @@ fn bench_group_mixed_add(c: &mut Criterion) { b.iter(|| black_box(fixture.p256_2g) + black_box(fixture.p256_affine_g)); }); - group.bench_function("openssl_nistz256_point_add_affine", |b| { - b.iter(|| unsafe { - ecp_nistz256_point_add_affine( - &mut nistz_out, - black_box(&nistz_2g), - black_box(&nistz_affine_g), - ); - black_box(nistz_out); - }); - }); - group.finish(); }