From ea9cddcecad94c76e7ca88eac9f59508469dd342 Mon Sep 17 00:00:00 2001 From: Suhas Date: Sun, 12 Jul 2026 21:04:47 -0700 Subject: [PATCH 1/5] Add crypto provider abstraction layer Refactor crypto operations behind a unified adapter interface supporting both OpenSSL and BoringSSL backends. --- CMakeLists.txt | 51 +- README.md | 4 +- benchmarks/bench_crypto_provider.cpp | 250 ++++++ docs/crypto_backend.md | 82 ++ include/privacy_pass/core/types.hpp | 9 +- justfile | 105 ++- src/core/types.cpp | 16 +- src/crypto/blind_rsa.cpp | 705 +++------------- src/crypto/common.hpp | 99 +++ src/crypto/compat.hpp | 488 ++++++++++++ src/crypto/hash.cpp | 149 +--- src/crypto/init.cpp | 19 + src/crypto/random.cpp | 12 - src/crypto/voprf.cpp | 1109 +++++++------------------- tests/test_blind_rsa.cpp | 6 + tests/test_crypto_provider.cpp | 597 ++++++++++++++ 16 files changed, 2098 insertions(+), 1603 deletions(-) create mode 100644 benchmarks/bench_crypto_provider.cpp create mode 100644 docs/crypto_backend.md create mode 100644 src/crypto/common.hpp create mode 100644 src/crypto/compat.hpp create mode 100644 src/crypto/init.cpp create mode 100644 tests/test_crypto_provider.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5885f0e..4f72eb4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,9 @@ option(PRIVACY_PASS_BUILD_MOQ "Build MOQ extension" ON) option(PRIVACY_PASS_BUILD_EXAMPLES "Build examples" ON) option(PRIVACY_PASS_ENABLE_SANITIZERS "Enable address and undefined sanitizers" OFF) +# Crypto backend selection: "auto", "openssl", or "boringssl" +set(PRIVACY_PASS_CRYPTO_BACKEND "auto" CACHE STRING "Crypto backend: auto, openssl, or boringssl") + # Sanitizer flags (applied globally since they affect linking) if(PRIVACY_PASS_ENABLE_SANITIZERS) add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer) @@ -29,9 +32,35 @@ function(privacy_pass_set_warnings target) endfunction() # Dependencies -find_package(OpenSSL 3.0 REQUIRED) - include(FetchContent) +include(CheckIncludeFileCXX) + +# Find OpenSSL (works for both OpenSSL and BoringSSL) +if(PRIVACY_PASS_CRYPTO_BACKEND STREQUAL "auto" OR PRIVACY_PASS_CRYPTO_BACKEND STREQUAL "openssl") + find_package(OpenSSL 3.0 QUIET) + if(NOT OPENSSL_FOUND) + find_package(OpenSSL REQUIRED) + endif() +else() + find_package(OpenSSL REQUIRED) +endif() + +# Detect BoringSSL via header +set(CMAKE_REQUIRED_INCLUDES ${OPENSSL_INCLUDE_DIR}) +check_include_file_cxx("openssl/is_boringssl.h" HAVE_BORINGSSL) + +# Resolve the crypto backend +if(PRIVACY_PASS_CRYPTO_BACKEND STREQUAL "boringssl" OR + (PRIVACY_PASS_CRYPTO_BACKEND STREQUAL "auto" AND HAVE_BORINGSSL)) + set(CRYPTO_BACKEND "boringssl") +elseif(PRIVACY_PASS_CRYPTO_BACKEND STREQUAL "openssl" OR + PRIVACY_PASS_CRYPTO_BACKEND STREQUAL "auto") + set(CRYPTO_BACKEND "openssl") +else() + message(FATAL_ERROR "Unknown crypto backend: ${PRIVACY_PASS_CRYPTO_BACKEND}") +endif() + +message(STATUS "Privacy Pass crypto backend: ${CRYPTO_BACKEND}") # Silence MSVC deprecation warnings for spdlog's bundled fmt library if(MSVC) @@ -51,7 +80,6 @@ FetchContent_MakeAvailable(spdlog) # Fetch doctest if building tests if(PRIVACY_PASS_BUILD_TESTS) - include(FetchContent) FetchContent_Declare( doctest GIT_REPOSITORY https://github.com/doctest/doctest.git @@ -70,7 +98,6 @@ endif() # Fetch Google Benchmark if building benchmarks if(PRIVACY_PASS_BUILD_BENCHMARKS) - include(FetchContent) FetchContent_Declare( benchmark GIT_REPOSITORY https://github.com/google/benchmark.git @@ -105,12 +132,13 @@ target_link_libraries(privacy_pass_core PRIVATE spdlog::spdlog ) -# Crypto library +# Crypto library — single source files, backend selected via compat.hpp add_library(privacy_pass_crypto - src/crypto/blind_rsa.cpp - src/crypto/voprf.cpp src/crypto/hash.cpp src/crypto/random.cpp + src/crypto/blind_rsa.cpp + src/crypto/voprf.cpp + src/crypto/init.cpp ) privacy_pass_set_warnings(privacy_pass_crypto) @@ -119,6 +147,13 @@ target_link_libraries(privacy_pass_crypto PUBLIC ) target_link_libraries(privacy_pass_crypto PRIVATE spdlog::spdlog) +# Backend compile definition so code can query which backend is active +if(CRYPTO_BACKEND STREQUAL "boringssl") + target_compile_definitions(privacy_pass_crypto PUBLIC PRIVACY_PASS_WITH_BORINGSSL) +else() + target_compile_definitions(privacy_pass_crypto PUBLIC PRIVACY_PASS_WITH_OPENSSL) +endif() + # HTTP layer add_library(privacy_pass_http src/http/auth_scheme.cpp @@ -241,6 +276,7 @@ if(PRIVACY_PASS_BUILD_TESTS) tests/test_origin.cpp tests/test_integration.cpp tests/test_http.cpp + tests/test_crypto_provider.cpp ) if(PRIVACY_PASS_BUILD_MOQ) list(APPEND TEST_SOURCES tests/test_moq.cpp) @@ -271,6 +307,7 @@ if(PRIVACY_PASS_BUILD_BENCHMARKS) benchmarks/bench_crypto.cpp benchmarks/bench_token_issuance.cpp benchmarks/bench_serialization.cpp + benchmarks/bench_crypto_provider.cpp ) privacy_pass_set_warnings(privacy_pass_benchmarks) diff --git a/README.md b/README.md index 774dab0..89888ef 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,12 @@ C++ implementation of the Privacy Pass protocol ([RFC9576](https://datatracker.i - Token challenges, requests, responses, and redemption - Optional MOQ extension ([draft-ietf-moq-privacy-pass-auth-02](https://datatracker.ietf.org/doc/html/draft-ietf-moq-privacy-pass-auth-02)) - Modern C++23 with `std::expected` error handling +- Pluggable crypto backend: OpenSSL 3.x or BoringSSL ([details](docs/crypto_backend.md)) ## Building -Requires CMake 3.20+, C++23 compiler, and OpenSSL 3.x. +Requires CMake 3.20+, C++23 compiler, and OpenSSL 3.x or BoringSSL. +See [docs/crypto_backend.md](docs/crypto_backend.md) for multi-backend setup. ```bash cmake -B build diff --git a/benchmarks/bench_crypto_provider.cpp b/benchmarks/bench_crypto_provider.cpp new file mode 100644 index 0000000..e16ad56 --- /dev/null +++ b/benchmarks/bench_crypto_provider.cpp @@ -0,0 +1,250 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems +// SPDX-License-Identifier: BSD-2-Clause + +// Benchmarks labeled by crypto backend for easy comparison between +// OpenSSL and BoringSSL builds. + +#include +#include +#include +#include +#include + +#include + +using namespace privacy_pass; +using namespace privacy_pass::crypto; + +// Backend label for benchmark names +#if defined(PRIVACY_PASS_WITH_BORINGSSL) +static const std::string BACKEND = "BoringSSL"; +#else +static const std::string BACKEND = "OpenSSL"; +#endif + +// --- Hash benchmarks --- + +static void BM_Provider_SHA256(benchmark::State& state) { + auto data = random_bytes(static_cast(state.range(0))).value(); + for (auto _ : state) { + auto result = sha256(ByteView(data.data(), data.size())); + benchmark::DoNotOptimize(result); + } + state.SetBytesProcessed(state.iterations() * state.range(0)); + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_SHA256)->Arg(32)->Arg(256)->Arg(1024)->Arg(4096); + +static void BM_Provider_SHA384(benchmark::State& state) { + auto data = random_bytes(static_cast(state.range(0))).value(); + for (auto _ : state) { + auto result = sha384(ByteView(data.data(), data.size())); + benchmark::DoNotOptimize(result); + } + state.SetBytesProcessed(state.iterations() * state.range(0)); + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_SHA384)->Arg(32)->Arg(256)->Arg(1024)->Arg(4096); + +static void BM_Provider_HMAC_SHA256(benchmark::State& state) { + auto key = random_bytes(32).value(); + auto data = random_bytes(static_cast(state.range(0))).value(); + for (auto _ : state) { + auto result = hmac_sha256( + ByteView(key.data(), key.size()), + ByteView(data.data(), data.size())); + benchmark::DoNotOptimize(result); + } + state.SetBytesProcessed(state.iterations() * state.range(0)); + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_HMAC_SHA256)->Arg(32)->Arg(256)->Arg(1024); + +static void BM_Provider_HKDF_Extract(benchmark::State& state) { + auto salt = random_bytes(32).value(); + auto ikm = random_bytes(32).value(); + for (auto _ : state) { + auto result = hkdf_extract_sha256( + ByteView(salt.data(), salt.size()), + ByteView(ikm.data(), ikm.size())); + benchmark::DoNotOptimize(result); + } + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_HKDF_Extract); + +static void BM_Provider_HKDF_Expand(benchmark::State& state) { + auto salt = random_bytes(32).value(); + auto ikm = random_bytes(32).value(); + auto prk = hkdf_extract_sha256( + ByteView(salt.data(), salt.size()), + ByteView(ikm.data(), ikm.size())).value(); + auto info = random_bytes(16).value(); + + for (auto _ : state) { + auto result = hkdf_expand_sha256( + ByteView(prk.data(), prk.size()), + ByteView(info.data(), info.size()), + 64); + benchmark::DoNotOptimize(result); + } + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_HKDF_Expand); + +// --- Random benchmarks --- + +static void BM_Provider_RandomBytes(benchmark::State& state) { + for (auto _ : state) { + auto result = random_bytes(static_cast(state.range(0))); + benchmark::DoNotOptimize(result); + } + state.SetBytesProcessed(state.iterations() * state.range(0)); + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_RandomBytes)->Arg(32)->Arg(256)->Arg(1024); + +// --- Blind RSA benchmarks --- + +static void BM_Provider_BlindRSA_KeyGen(benchmark::State& state) { + for (auto _ : state) { + auto keypair = BlindRsaPrivateKey::generate(); + benchmark::DoNotOptimize(keypair); + } + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_BlindRSA_KeyGen); + +static void BM_Provider_BlindRSA_Blind(benchmark::State& state) { + auto keypair = BlindRsaPrivateKey::generate().value(); + auto msg = random_bytes(98).value(); + for (auto _ : state) { + auto result = keypair.second.blind(ByteView(msg.data(), msg.size())); + benchmark::DoNotOptimize(result); + } + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_BlindRSA_Blind); + +static void BM_Provider_BlindRSA_BlindSign(benchmark::State& state) { + auto keypair = BlindRsaPrivateKey::generate().value(); + auto msg = random_bytes(98).value(); + auto blinding = keypair.second.blind(ByteView(msg.data(), msg.size())).value(); + for (auto _ : state) { + auto result = keypair.first.blind_sign( + ByteView(blinding.blinded_msg.data(), blinding.blinded_msg.size())); + benchmark::DoNotOptimize(result); + } + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_BlindRSA_BlindSign); + +static void BM_Provider_BlindRSA_Verify(benchmark::State& state) { + auto keypair = BlindRsaPrivateKey::generate().value(); + auto msg = random_bytes(98).value(); + auto sig = keypair.first.sign(ByteView(msg.data(), msg.size())).value(); + for (auto _ : state) { + auto result = keypair.second.verify( + ByteView(msg.data(), msg.size()), + ByteView(sig.data(), sig.size())); + benchmark::DoNotOptimize(result); + } + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_BlindRSA_Verify); + +static void BM_Provider_BlindRSA_FullProtocol(benchmark::State& state) { + auto keypair = BlindRsaPrivateKey::generate().value(); + auto msg = random_bytes(98).value(); + for (auto _ : state) { + auto blinding = keypair.second.blind(ByteView(msg.data(), msg.size())).value(); + auto blind_sig = keypair.first.blind_sign( + ByteView(blinding.blinded_msg.data(), blinding.blinded_msg.size())).value(); + auto sig = keypair.second.finalize( + ByteView(blind_sig.data(), blind_sig.size()), + blinding, + ByteView(msg.data(), msg.size())).value(); + auto valid = keypair.second.verify( + ByteView(msg.data(), msg.size()), + ByteView(sig.data(), sig.size())); + benchmark::DoNotOptimize(valid); + } + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_BlindRSA_FullProtocol); + +// --- VOPRF benchmarks --- + +static void BM_Provider_VOPRF_KeyGen(benchmark::State& state) { + for (auto _ : state) { + auto keypair = VoprfPrivateKey::generate(); + benchmark::DoNotOptimize(keypair); + } + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_VOPRF_KeyGen); + +static void BM_Provider_VOPRF_Blind(benchmark::State& state) { + auto keypair = VoprfPrivateKey::generate().value(); + auto pub_bytes = keypair.second.to_bytes().value(); + auto client_key = VoprfPublicKey::from_bytes( + ByteView(pub_bytes.data(), pub_bytes.size())).value(); + VoprfClient client(std::move(client_key)); + auto input = random_bytes(64).value(); + + for (auto _ : state) { + auto result = client.blind(ByteView(input.data(), input.size())); + benchmark::DoNotOptimize(result); + } + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_VOPRF_Blind); + +static void BM_Provider_VOPRF_Evaluate(benchmark::State& state) { + auto keypair = VoprfPrivateKey::generate().value(); + auto pub_bytes = keypair.second.to_bytes().value(); + auto client_key = VoprfPublicKey::from_bytes( + ByteView(pub_bytes.data(), pub_bytes.size())).value(); + auto priv_bytes = keypair.first.to_bytes().value(); + auto server_key = VoprfPrivateKey::from_bytes(priv_bytes.view()).value(); + + VoprfClient client(std::move(client_key)); + VoprfServer server(std::move(server_key)); + + auto input = random_bytes(64).value(); + auto blind_data = client.blind(ByteView(input.data(), input.size())).value(); + + for (auto _ : state) { + auto result = server.blind_evaluate( + ByteView(blind_data.blinded_element.data(), blind_data.blinded_element.size())); + benchmark::DoNotOptimize(result); + } + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_VOPRF_Evaluate); + +static void BM_Provider_VOPRF_FullProtocol(benchmark::State& state) { + auto keypair = VoprfPrivateKey::generate().value(); + auto pub_bytes = keypair.second.to_bytes().value(); + auto priv_bytes = keypair.first.to_bytes().value(); + + auto input = random_bytes(64).value(); + + for (auto _ : state) { + auto ck = VoprfPublicKey::from_bytes(ByteView(pub_bytes.data(), pub_bytes.size())).value(); + VoprfClient client(std::move(ck)); + auto sk = VoprfPrivateKey::from_bytes(priv_bytes.view()).value(); + VoprfServer server(std::move(sk)); + + auto blind_data = client.blind(ByteView(input.data(), input.size())).value(); + auto eval = server.blind_evaluate( + ByteView(blind_data.blinded_element.data(), blind_data.blinded_element.size())).value(); + auto output = client.finalize(blind_data, eval).value(); + auto valid = server.verify_finalize( + ByteView(input.data(), input.size()), + ByteView(output.data(), output.size())); + benchmark::DoNotOptimize(valid); + } + state.SetLabel(BACKEND); +} +BENCHMARK(BM_Provider_VOPRF_FullProtocol); diff --git a/docs/crypto_backend.md b/docs/crypto_backend.md new file mode 100644 index 0000000..13f2f69 --- /dev/null +++ b/docs/crypto_backend.md @@ -0,0 +1,82 @@ +# Crypto Backend Selection + +privacypass-cpp supports **OpenSSL 3.x** and **BoringSSL** as crypto backends, selected at compile time. + +## Quick Start + +### OpenSSL (default) + +```bash +cmake -B build +cmake --build build -j$(nproc) +./build/privacy_pass_tests +``` + +### BoringSSL + +Build BoringSSL from source first: + +```bash +git clone https://boringssl.googlesource.com/boringssl +cd boringssl && cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Release +cmake --build build --target crypto ssl +cd .. +``` + +Then point privacypass-cpp at it: + +```bash +cmake -B build-boringssl \ + -DPRIVACY_PASS_CRYPTO_BACKEND=boringssl \ + -DOPENSSL_ROOT_DIR=$(pwd)/boringssl \ + -DOPENSSL_INCLUDE_DIR=$(pwd)/boringssl/include \ + -DOPENSSL_CRYPTO_LIBRARY=$(pwd)/boringssl/build/libcrypto.a \ + -DOPENSSL_SSL_LIBRARY=$(pwd)/boringssl/build/libssl.a + +cmake --build build-boringssl -j$(nproc) +./build-boringssl/privacy_pass_tests +``` + +## CMake Options + +| Option | Values | Default | Description | +|--------|--------|---------|-------------| +| `PRIVACY_PASS_CRYPTO_BACKEND` | `auto`, `openssl`, `boringssl` | `auto` | Crypto backend selection | + +In `auto` mode, the build checks for `openssl/is_boringssl.h` in the include path. If found, BoringSSL is selected; otherwise OpenSSL. + +## Compile Definitions + +The active backend is exposed via a compile definition on `privacy_pass_crypto`: + +- `PRIVACY_PASS_WITH_OPENSSL` — OpenSSL 3.x backend +- `PRIVACY_PASS_WITH_BORINGSSL` — BoringSSL backend + +## Architecture + +``` +src/crypto/ +├── common.hpp # RAII wrappers for OpenSSL/BoringSSL C types +├── compat.hpp # Thin compatibility layer bridging API differences +├── init.cpp # secure_clear + backend init/shutdown +├── hash.cpp # SHA-256, SHA-384, HMAC, HKDF +├── random.cpp # RAND_bytes +├── blind_rsa.cpp # Blind RSA (RFC 9474) +└── voprf.cpp # VOPRF P-384 (RFC 9497) +``` + +All API differences between OpenSSL and BoringSSL are isolated in `compat.hpp`. The source files are shared — no per-backend duplication. Backend selection is purely compile-time via `#ifdef` in the compat layer, with zero runtime overhead. + +## Known Differences + +- **RSA-PSS SPKI**: OpenSSL uses OID `1.2.840.113549.1.1.10` (rsaPSS) for RSA-PSS keys. BoringSSL uses plain RSA OID `1.2.840.113549.1.1.1`. SPKI bytes generated by one backend may not be parseable by the other. +- **Key generation**: Keys generated by either backend are functionally interoperable at the protocol level (blinded messages, signatures, tokens all work cross-backend). + +## Benchmarking + +Run benchmarks with backend labels: + +```bash +./build/privacy_pass_benchmarks --benchmark_filter="BM_Provider_" +./build-boringssl/privacy_pass_benchmarks --benchmark_filter="BM_Provider_" +``` diff --git a/include/privacy_pass/core/types.hpp b/include/privacy_pass/core/types.hpp index 9a7c57e..6e93161 100644 --- a/include/privacy_pass/core/types.hpp +++ b/include/privacy_pass/core/types.hpp @@ -12,10 +12,12 @@ #include #include -#include - namespace privacy_pass { +// Portable secure memory clearing (implemented by the active crypto backend). +// Guaranteed not to be optimized away by the compiler. +void secure_clear(void* ptr, size_t len) noexcept; + // Token type constants (RFC 9578) enum class TokenType : uint16_t { VOPRF_P384_SHA384 = 0x0001, @@ -136,8 +138,7 @@ class SecureBytes { void clear() noexcept { if (!data_.empty()) { - // Use OPENSSL_cleanse for guaranteed secure memory clearing - OPENSSL_cleanse(data_.data(), data_.size()); + secure_clear(data_.data(), data_.size()); data_.clear(); } } diff --git a/justfile b/justfile index 05c76ad..456637a 100644 --- a/justfile +++ b/justfile @@ -3,15 +3,31 @@ default: @just --list -# Build directory and options -build_dir := "build" +# Build options build_type := "Release" moq := "ON" sanitizers := "OFF" +backend := "openssl" -# Configure CMake +# BoringSSL path (override with: just boringssl_dir=/path/to/boringssl ...) +boringssl_dir := env("BORINGSSL_DIR", "../boringssl") + +# Resolved build directory per backend +build_dir := if backend == "boringssl" { "build-boringssl" } else { "build" } + +# ── Configure ──────────────────────────────────────────────────────────────── + +# Configure CMake for the selected backend configure: - cmake -B {{build_dir}} -DCMAKE_BUILD_TYPE={{build_type}} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DPRIVACY_PASS_BUILD_MOQ={{moq}} -DPRIVACY_PASS_ENABLE_SANITIZERS={{sanitizers}} + cmake -B {{build_dir}} \ + -DCMAKE_BUILD_TYPE={{build_type}} \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DPRIVACY_PASS_BUILD_MOQ={{moq}} \ + -DPRIVACY_PASS_ENABLE_SANITIZERS={{sanitizers}} \ + -DPRIVACY_PASS_CRYPTO_BACKEND={{backend}} \ + {{ if backend == "boringssl" { "-DOPENSSL_ROOT_DIR=" + boringssl_dir + " -DOPENSSL_INCLUDE_DIR=" + boringssl_dir + "/include -DOPENSSL_CRYPTO_LIBRARY=" + boringssl_dir + "/build/libcrypto.a -DOPENSSL_SSL_LIBRARY=" + boringssl_dir + "/build/libssl.a" } else { "" } }} + +# ── Build ──────────────────────────────────────────────────────────────────── # Build all targets build: configure @@ -19,7 +35,13 @@ build: configure # Build debug (shorthand for build_type=Debug sanitizers=ON) build-debug: - just build_type=Debug sanitizers=ON build + just build_type=Debug sanitizers=ON backend={{backend}} build + +# Full rebuild +rebuild: + just clean backend={{backend}} && just moq={{moq}} build_type={{build_type}} sanitizers={{sanitizers}} backend={{backend}} build + +# ── Test ───────────────────────────────────────────────────────────────────── # Run tests test: build @@ -29,21 +51,82 @@ test: build test-verbose: build cd {{build_dir}} && ./privacy_pass_tests --success +# Run a specific test suite +test-suite suite: build + cd {{build_dir}} && ./privacy_pass_tests --test-suite="{{suite}}" + +# Run crypto provider tests only +test-crypto: build + cd {{build_dir}} && ./privacy_pass_tests --test-suite="Crypto Provider" + +# ── Benchmarks ─────────────────────────────────────────────────────────────── + # Run benchmarks bench: build cd {{build_dir}} && ./privacy_pass_benchmarks +# Run provider-labeled benchmarks (for cross-backend comparison) +bench-provider: build + cd {{build_dir}} && ./privacy_pass_benchmarks --benchmark_filter="BM_Provider_" + # Run benchmarks with specific filter bench-filter filter: build cd {{build_dir}} && ./privacy_pass_benchmarks --benchmark_filter={{filter}} -# Clean build directory +# Save benchmark results as JSON +bench-json: build + cd {{build_dir}} && ./privacy_pass_benchmarks --benchmark_out={{backend}}_bench.json --benchmark_out_format=json + +# ── Multi-backend ──────────────────────────────────────────────────────────── + +# Build and test with OpenSSL +test-openssl: + just backend=openssl test + +# Build and test with BoringSSL +test-boringssl: + just backend=boringssl test + +# Build and test both backends +test-all: + just backend=openssl test + just backend=boringssl test + +# Run provider benchmarks on both backends +bench-all: + just backend=openssl bench-provider + @echo "" + just backend=boringssl bench-provider + +# Save benchmark JSON for both backends +bench-all-json: + just backend=openssl bench-json + just backend=boringssl bench-json + +# ── BoringSSL setup ────────────────────────────────────────────────────────── + +# Clone and build BoringSSL (one-time setup) +setup-boringssl: + #!/usr/bin/env bash + set -euo pipefail + if [ ! -d "{{boringssl_dir}}" ]; then + git clone https://boringssl.googlesource.com/boringssl "{{boringssl_dir}}" + fi + cmake -B "{{boringssl_dir}}/build" -S "{{boringssl_dir}}" -GNinja -DCMAKE_BUILD_TYPE=Release + cmake --build "{{boringssl_dir}}/build" --target crypto ssl + echo "BoringSSL built at {{boringssl_dir}}" + +# ── Cleanup ────────────────────────────────────────────────────────────────── + +# Clean build directory for selected backend clean: rm -rf {{build_dir}} -# Full rebuild -rebuild: - just clean && just moq={{moq}} build_type={{build_type}} sanitizers={{sanitizers}} build +# Clean all build directories +clean-all: + rm -rf build build-boringssl + +# ── Code quality ───────────────────────────────────────────────────────────── # Format code format: @@ -61,6 +144,8 @@ lint: compile-commands: configure cp {{build_dir}}/compile_commands.json . +# ── Dependencies ───────────────────────────────────────────────────────────── + # Install dependencies (macOS) deps-macos: brew install openssl@3 spdlog cmake @@ -69,6 +154,8 @@ deps-macos: deps-ubuntu: sudo apt-get update && sudo apt-get install -y libssl-dev libspdlog-dev cmake +# ── Debug / analysis ───────────────────────────────────────────────────────── + # Run memory check with valgrind memcheck: build-debug valgrind --leak-check=full --show-leak-kinds=all {{build_dir}}/privacy_pass_tests diff --git a/src/core/types.cpp b/src/core/types.cpp index 00fc5db..4c8363a 100644 --- a/src/core/types.cpp +++ b/src/core/types.cpp @@ -3,11 +3,14 @@ #include #include -#include -#include -#include #include +// Backend-specific init/shutdown are defined in src/crypto/{openssl,boringssl}/init.cpp +namespace privacy_pass::crypto::detail { +void backend_init(); +void backend_shutdown(); +} // namespace privacy_pass::crypto::detail + namespace privacy_pass { namespace { @@ -19,8 +22,7 @@ Result initialize() { return {}; } - // Initialize OpenSSL - OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, nullptr); + crypto::detail::backend_init(); g_initialized = true; spdlog::debug("Privacy Pass library initialized"); @@ -33,9 +35,7 @@ void shutdown() { return; } - // Clean up OpenSSL - EVP_cleanup(); - ERR_free_strings(); + crypto::detail::backend_shutdown(); g_initialized = false; spdlog::debug("Privacy Pass library shut down"); diff --git a/src/crypto/blind_rsa.cpp b/src/crypto/blind_rsa.cpp index 2cefc27..4217742 100644 --- a/src/crypto/blind_rsa.cpp +++ b/src/crypto/blind_rsa.cpp @@ -4,153 +4,35 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include +#include "compat.hpp" #include namespace privacy_pass::crypto { +using namespace detail; +using namespace compat; + namespace { -// RSASSA-PSS parameters for Privacy Pass constexpr int RSA_BITS = 2048; -constexpr int SALT_LENGTH = 48; // SHA-384 output size - -// Maximum input size for OpenSSL APIs (prevent integer truncation) +constexpr int SALT_LENGTH = 48; constexpr size_t MAX_INPUT_SIZE = static_cast(INT_MAX); -// Get OpenSSL error string (sanitized for external exposure) -std::string get_openssl_error() { - char buf[256]; - ERR_error_string_n(ERR_get_error(), buf, sizeof(buf)); - return buf; -} - -// Get sanitized error string for external callers -std::string get_sanitized_error() { - // Log the detailed error internally - spdlog::debug("OpenSSL error: {}", get_openssl_error()); - // Return generic message to callers - return "Cryptographic operation failed"; -} - -bool is_rsa_pss_key(const EVP_PKEY* key) { - return EVP_PKEY_base_id(key) == EVP_PKEY_RSA_PSS; -} - -bool digest_name_is_sha384(std::string_view name) { - return name == "SHA384" || name == "SHA-384" || name == "SHA2-384"; -} - -Result validate_rsa_pss_params(const EVP_PKEY* key) { - if (!is_rsa_pss_key(key)) { - return std::unexpected(Error{ErrorCode::INVALID_KEY, "Not an RSASSA-PSS key"}); - } - - BIGNUM* n = nullptr; - BIGNUM* e = nullptr; - if (EVP_PKEY_get_bn_param(key, OSSL_PKEY_PARAM_RSA_N, &n) != 1 || - EVP_PKEY_get_bn_param(key, OSSL_PKEY_PARAM_RSA_E, &e) != 1) { - BN_free(n); - BN_free(e); - return std::unexpected(Error{ErrorCode::INVALID_KEY, "Missing RSA key parameters"}); - } - - const bool key_params_ok = BN_num_bits(n) == RSA_BITS && BN_is_word(e, RSA_PUBLIC_EXPONENT) == 1; - BN_free(n); - BN_free(e); - if (!key_params_ok) { - return std::unexpected(Error{ErrorCode::INVALID_KEY, - "RSASSA-PSS key must use RSA-2048 and exponent 65537"}); - } - - int salt_len = 0; - if (EVP_PKEY_get_int_param(key, OSSL_PKEY_PARAM_RSA_PSS_SALTLEN, &salt_len) != 1 || - salt_len != SALT_LENGTH) { - return std::unexpected(Error{ErrorCode::INVALID_KEY, - "RSASSA-PSS key must use 48-byte salt"}); - } - - char digest[80]{}; - char mgf1_digest[80]{}; - size_t digest_len = 0; - size_t mgf1_digest_len = 0; - if (EVP_PKEY_get_utf8_string_param( - key, OSSL_PKEY_PARAM_RSA_DIGEST, digest, sizeof(digest), &digest_len) != 1 || - EVP_PKEY_get_utf8_string_param( - key, OSSL_PKEY_PARAM_RSA_MGF1_DIGEST, mgf1_digest, sizeof(mgf1_digest), - &mgf1_digest_len) != 1 || - !digest_name_is_sha384(std::string_view(digest, digest_len)) || - !digest_name_is_sha384(std::string_view(mgf1_digest, mgf1_digest_len))) { - return std::unexpected(Error{ErrorCode::INVALID_KEY, - "RSASSA-PSS key must use SHA-384 and MGF1-SHA-384"}); - } - - return {}; -} - -// EMSA-PSS encoding for blind RSA (RFC 9474) -Result emsa_pss_encode(EVP_PKEY* pkey, ByteView msg) { +Result do_emsa_pss_encode(EVP_PKEY* pkey, ByteView msg) { auto mHash = sha384(msg); - if (!mHash) { - return std::unexpected(mHash.error()); - } - -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable: 4996) -#elif defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -#endif - RSA* rsa = EVP_PKEY_get1_RSA(pkey); - if (!rsa) { - return std::unexpected(Error{ErrorCode::INVALID_KEY, "Failed to get RSA key"}); - } - - Bytes encoded(static_cast(RSA_size(rsa))); - const int ok = RSA_padding_add_PKCS1_PSS_mgf1( - rsa, - encoded.data(), - mHash->data(), - EVP_sha384(), - EVP_sha384(), - SALT_LENGTH); - RSA_free(rsa); -#if defined(_MSC_VER) -#pragma warning(pop) -#elif defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic pop -#endif - - if (ok != 1) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, get_sanitized_error()}); - } - - return encoded; + if (!mHash) return std::unexpected(mHash.error()); + return emsa_pss_encode(pkey, ByteView(mHash->data(), mHash->size()), SALT_LENGTH); } } // namespace // BlindRsaPublicKey implementation struct BlindRsaPublicKey::Impl { - EVP_PKEY* pkey = nullptr; + UniqueEVP_PKEY pkey; Bytes original_spki; TokenKeyId cached_key_id{}; bool key_id_computed = false; - - ~Impl() { - if (pkey) { - EVP_PKEY_free(pkey); - } - } }; BlindRsaPublicKey::BlindRsaPublicKey() : impl_(std::make_unique()) {} @@ -159,109 +41,43 @@ BlindRsaPublicKey::BlindRsaPublicKey(BlindRsaPublicKey&&) noexcept = default; BlindRsaPublicKey& BlindRsaPublicKey::operator=(BlindRsaPublicKey&&) noexcept = default; Result BlindRsaPublicKey::from_spki(ByteView spki) { - // Validate input size to prevent integer truncation if (spki.size() > MAX_INPUT_SIZE) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "SPKI data too large"}); } BlindRsaPublicKey key; - - const uint8_t* p = spki.data(); - key.impl_->pkey = d2i_PUBKEY(nullptr, &p, static_cast(spki.size())); - + key.impl_->pkey = parse_public_key_spki(spki); if (!key.impl_->pkey) { return std::unexpected(Error{ErrorCode::INVALID_KEY, get_sanitized_error()}); } - auto params = validate_rsa_pss_params(key.impl_->pkey); - if (!params) { - return std::unexpected(params.error()); - } + auto params = validate_rsa_params(key.impl_->pkey.get(), RSA_BITS, RSA_PUBLIC_EXPONENT); + if (!params) return std::unexpected(params.error()); key.impl_->original_spki.assign(spki.begin(), spki.end()); - return key; } Result BlindRsaPublicKey::from_components(ByteView modulus, ByteView exponent) { - // Validate input sizes to prevent integer truncation if (modulus.size() > MAX_INPUT_SIZE || exponent.size() > MAX_INPUT_SIZE) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Key component too large"}); } - BlindRsaPublicKey key; - - BIGNUM* n = BN_bin2bn(modulus.data(), static_cast(modulus.size()), nullptr); - BIGNUM* e = BN_bin2bn(exponent.data(), static_cast(exponent.size()), nullptr); - + auto n = bin2bn(modulus.data(), static_cast(modulus.size())); + auto e = bin2bn(exponent.data(), static_cast(exponent.size())); if (!n || !e) { - BN_free(n); - BN_free(e); return std::unexpected(Error{ErrorCode::INVALID_KEY, "Failed to create bignums"}); } - OSSL_PARAM_BLD* bld = OSSL_PARAM_BLD_new(); - if (!bld) { - BN_free(n); - BN_free(e); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create parameter builder"}); - } - - const bool params_pushed = - OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_N, n) == 1 && - OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_E, e) == 1 && - OSSL_PARAM_BLD_push_utf8_string(bld, OSSL_PKEY_PARAM_RSA_DIGEST, - const_cast("SHA384"), 0) == 1 && - OSSL_PARAM_BLD_push_utf8_string(bld, OSSL_PKEY_PARAM_RSA_MASKGENFUNC, - const_cast("MGF1"), 0) == 1 && - OSSL_PARAM_BLD_push_utf8_string(bld, OSSL_PKEY_PARAM_RSA_MGF1_DIGEST, - const_cast("SHA384"), 0) == 1 && - OSSL_PARAM_BLD_push_int(bld, OSSL_PKEY_PARAM_RSA_PSS_SALTLEN, SALT_LENGTH) == 1; - if (!params_pushed) { - OSSL_PARAM_BLD_free(bld); - BN_free(n); - BN_free(e); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to build RSA parameters"}); - } - - OSSL_PARAM* params = OSSL_PARAM_BLD_to_param(bld); - if (!params) { - OSSL_PARAM_BLD_free(bld); - BN_free(n); - BN_free(e); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create RSA parameters"}); - } - - EVP_PKEY* pkey = nullptr; - EVP_PKEY_CTX* from_data_ctx = EVP_PKEY_CTX_new_from_name(nullptr, "RSA-PSS", nullptr); - if (!from_data_ctx) { - OSSL_PARAM_free(params); - OSSL_PARAM_BLD_free(bld); - BN_free(n); - BN_free(e); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create RSA-PSS context"}); - } - - bool success = EVP_PKEY_fromdata_init(from_data_ctx) == 1 && - EVP_PKEY_fromdata(from_data_ctx, &pkey, EVP_PKEY_PUBLIC_KEY, params) == 1; - - OSSL_PARAM_free(params); - OSSL_PARAM_BLD_free(bld); - EVP_PKEY_CTX_free(from_data_ctx); - BN_free(n); - BN_free(e); - - if (!success || !pkey) { - return std::unexpected(Error{ErrorCode::INVALID_KEY, "Failed to create RSASSA-PSS key"}); + BlindRsaPublicKey key; + key.impl_->pkey = rsa_public_key_from_components(n.get(), e.get()); + if (!key.impl_->pkey) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create RSA key"}); } - auto validated = validate_rsa_pss_params(pkey); - if (!validated) { - EVP_PKEY_free(pkey); - return std::unexpected(validated.error()); - } + auto validated = validate_rsa_params(key.impl_->pkey.get(), RSA_BITS, RSA_PUBLIC_EXPONENT); + if (!validated) return std::unexpected(validated.error()); - key.impl_->pkey = pkey; return key; } @@ -269,41 +85,23 @@ Result BlindRsaPublicKey::to_spki() const { if (!impl_->pkey) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Key not initialized"}); } - - int len = i2d_PUBKEY(impl_->pkey, nullptr); - if (len <= 0) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute SPKI size"}); - } - - Bytes result(static_cast(len)); - uint8_t* p = result.data(); - if (i2d_PUBKEY(impl_->pkey, &p) != len) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to encode SPKI"}); - } - - return result; + return marshal_public_key(impl_->pkey.get()); } Result BlindRsaPublicKey::key_id() const { - if (impl_->key_id_computed) { - return impl_->cached_key_id; - } + if (impl_->key_id_computed) return impl_->cached_key_id; Bytes spki; if (!impl_->original_spki.empty()) { spki = impl_->original_spki; } else { auto encoded = to_spki(); - if (!encoded) { - return std::unexpected(encoded.error()); - } + if (!encoded) return std::unexpected(encoded.error()); spki = std::move(*encoded); } auto hash = sha256(ByteView(spki.data(), spki.size())); - if (!hash) { - return std::unexpected(hash.error()); - } + if (!hash) return std::unexpected(hash.error()); impl_->cached_key_id = *hash; impl_->key_id_computed = true; @@ -315,294 +113,109 @@ Result BlindRsaPublicKey::blind(ByteView msg) const { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Key not initialized"}); } - // Get modulus size - BIGNUM* n_bn = nullptr; - EVP_PKEY_get_bn_param(impl_->pkey, OSSL_PKEY_PARAM_RSA_N, &n_bn); - if (!n_bn) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get modulus"}); + UniqueBIGNUM n_bn, e_bn; + if (!rsa_get_bn_param(impl_->pkey.get(), PARAM_RSA_N, n_bn) || + !rsa_get_bn_param(impl_->pkey.get(), PARAM_RSA_E, e_bn)) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get key params"}); } - int mod_size = BN_num_bytes(n_bn); + int mod_size = BN_num_bytes(n_bn.get()); - // EMSA-PSS encode the message - auto encoded = emsa_pss_encode(impl_->pkey, msg); - if (!encoded) { - BN_free(n_bn); - return std::unexpected(encoded.error()); - } + auto encoded = do_emsa_pss_encode(impl_->pkey.get(), msg); + if (!encoded) return std::unexpected(encoded.error()); - // Convert encoded message to BIGNUM - BIGNUM* m = BN_bin2bn(encoded->data(), static_cast(encoded->size()), nullptr); + auto m = bin2bn(encoded->data(), static_cast(encoded->size())); if (!m) { - BN_free(n_bn); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to convert message"}); } - // Get public exponent - BIGNUM* e_bn = nullptr; - EVP_PKEY_get_bn_param(impl_->pkey, OSSL_PKEY_PARAM_RSA_E, &e_bn); - if (!e_bn) { - BN_free(n_bn); - BN_free(m); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get exponent"}); - } - - BN_CTX* bn_ctx = BN_CTX_new(); - if (!bn_ctx) { - BN_free(n_bn); - BN_free(m); - BN_free(e_bn); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create BN context"}); - } - - // Generate random blinding factor r - BIGNUM* r = BN_new(); - BIGNUM* r_inv = BN_new(); - BIGNUM* x = BN_new(); - BIGNUM* x_mont = BN_new(); - BIGNUM* blinded = BN_new(); - BN_MONT_CTX* mont = BN_MONT_CTX_new(); - - if (!r || !r_inv || !x || !x_mont || !blinded || !mont) { - BN_free(n_bn); - BN_free(m); - BN_free(e_bn); - BN_clear_free(r); - BN_clear_free(r_inv); - BN_free(x); - BN_free(x_mont); - BN_free(blinded); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); + auto bn_ctx = make_bn_ctx(); + auto r = make_secure_bignum(); + auto r_inv = make_secure_bignum(); + auto x = make_bignum(); + auto x_mont = make_bignum(); + auto blinded = make_bignum(); + auto mont = make_bn_mont_ctx(); + + if (!bn_ctx || !r || !r_inv || !x || !x_mont || !blinded || !mont) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to allocate bignums"}); } - if (BN_MONT_CTX_set(mont, n_bn, bn_ctx) != 1) { - BN_free(n_bn); - BN_free(m); - BN_free(e_bn); - BN_clear_free(r); - BN_clear_free(r_inv); - BN_free(x); - BN_free(x_mont); - BN_free(blinded); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); + if (BN_MONT_CTX_set(mont.get(), n_bn.get(), bn_ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create Montgomery context"}); } - // Generate r coprime to n do { - if (!BN_rand_range(r, n_bn)) { - BN_free(n_bn); - BN_free(m); - BN_free(e_bn); - BN_clear_free(r); - BN_clear_free(r_inv); - BN_free(x); - BN_free(x_mont); - BN_free(blinded); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); + if (!BN_rand_range(r.get(), n_bn.get())) { return std::unexpected(Error{ErrorCode::BLINDING_FAILED, "Failed to generate r"}); } - } while (BN_is_zero(r) || !BN_mod_inverse(r_inv, r, n_bn, bn_ctx)); - - // x = r^e mod n - if (BN_mod_exp_mont(x, r, e_bn, n_bn, bn_ctx, mont) != 1 || - BN_to_montgomery(x_mont, x, mont, bn_ctx) != 1) { - BN_free(n_bn); - BN_free(m); - BN_free(e_bn); - BN_clear_free(r); - BN_clear_free(r_inv); - BN_free(x); - BN_free(x_mont); - BN_free(blinded); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); - return std::unexpected(Error{ErrorCode::BLINDING_FAILED, "Failed to compute x"}); - } - - // blinded = m * x mod n - if (BN_mod_mul_montgomery(blinded, m, x_mont, mont, bn_ctx) != 1) { - BN_free(n_bn); - BN_free(m); - BN_free(e_bn); - BN_clear_free(r); - BN_clear_free(r_inv); - BN_free(x); - BN_free(x_mont); - BN_free(blinded); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); + } while (BN_is_zero(r.get()) || !BN_mod_inverse(r_inv.get(), r.get(), n_bn.get(), bn_ctx.get())); + + if (BN_mod_exp_mont(x.get(), r.get(), e_bn.get(), n_bn.get(), bn_ctx.get(), mont.get()) != 1 || + BN_to_montgomery(x_mont.get(), x.get(), mont.get(), bn_ctx.get()) != 1 || + BN_mod_mul_montgomery(blinded.get(), m.get(), x_mont.get(), mont.get(), bn_ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::BLINDING_FAILED, "Failed to blind message"}); } BlindingData result; - - // Store inverse in standard form for portable serialization. result.inverse.resize(static_cast(mod_size)); - if (BN_bn2binpad(r_inv, result.inverse.data(), mod_size) != mod_size) { - BN_free(n_bn); - BN_free(m); - BN_free(e_bn); - BN_clear_free(r); - BN_clear_free(r_inv); - BN_free(x); - BN_free(x_mont); - BN_free(blinded); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); - return std::unexpected(Error{ErrorCode::BLINDING_FAILED, "Failed to serialize inverse"}); - } - - // Store blinded message result.blinded_msg.resize(static_cast(mod_size)); - if (BN_bn2binpad(blinded, result.blinded_msg.data(), mod_size) != mod_size) { - BN_free(n_bn); - BN_free(m); - BN_free(e_bn); - BN_clear_free(r); - BN_clear_free(r_inv); - BN_free(x); - BN_free(x_mont); - BN_free(blinded); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); - return std::unexpected(Error{ErrorCode::BLINDING_FAILED, "Failed to serialize blinded message"}); - } - - BN_free(n_bn); - BN_free(m); - BN_free(e_bn); - BN_clear_free(r); - BN_clear_free(r_inv); - BN_free(x); - BN_free(x_mont); - BN_free(blinded); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); + if (BN_bn2binpad(r_inv.get(), result.inverse.data(), mod_size) != mod_size || + BN_bn2binpad(blinded.get(), result.blinded_msg.data(), mod_size) != mod_size) { + return std::unexpected(Error{ErrorCode::BLINDING_FAILED, "Failed to serialize"}); + } return result; } Result BlindRsaPublicKey::finalize( - ByteView blind_sig, - BlindingData& blinding_data, - ByteView msg) const { + ByteView blind_sig, BlindingData& blinding_data, ByteView msg) const { if (!impl_->pkey) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Key not initialized"}); } - - // Validate input sizes if (blind_sig.size() > MAX_INPUT_SIZE || blinding_data.inverse.size() > MAX_INPUT_SIZE) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Input too large"}); } - // Get modulus - BIGNUM* n_bn = nullptr; - EVP_PKEY_get_bn_param(impl_->pkey, OSSL_PKEY_PARAM_RSA_N, &n_bn); - if (!n_bn) { + UniqueBIGNUM n_bn; + if (!rsa_get_bn_param(impl_->pkey.get(), PARAM_RSA_N, n_bn)) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get modulus"}); } - int mod_size = BN_num_bytes(n_bn); - const auto expected_size = static_cast(mod_size); - + int mod_size = BN_num_bytes(n_bn.get()); + auto expected_size = static_cast(mod_size); if (blind_sig.size() != expected_size || blinding_data.inverse.size() != expected_size) { - BN_free(n_bn); return std::unexpected(Error{ErrorCode::INVALID_LENGTH, "Blind signature and inverse must match modulus length"}); } - BN_CTX* bn_ctx = BN_CTX_new(); - if (!bn_ctx) { - BN_free(n_bn); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create BN context"}); - } - - BIGNUM* z = BN_bin2bn(blind_sig.data(), static_cast(blind_sig.size()), nullptr); - BIGNUM* r_inv = BN_bin2bn(blinding_data.inverse.data(), - static_cast(blinding_data.inverse.size()), nullptr); - BIGNUM* r_inv_mont = BN_new(); - BIGNUM* sig = BN_new(); - BN_MONT_CTX* mont = BN_MONT_CTX_new(); + auto bn_ctx = make_bn_ctx(); + auto z = bin2bn(blind_sig.data(), static_cast(blind_sig.size())); + auto r_inv = bin2bn_secure(blinding_data.inverse.data(), + static_cast(blinding_data.inverse.size())); + auto r_inv_mont = make_secure_bignum(); + auto sig = make_bignum(); + auto mont = make_bn_mont_ctx(); - if (!z || !r_inv || !r_inv_mont || !sig || !mont) { - BN_free(n_bn); - BN_free(z); - BN_clear_free(r_inv); - BN_clear_free(r_inv_mont); - BN_free(sig); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); + if (!bn_ctx || !z || !r_inv || !r_inv_mont || !sig || !mont) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to allocate bignums"}); } - // Reject blind signatures outside [0, n) - if (BN_cmp(z, n_bn) >= 0) { - BN_free(n_bn); - BN_free(z); - BN_clear_free(r_inv); - BN_clear_free(r_inv_mont); - BN_free(sig); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); + if (BN_cmp(z.get(), n_bn.get()) >= 0) { return std::unexpected(Error{ErrorCode::UNBLINDING_FAILED, "Blind signature out of range"}); } - if (BN_MONT_CTX_set(mont, n_bn, bn_ctx) != 1) { - BN_free(n_bn); - BN_free(z); - BN_clear_free(r_inv); - BN_clear_free(r_inv_mont); - BN_free(sig); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create Montgomery context"}); - } - - // Convert standard-form inverse to Montgomery form for constant-shape multiply - if (BN_to_montgomery(r_inv_mont, r_inv, mont, bn_ctx) != 1) { - BN_free(n_bn); - BN_free(z); - BN_clear_free(r_inv); - BN_clear_free(r_inv_mont); - BN_free(sig); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); - return std::unexpected(Error{ErrorCode::UNBLINDING_FAILED, "Failed to convert inverse to Montgomery form"}); - } - - // sig = z * r_inv mod n - if (BN_mod_mul_montgomery(sig, z, r_inv_mont, mont, bn_ctx) != 1) { - BN_free(n_bn); - BN_free(z); - BN_clear_free(r_inv); - BN_clear_free(r_inv_mont); - BN_free(sig); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); + if (BN_MONT_CTX_set(mont.get(), n_bn.get(), bn_ctx.get()) != 1 || + BN_to_montgomery(r_inv_mont.get(), r_inv.get(), mont.get(), bn_ctx.get()) != 1 || + BN_mod_mul_montgomery(sig.get(), z.get(), r_inv_mont.get(), mont.get(), bn_ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::UNBLINDING_FAILED, "Failed to unblind"}); } Bytes result(static_cast(mod_size)); - BN_bn2binpad(sig, result.data(), mod_size); - - BN_free(n_bn); - BN_free(z); - BN_clear_free(r_inv); - BN_clear_free(r_inv_mont); - BN_free(sig); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); - - // Clear the blinding data after successful use to prevent reuse + BN_bn2binpad(sig.get(), result.data(), mod_size); blinding_data.inverse.clear(); - // Verify the unblinded signature if message was provided if (!msg.empty()) { auto verify_result = verify(msg, ByteView(result.data(), result.size())); if (!verify_result || !*verify_result) { @@ -619,15 +232,13 @@ Result BlindRsaPublicKey::verify(ByteView msg, ByteView signature) const { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Key not initialized"}); } - EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); + auto md_ctx = make_evp_md_ctx(); if (!md_ctx) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create context"}); } EVP_PKEY_CTX* pkey_ctx = nullptr; - - bool success = EVP_DigestVerifyInit(md_ctx, &pkey_ctx, EVP_sha384(), nullptr, impl_->pkey) == 1; - + bool success = EVP_DigestVerifyInit(md_ctx.get(), &pkey_ctx, EVP_sha384(), nullptr, impl_->pkey.get()) == 1; if (success) { success = EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, RSA_PKCS1_PSS_PADDING) == 1 && EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, SALT_LENGTH) == 1 && @@ -636,17 +247,13 @@ Result BlindRsaPublicKey::verify(ByteView msg, ByteView signature) const { int result = 0; if (success) { - result = EVP_DigestVerify(md_ctx, - signature.data(), signature.size(), - msg.data(), msg.size()); + result = EVP_DigestVerify(md_ctx.get(), + signature.data(), signature.size(), msg.data(), msg.size()); } - EVP_MD_CTX_free(md_ctx); - if (!success) { return std::unexpected(Error{ErrorCode::VERIFICATION_FAILED, "Verification setup failed"}); } - return result == 1; } @@ -656,13 +263,7 @@ bool BlindRsaPublicKey::is_valid() const noexcept { // BlindRsaPrivateKey implementation struct BlindRsaPrivateKey::Impl { - EVP_PKEY* pkey = nullptr; - - ~Impl() { - if (pkey) { - EVP_PKEY_free(pkey); - } - } + UniqueEVP_PKEY pkey; }; BlindRsaPrivateKey::BlindRsaPrivateKey() : impl_(std::make_unique()) {} @@ -671,59 +272,33 @@ BlindRsaPrivateKey::BlindRsaPrivateKey(BlindRsaPrivateKey&&) noexcept = default; BlindRsaPrivateKey& BlindRsaPrivateKey::operator=(BlindRsaPrivateKey&&) noexcept = default; Result> BlindRsaPrivateKey::generate() { - EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA_PSS, nullptr); - if (!ctx) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create context"}); - } - - bool success = EVP_PKEY_keygen_init(ctx) == 1 && - EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, RSA_BITS) == 1 && - EVP_PKEY_CTX_set_rsa_pss_keygen_md(ctx, EVP_sha384()) == 1 && - EVP_PKEY_CTX_set_rsa_pss_keygen_mgf1_md(ctx, EVP_sha384()) == 1 && - EVP_PKEY_CTX_set_rsa_pss_keygen_saltlen(ctx, SALT_LENGTH) == 1; - - EVP_PKEY* pkey = nullptr; - success = success && EVP_PKEY_keygen(ctx, &pkey) == 1; - - EVP_PKEY_CTX_free(ctx); - - if (!success || !pkey) { + auto pkey = generate_rsa_pss_keypair(RSA_BITS, SALT_LENGTH); + if (!pkey) { spdlog::debug("Key generation failed: {}", get_openssl_error()); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, - "Key generation failed"}); + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Key generation failed"}); } BlindRsaPrivateKey private_key; - private_key.impl_->pkey = pkey; + private_key.impl_->pkey = std::move(pkey); auto public_key_result = private_key.public_key(); - if (!public_key_result) { - return std::unexpected(public_key_result.error()); - } - + if (!public_key_result) return std::unexpected(public_key_result.error()); return std::make_pair(std::move(private_key), std::move(*public_key_result)); } Result BlindRsaPrivateKey::from_pkcs8(ByteView pkcs8) { - // Validate input size to prevent integer truncation if (pkcs8.size() > MAX_INPUT_SIZE) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "PKCS#8 data too large"}); } BlindRsaPrivateKey key; - - const uint8_t* p = pkcs8.data(); - key.impl_->pkey = d2i_AutoPrivateKey(nullptr, &p, static_cast(pkcs8.size())); - + key.impl_->pkey = parse_private_key_der(pkcs8); if (!key.impl_->pkey) { return std::unexpected(Error{ErrorCode::INVALID_KEY, get_sanitized_error()}); } - auto params = validate_rsa_pss_params(key.impl_->pkey); - if (!params) { - return std::unexpected(params.error()); - } - + auto params = validate_rsa_params(key.impl_->pkey.get(), RSA_BITS, RSA_PUBLIC_EXPONENT); + if (!params) return std::unexpected(params.error()); return key; } @@ -731,127 +306,64 @@ Result BlindRsaPrivateKey::to_pkcs8() const { if (!impl_->pkey) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Key not initialized"}); } - - int len = i2d_PrivateKey(impl_->pkey, nullptr); - if (len <= 0) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute PKCS#8 size"}); - } - - SecureBytes result(static_cast(len)); - uint8_t* p = result.data(); - if (i2d_PrivateKey(impl_->pkey, &p) != len) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to encode PKCS#8"}); - } - - return result; + return marshal_private_key(impl_->pkey.get()); } Result BlindRsaPrivateKey::public_key() const { if (!impl_->pkey) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Key not initialized"}); } - - // Export SPKI and reimport as public key - int len = i2d_PUBKEY(impl_->pkey, nullptr); - if (len <= 0) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to extract public key"}); - } - - Bytes spki(static_cast(len)); - uint8_t* p = spki.data(); - i2d_PUBKEY(impl_->pkey, &p); - - return BlindRsaPublicKey::from_spki(ByteView(spki.data(), spki.size())); + auto spki = marshal_public_key(impl_->pkey.get()); + if (!spki) return std::unexpected(spki.error()); + return BlindRsaPublicKey::from_spki(ByteView(spki->data(), spki->size())); } Result BlindRsaPrivateKey::blind_sign(ByteView blinded_msg) const { if (!impl_->pkey) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Key not initialized"}); } - - // Validate input size to prevent integer truncation if (blinded_msg.size() > MAX_INPUT_SIZE) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Blinded message too large"}); } - // Get modulus - BIGNUM* n_bn = nullptr; - BIGNUM* d_bn = nullptr; - EVP_PKEY_get_bn_param(impl_->pkey, OSSL_PKEY_PARAM_RSA_N, &n_bn); - EVP_PKEY_get_bn_param(impl_->pkey, OSSL_PKEY_PARAM_RSA_D, &d_bn); - - if (!n_bn || !d_bn) { - BN_free(n_bn); - BN_clear_free(d_bn); + UniqueBIGNUM n_bn; + UniqueSecureBIGNUM d_bn; + if (!rsa_get_bn_param(impl_->pkey.get(), PARAM_RSA_N, n_bn) || + !rsa_get_secure_bn_param(impl_->pkey.get(), PARAM_RSA_D, d_bn)) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get key parameters"}); } - int mod_size = BN_num_bytes(n_bn); + int mod_size = BN_num_bytes(n_bn.get()); if (blinded_msg.size() != static_cast(mod_size)) { - BN_free(n_bn); - BN_clear_free(d_bn); return std::unexpected(Error{ErrorCode::INVALID_LENGTH, "Blinded message must match RSA modulus size"}); } - BN_CTX* bn_ctx = BN_CTX_new(); - BN_MONT_CTX* mont = BN_MONT_CTX_new(); - BIGNUM* m = BN_bin2bn(blinded_msg.data(), static_cast(blinded_msg.size()), nullptr); - BIGNUM* sig = BN_new(); + auto bn_ctx = make_bn_ctx(); + auto mont = make_bn_mont_ctx(); + auto m = bin2bn(blinded_msg.data(), static_cast(blinded_msg.size())); + auto sig = make_bignum(); if (!bn_ctx || !mont || !m || !sig) { - BN_free(n_bn); - BN_clear_free(d_bn); - BN_free(m); - BN_free(sig); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to allocate bignums"}); } - if (BN_is_zero(m) || BN_cmp(m, n_bn) >= 0) { - BN_free(n_bn); - BN_clear_free(d_bn); - BN_free(m); - BN_free(sig); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); + if (BN_is_zero(m.get()) || BN_cmp(m.get(), n_bn.get()) >= 0) { return std::unexpected(Error{ErrorCode::INVALID_LENGTH, "Blinded message representative out of range"}); } - if (BN_MONT_CTX_set(mont, n_bn, bn_ctx) != 1) { - BN_free(n_bn); - BN_clear_free(d_bn); - BN_free(m); - BN_free(sig); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); + if (BN_MONT_CTX_set(mont.get(), n_bn.get(), bn_ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create Montgomery context"}); } - // sig = m^d mod n - BN_set_flags(d_bn, BN_FLG_CONSTTIME); - if (BN_mod_exp_mont_consttime(sig, m, d_bn, n_bn, bn_ctx, mont) != 1) { - BN_free(n_bn); - BN_clear_free(d_bn); - BN_free(m); - BN_free(sig); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); + bn_set_consttime(d_bn.get()); + if (BN_mod_exp_mont_consttime(sig.get(), m.get(), d_bn.get(), n_bn.get(), bn_ctx.get(), mont.get()) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Blind sign failed"}); } Bytes result(static_cast(mod_size)); - BN_bn2binpad(sig, result.data(), mod_size); - - BN_free(n_bn); - BN_clear_free(d_bn); - BN_free(m); - BN_free(sig); - BN_MONT_CTX_free(mont); - BN_CTX_free(bn_ctx); - + BN_bn2binpad(sig.get(), result.data(), mod_size); return result; } @@ -860,15 +372,13 @@ Result BlindRsaPrivateKey::sign(ByteView msg) const { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Key not initialized"}); } - EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); + auto md_ctx = make_evp_md_ctx(); if (!md_ctx) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create context"}); } EVP_PKEY_CTX* pkey_ctx = nullptr; - - bool success = EVP_DigestSignInit(md_ctx, &pkey_ctx, EVP_sha384(), nullptr, impl_->pkey) == 1; - + bool success = EVP_DigestSignInit(md_ctx.get(), &pkey_ctx, EVP_sha384(), nullptr, impl_->pkey.get()) == 1; if (success) { success = EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, RSA_PKCS1_PSS_PADDING) == 1 && EVP_PKEY_CTX_set_rsa_pss_saltlen(pkey_ctx, SALT_LENGTH) == 1 && @@ -876,23 +386,18 @@ Result BlindRsaPrivateKey::sign(ByteView msg) const { } size_t sig_len = 0; - if (success) { - success = EVP_DigestSign(md_ctx, nullptr, &sig_len, msg.data(), msg.size()) == 1; - } + if (success) success = EVP_DigestSign(md_ctx.get(), nullptr, &sig_len, msg.data(), msg.size()) == 1; Bytes signature; if (success) { signature.resize(sig_len); - success = EVP_DigestSign(md_ctx, signature.data(), &sig_len, msg.data(), msg.size()) == 1; + success = EVP_DigestSign(md_ctx.get(), signature.data(), &sig_len, msg.data(), msg.size()) == 1; signature.resize(sig_len); } - EVP_MD_CTX_free(md_ctx); - if (!success) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Sign failed"}); } - return signature; } diff --git a/src/crypto/common.hpp b/src/crypto/common.hpp new file mode 100644 index 0000000..0af97ec --- /dev/null +++ b/src/crypto/common.hpp @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems +// SPDX-License-Identifier: BSD-2-Clause + +// Shared RAII wrappers for OpenSSL/BoringSSL C types. +// Both libraries use the same C type names, so these wrappers work with either. + +#pragma once + +#include + +// Suppress deprecation warnings for OpenSSL 3.0 low-level APIs (RSA, EC) +// that we must use for Blind RSA arithmetic. These are not deprecated in BoringSSL. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +#include +#include +#include +#include + +namespace privacy_pass::crypto::detail { + +// Generic deleter adapter: wraps a C free function as a std::unique_ptr deleter. +template +struct CDeleter { + void operator()(T* ptr) const noexcept { + if (ptr) Free(ptr); + } +}; + +// Secure-clear deleter for sensitive BIGNUMs +struct BNClearDeleter { + void operator()(BIGNUM* ptr) const noexcept { + if (ptr) BN_clear_free(ptr); + } +}; + +// EVP types +using UniqueEVP_MD_CTX = std::unique_ptr>; +using UniqueEVP_PKEY = std::unique_ptr>; +using UniqueEVP_PKEY_CTX = std::unique_ptr>; + +// BIGNUM types +using UniqueBIGNUM = std::unique_ptr>; +using UniqueSecureBIGNUM = std::unique_ptr; +using UniqueBN_CTX = std::unique_ptr>; +using UniqueBN_MONT_CTX = std::unique_ptr>; + +// EC types +using UniqueEC_POINT = std::unique_ptr>; +using UniqueEC_GROUP = std::unique_ptr>; + +// RSA type +using UniqueRSA = std::unique_ptr>; + +// Convenience factory functions +inline UniqueEVP_MD_CTX make_evp_md_ctx() { + return UniqueEVP_MD_CTX(EVP_MD_CTX_new()); +} + +inline UniqueEVP_PKEY_CTX make_evp_pkey_ctx(int id) { + return UniqueEVP_PKEY_CTX(EVP_PKEY_CTX_new_id(id, nullptr)); +} + +inline UniqueBIGNUM make_bignum() { + return UniqueBIGNUM(BN_new()); +} + +inline UniqueSecureBIGNUM make_secure_bignum() { + return UniqueSecureBIGNUM(BN_new()); +} + +inline UniqueBN_CTX make_bn_ctx() { + return UniqueBN_CTX(BN_CTX_new()); +} + +inline UniqueBN_MONT_CTX make_bn_mont_ctx() { + return UniqueBN_MONT_CTX(BN_MONT_CTX_new()); +} + +inline UniqueEC_POINT make_ec_point(const EC_GROUP* group) { + return UniqueEC_POINT(EC_POINT_new(group)); +} + +inline UniqueBIGNUM bin2bn(const uint8_t* data, int len) { + return UniqueBIGNUM(BN_bin2bn(data, len, nullptr)); +} + +inline UniqueSecureBIGNUM bin2bn_secure(const uint8_t* data, int len) { + return UniqueSecureBIGNUM(BN_bin2bn(data, len, nullptr)); +} + +} // namespace privacy_pass::crypto::detail + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif diff --git a/src/crypto/compat.hpp b/src/crypto/compat.hpp new file mode 100644 index 0000000..21b1f4c --- /dev/null +++ b/src/crypto/compat.hpp @@ -0,0 +1,488 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems +// SPDX-License-Identifier: BSD-2-Clause + +// Thin compatibility layer bridging OpenSSL 3.x and BoringSSL API differences. +// Each function provides a unified interface; the implementation is selected +// at compile time via PRIVACY_PASS_WITH_BORINGSSL / PRIVACY_PASS_WITH_OPENSSL. + +#pragma once + +// Suppress OpenSSL 3.0 deprecation warnings for low-level RSA APIs +// that we must use for Blind RSA arithmetic (not deprecated in BoringSSL). +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +#include "common.hpp" +#include + +#include +#include +#include +#include +#include + +#ifdef PRIVACY_PASS_WITH_BORINGSSL +#include +#include +#else +#include +#include +#include +#endif + +#include + +namespace privacy_pass::crypto::compat { + +using namespace detail; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +inline std::string get_openssl_error() { + char buf[256]; + ERR_error_string_n(ERR_get_error(), buf, sizeof(buf)); + return buf; +} + +inline std::string get_sanitized_error() { + spdlog::debug("Crypto error: {}", get_openssl_error()); + return "Cryptographic operation failed"; +} + +// ── EC curve query ─────────────────────────────────────────────────────────── +// BoringSSL only has EC_GROUP_get_curve_GFp; OpenSSL 3.x has the generic name. + +inline int ec_group_get_curve(const EC_GROUP* group, BIGNUM* p, + BIGNUM* a, BIGNUM* b, BN_CTX* ctx) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + return EC_GROUP_get_curve_GFp(group, p, a, b, ctx); +#else + return EC_GROUP_get_curve(group, p, a, b, ctx); +#endif +} + +// ── HKDF ───────────────────────────────────────────────────────────────────── +// OpenSSL 3.x: EVP_PKEY_CTX + EVP_PKEY_HKDF +// BoringSSL: HKDF_extract / HKDF_expand + +inline Result hkdf_extract(const EVP_MD* md, ByteView salt, ByteView ikm) { + size_t hash_len = static_cast(EVP_MD_size(md)); + Bytes prk(hash_len); + size_t prk_len = prk.size(); + +#ifdef PRIVACY_PASS_WITH_BORINGSSL + if (HKDF_extract(prk.data(), &prk_len, md, + ikm.data(), ikm.size(), + salt.data(), salt.size()) != 1) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "HKDF-Extract failed"}); + } +#else + auto ctx = make_evp_pkey_ctx(EVP_PKEY_HKDF); + if (!ctx) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create HKDF context"}); + } + if (EVP_PKEY_derive_init(ctx.get()) != 1 || + EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) != 1 || + EVP_PKEY_CTX_set1_hkdf_salt(ctx.get(), salt.data(), static_cast(salt.size())) != 1 || + EVP_PKEY_CTX_set1_hkdf_key(ctx.get(), ikm.data(), static_cast(ikm.size())) != 1 || + EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY) != 1 || + EVP_PKEY_derive(ctx.get(), prk.data(), &prk_len) != 1) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "HKDF-Extract failed"}); + } +#endif + prk.resize(prk_len); + return prk; +} + +inline Result hkdf_expand(const EVP_MD* md, ByteView prk, ByteView info, size_t length) { + Bytes okm(length); + +#ifdef PRIVACY_PASS_WITH_BORINGSSL + if (HKDF_expand(okm.data(), length, md, + prk.data(), prk.size(), + info.data(), info.size()) != 1) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "HKDF-Expand failed"}); + } +#else + auto ctx = make_evp_pkey_ctx(EVP_PKEY_HKDF); + if (!ctx) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create HKDF context"}); + } + size_t okm_len = length; + if (EVP_PKEY_derive_init(ctx.get()) != 1 || + EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) != 1 || + EVP_PKEY_CTX_set1_hkdf_key(ctx.get(), prk.data(), static_cast(prk.size())) != 1 || + EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data(), static_cast(info.size())) != 1 || + EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) != 1 || + EVP_PKEY_derive(ctx.get(), okm.data(), &okm_len) != 1) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "HKDF-Expand failed"}); + } +#endif + return okm; +} + +// ── RSA key validation ─────────────────────────────────────────────────────── +// OpenSSL 3.x: queries EVP_PKEY params via OSSL_PKEY_PARAM_* +// BoringSSL: queries RSA struct directly via RSA_get0_key + +inline Result validate_rsa_params(const EVP_PKEY* pkey, + int expected_bits, unsigned long expected_e) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + if (EVP_PKEY_id(pkey) != EVP_PKEY_RSA) { + return std::unexpected(Error{ErrorCode::INVALID_KEY, "Not an RSA key"}); + } + const RSA* rsa = EVP_PKEY_get0_RSA(pkey); + if (!rsa) { + return std::unexpected(Error{ErrorCode::INVALID_KEY, "Failed to get RSA key"}); + } + const BIGNUM* n = nullptr; + const BIGNUM* e = nullptr; + RSA_get0_key(rsa, &n, &e, nullptr); + if (!n || !e || BN_num_bits(n) != static_cast(expected_bits) || + !BN_is_word(e, expected_e)) { + return std::unexpected(Error{ErrorCode::INVALID_KEY, + "RSA key must be RSA-2048 with exponent 65537"}); + } +#else + if (EVP_PKEY_base_id(pkey) != EVP_PKEY_RSA_PSS) { + return std::unexpected(Error{ErrorCode::INVALID_KEY, "Not an RSASSA-PSS key"}); + } + BIGNUM* n_raw = nullptr; + BIGNUM* e_raw = nullptr; + if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_N, &n_raw) != 1 || + EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_E, &e_raw) != 1) { + BN_free(n_raw); + BN_free(e_raw); + return std::unexpected(Error{ErrorCode::INVALID_KEY, "Missing RSA key parameters"}); + } + UniqueBIGNUM n(n_raw); + UniqueBIGNUM e(e_raw); + if (BN_num_bits(n.get()) != expected_bits || !BN_is_word(e.get(), expected_e)) { + return std::unexpected(Error{ErrorCode::INVALID_KEY, + "RSA key must be RSA-2048 with exponent 65537"}); + } + + int salt_len = 0; + if (EVP_PKEY_get_int_param(pkey, OSSL_PKEY_PARAM_RSA_PSS_SALTLEN, &salt_len) != 1 || + salt_len != 48) { + return std::unexpected(Error{ErrorCode::INVALID_KEY, + "RSASSA-PSS key must use 48-byte salt"}); + } + auto digest_ok = [](const char* param, const EVP_PKEY* k) { + char buf[80]{}; + size_t len = 0; + if (EVP_PKEY_get_utf8_string_param(k, param, buf, sizeof(buf), &len) != 1) + return false; + std::string_view sv(buf, len); + return sv == "SHA384" || sv == "SHA-384" || sv == "SHA2-384"; + }; + if (!digest_ok(OSSL_PKEY_PARAM_RSA_DIGEST, pkey) || + !digest_ok(OSSL_PKEY_PARAM_RSA_MGF1_DIGEST, pkey)) { + return std::unexpected(Error{ErrorCode::INVALID_KEY, + "RSASSA-PSS key must use SHA-384 and MGF1-SHA-384"}); + } +#endif + return {}; +} + +// ── RSA EMSA-PSS encode ───────────────────────────────────────────────────── + +inline Result emsa_pss_encode(EVP_PKEY* pkey, ByteView msg_hash, + int salt_len) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + const RSA* rsa = EVP_PKEY_get0_RSA(pkey); +#else + RSA* rsa = EVP_PKEY_get1_RSA(pkey); + UniqueRSA rsa_guard(rsa); +#endif + if (!rsa) { + return std::unexpected(Error{ErrorCode::INVALID_KEY, "Failed to get RSA key"}); + } + + Bytes encoded(static_cast(RSA_size(rsa))); + if (RSA_padding_add_PKCS1_PSS_mgf1(rsa, encoded.data(), msg_hash.data(), + EVP_sha384(), EVP_sha384(), salt_len) != 1) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, get_sanitized_error()}); + } + return encoded; +} + +// ── RSA key parsing ────────────────────────────────────────────────────────── + +inline UniqueEVP_PKEY parse_public_key_spki(ByteView spki) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + // Try BoringSSL parser, fall back to d2i_PUBKEY for RSA-PSS SPKI + CBS cbs; + CBS_init(&cbs, spki.data(), spki.size()); + UniqueEVP_PKEY pkey(EVP_parse_public_key(&cbs)); + if (!pkey || CBS_len(&cbs) != 0) { + const uint8_t* p = spki.data(); + pkey.reset(d2i_PUBKEY(nullptr, &p, static_cast(spki.size()))); + } + return pkey; +#else + const uint8_t* p = spki.data(); + return UniqueEVP_PKEY(d2i_PUBKEY(nullptr, &p, static_cast(spki.size()))); +#endif +} + +inline UniqueEVP_PKEY parse_private_key_der(ByteView der) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + CBS cbs; + CBS_init(&cbs, der.data(), der.size()); + UniqueEVP_PKEY pkey(EVP_parse_private_key(&cbs)); + if (pkey && CBS_len(&cbs) != 0) pkey.reset(); + return pkey; +#else + const uint8_t* p = der.data(); + return UniqueEVP_PKEY(d2i_AutoPrivateKey(nullptr, &p, static_cast(der.size()))); +#endif +} + +// ── RSA key serialization ──────────────────────────────────────────────────── + +inline Result marshal_public_key(const EVP_PKEY* pkey) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + CBB cbb; + if (!CBB_init(&cbb, 0) || !EVP_marshal_public_key(&cbb, pkey)) { + CBB_cleanup(&cbb); + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to encode SPKI"}); + } + uint8_t* data = nullptr; + size_t len = 0; + if (!CBB_finish(&cbb, &data, &len)) { + CBB_cleanup(&cbb); + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to finish SPKI"}); + } + Bytes result(data, data + len); + OPENSSL_free(data); + return result; +#else + int len = i2d_PUBKEY(pkey, nullptr); + if (len <= 0) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute SPKI size"}); + } + Bytes result(static_cast(len)); + uint8_t* p = result.data(); + if (i2d_PUBKEY(pkey, &p) != len) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to encode SPKI"}); + } + return result; +#endif +} + +inline Result marshal_private_key(const EVP_PKEY* pkey) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + CBB cbb; + if (!CBB_init(&cbb, 0) || !EVP_marshal_private_key(&cbb, pkey)) { + CBB_cleanup(&cbb); + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to encode PKCS#8"}); + } + uint8_t* data = nullptr; + size_t len = 0; + if (!CBB_finish(&cbb, &data, &len)) { + CBB_cleanup(&cbb); + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to finish PKCS#8"}); + } + SecureBytes result(ByteView(data, len)); + OPENSSL_free(data); + return result; +#else + int len = i2d_PrivateKey(pkey, nullptr); + if (len <= 0) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute PKCS#8 size"}); + } + SecureBytes result(static_cast(len)); + uint8_t* p = result.data(); + if (i2d_PrivateKey(pkey, &p) != len) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to encode PKCS#8"}); + } + return result; +#endif +} + +// ── RSA key construction from components ───────────────────────────────────── + +inline UniqueEVP_PKEY rsa_public_key_from_components(const BIGNUM* n, const BIGNUM* e) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + RSA* rsa = RSA_new(); + if (!rsa) return nullptr; + BIGNUM* n_dup = BN_dup(n); + BIGNUM* e_dup = BN_dup(e); + if (!n_dup || !e_dup || RSA_set0_key(rsa, n_dup, e_dup, nullptr) != 1) { + if (n_dup) BN_free(n_dup); + if (e_dup) BN_free(e_dup); + RSA_free(rsa); + return nullptr; + } + auto pkey = UniqueEVP_PKEY(EVP_PKEY_new()); + if (!pkey || EVP_PKEY_assign_RSA(pkey.get(), rsa) != 1) { + RSA_free(rsa); + return nullptr; + } + return pkey; // rsa ownership transferred to pkey +#else + OSSL_PARAM_BLD* bld = OSSL_PARAM_BLD_new(); + if (!bld) return nullptr; + + bool ok = + OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_N, n) == 1 && + OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_RSA_E, e) == 1 && + OSSL_PARAM_BLD_push_utf8_string(bld, OSSL_PKEY_PARAM_RSA_DIGEST, + const_cast("SHA384"), 0) == 1 && + OSSL_PARAM_BLD_push_utf8_string(bld, OSSL_PKEY_PARAM_RSA_MASKGENFUNC, + const_cast("MGF1"), 0) == 1 && + OSSL_PARAM_BLD_push_utf8_string(bld, OSSL_PKEY_PARAM_RSA_MGF1_DIGEST, + const_cast("SHA384"), 0) == 1 && + OSSL_PARAM_BLD_push_int(bld, OSSL_PKEY_PARAM_RSA_PSS_SALTLEN, 48) == 1; + if (!ok) { + OSSL_PARAM_BLD_free(bld); + return nullptr; + } + + OSSL_PARAM* params = OSSL_PARAM_BLD_to_param(bld); + OSSL_PARAM_BLD_free(bld); + if (!params) return nullptr; + + auto ctx = UniqueEVP_PKEY_CTX(EVP_PKEY_CTX_new_from_name(nullptr, "RSA-PSS", nullptr)); + if (!ctx) { OSSL_PARAM_free(params); return nullptr; } + + EVP_PKEY* pkey = nullptr; + ok = EVP_PKEY_fromdata_init(ctx.get()) == 1 && + EVP_PKEY_fromdata(ctx.get(), &pkey, EVP_PKEY_PUBLIC_KEY, params) == 1; + OSSL_PARAM_free(params); + + if (!ok || !pkey) return nullptr; + return UniqueEVP_PKEY(pkey); +#endif +} + +// ── RSA key generation ─────────────────────────────────────────────────────── + +inline UniqueEVP_PKEY generate_rsa_pss_keypair(int bits, [[maybe_unused]] int salt_len) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + auto bn_e = make_bignum(); + if (!bn_e || !BN_set_word(bn_e.get(), RSA_F4)) return nullptr; + + RSA* rsa = RSA_new(); + if (!rsa) return nullptr; + if (RSA_generate_key_ex(rsa, bits, bn_e.get(), nullptr) != 1) { + RSA_free(rsa); + return nullptr; + } + auto pkey = UniqueEVP_PKEY(EVP_PKEY_new()); + if (!pkey || EVP_PKEY_assign_RSA(pkey.get(), rsa) != 1) { + RSA_free(rsa); + return nullptr; + } + return pkey; +#else + auto ctx = UniqueEVP_PKEY_CTX(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA_PSS, nullptr)); + if (!ctx) return nullptr; + if (EVP_PKEY_keygen_init(ctx.get()) != 1 || + EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), bits) != 1 || + EVP_PKEY_CTX_set_rsa_pss_keygen_md(ctx.get(), EVP_sha384()) != 1 || + EVP_PKEY_CTX_set_rsa_pss_keygen_mgf1_md(ctx.get(), EVP_sha384()) != 1 || + EVP_PKEY_CTX_set_rsa_pss_keygen_saltlen(ctx.get(), salt_len) != 1) { + return nullptr; + } + EVP_PKEY* pkey = nullptr; + if (EVP_PKEY_keygen(ctx.get(), &pkey) != 1) return nullptr; + return UniqueEVP_PKEY(pkey); +#endif +} + +// ── RSA get key parameters ─────────────────────────────────────────────────── + +inline bool rsa_get_bn_param(const EVP_PKEY* pkey, const char* name, UniqueBIGNUM& out) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + const RSA* rsa = EVP_PKEY_get0_RSA(pkey); + if (!rsa) return false; + const BIGNUM* n = nullptr; + const BIGNUM* e = nullptr; + const BIGNUM* d = nullptr; + RSA_get0_key(rsa, &n, &e, &d); + const BIGNUM* src = nullptr; + if (std::string_view(name) == "n") src = n; + else if (std::string_view(name) == "e") src = e; + if (!src) return false; + out.reset(BN_dup(src)); + return out != nullptr; +#else + BIGNUM* raw = nullptr; + if (EVP_PKEY_get_bn_param(pkey, name, &raw) != 1) return false; + out.reset(raw); + return true; +#endif +} + +inline bool rsa_get_secure_bn_param(const EVP_PKEY* pkey, const char* name, + UniqueSecureBIGNUM& out) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + const RSA* rsa = EVP_PKEY_get0_RSA(pkey); + if (!rsa) return false; + const BIGNUM* n = nullptr; + const BIGNUM* e = nullptr; + const BIGNUM* d = nullptr; + RSA_get0_key(rsa, &n, &e, &d); + const BIGNUM* src = nullptr; + if (std::string_view(name) == "d") src = d; + if (!src) return false; + out.reset(BN_dup(src)); + return out != nullptr; +#else + BIGNUM* raw = nullptr; + if (EVP_PKEY_get_bn_param(pkey, name, &raw) != 1) return false; + out.reset(raw); + return true; +#endif +} + +// Portable param name constants +#ifdef PRIVACY_PASS_WITH_BORINGSSL +constexpr const char* PARAM_RSA_N = "n"; +constexpr const char* PARAM_RSA_E = "e"; +constexpr const char* PARAM_RSA_D = "d"; +#else +constexpr const char* PARAM_RSA_N = OSSL_PKEY_PARAM_RSA_N; +constexpr const char* PARAM_RSA_E = OSSL_PKEY_PARAM_RSA_E; +constexpr const char* PARAM_RSA_D = OSSL_PKEY_PARAM_RSA_D; +#endif + +// ── BN consttime flag ──────────────────────────────────────────────────────── +// BoringSSL: always constant-time, no flag needed + +inline void bn_set_consttime(BIGNUM* bn) { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + (void)bn; // BoringSSL is always constant-time +#else + BN_set_flags(bn, BN_FLG_CONSTTIME); +#endif +} + +// ── Init / shutdown ────────────────────────────────────────────────────────── + +inline void backend_init() { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + // BoringSSL auto-initializes +#else + OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, nullptr); +#endif +} + +inline void backend_shutdown() { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + // nothing to do +#else + EVP_cleanup(); + ERR_free_strings(); +#endif +} + +} // namespace privacy_pass::crypto::compat + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif diff --git a/src/crypto/hash.cpp b/src/crypto/hash.cpp index 3003bc4..c53fa5a 100644 --- a/src/crypto/hash.cpp +++ b/src/crypto/hash.cpp @@ -2,77 +2,50 @@ // SPDX-License-Identifier: BSD-2-Clause #include -#include -#include -#include +#include "compat.hpp" -#include +#include namespace privacy_pass::crypto { -// Maximum input size for OpenSSL APIs (prevent integer truncation) +using namespace detail; + constexpr size_t MAX_INPUT_SIZE = static_cast(INT_MAX); Result sha256(ByteView data) { Hash256 result; - - EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + auto ctx = make_evp_md_ctx(); if (!ctx) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create hash context"}); } - - bool success = EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) == 1 && - EVP_DigestUpdate(ctx, data.data(), data.size()) == 1; - unsigned int len = 0; - success = success && EVP_DigestFinal_ex(ctx, result.data(), &len) == 1; - - EVP_MD_CTX_free(ctx); - - if (!success || len != 32) { + if (EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr) != 1 || + EVP_DigestUpdate(ctx.get(), data.data(), data.size()) != 1 || + EVP_DigestFinal_ex(ctx.get(), result.data(), &len) != 1 || len != 32) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "SHA-256 computation failed"}); } - return result; } Result sha384(ByteView data) { Hash384 result; - - EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + auto ctx = make_evp_md_ctx(); if (!ctx) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create hash context"}); } - - bool success = EVP_DigestInit_ex(ctx, EVP_sha384(), nullptr) == 1 && - EVP_DigestUpdate(ctx, data.data(), data.size()) == 1; - unsigned int len = 0; - success = success && EVP_DigestFinal_ex(ctx, result.data(), &len) == 1; - - EVP_MD_CTX_free(ctx); - - if (!success || len != 48) { + if (EVP_DigestInit_ex(ctx.get(), EVP_sha384(), nullptr) != 1 || + EVP_DigestUpdate(ctx.get(), data.data(), data.size()) != 1 || + EVP_DigestFinal_ex(ctx.get(), result.data(), &len) != 1 || len != 48) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "SHA-384 computation failed"}); } - return result; } struct Sha256Hasher::Impl { - EVP_MD_CTX* ctx = nullptr; - - Impl() { - ctx = EVP_MD_CTX_new(); - if (ctx) { - EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr); - } - } - - ~Impl() { - if (ctx) { - EVP_MD_CTX_free(ctx); - } + UniqueEVP_MD_CTX ctx; + Impl() : ctx(make_evp_md_ctx()) { + if (ctx) EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr); } }; @@ -82,40 +55,25 @@ Sha256Hasher::Sha256Hasher(Sha256Hasher&&) noexcept = default; Sha256Hasher& Sha256Hasher::operator=(Sha256Hasher&&) noexcept = default; void Sha256Hasher::update(ByteView data) { - if (impl_ && impl_->ctx) { - EVP_DigestUpdate(impl_->ctx, data.data(), data.size()); - } + if (impl_ && impl_->ctx) EVP_DigestUpdate(impl_->ctx.get(), data.data(), data.size()); } Result Sha256Hasher::finalize() { if (!impl_ || !impl_->ctx) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Invalid hasher state"}); } - Hash256 result; unsigned int len = 0; - - if (EVP_DigestFinal_ex(impl_->ctx, result.data(), &len) != 1 || len != 32) { + if (EVP_DigestFinal_ex(impl_->ctx.get(), result.data(), &len) != 1 || len != 32) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Finalize failed"}); } - return result; } struct Sha384Hasher::Impl { - EVP_MD_CTX* ctx = nullptr; - - Impl() { - ctx = EVP_MD_CTX_new(); - if (ctx) { - EVP_DigestInit_ex(ctx, EVP_sha384(), nullptr); - } - } - - ~Impl() { - if (ctx) { - EVP_MD_CTX_free(ctx); - } + UniqueEVP_MD_CTX ctx; + Impl() : ctx(make_evp_md_ctx()) { + if (ctx) EVP_DigestInit_ex(ctx.get(), EVP_sha384(), nullptr); } }; @@ -125,105 +83,46 @@ Sha384Hasher::Sha384Hasher(Sha384Hasher&&) noexcept = default; Sha384Hasher& Sha384Hasher::operator=(Sha384Hasher&&) noexcept = default; void Sha384Hasher::update(ByteView data) { - if (impl_ && impl_->ctx) { - EVP_DigestUpdate(impl_->ctx, data.data(), data.size()); - } + if (impl_ && impl_->ctx) EVP_DigestUpdate(impl_->ctx.get(), data.data(), data.size()); } Result Sha384Hasher::finalize() { if (!impl_ || !impl_->ctx) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Invalid hasher state"}); } - Hash384 result; unsigned int len = 0; - - if (EVP_DigestFinal_ex(impl_->ctx, result.data(), &len) != 1 || len != 48) { + if (EVP_DigestFinal_ex(impl_->ctx.get(), result.data(), &len) != 1 || len != 48) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Finalize failed"}); } - return result; } Result hmac_sha256(ByteView key, ByteView data) { - // Validate key size to prevent integer truncation if (key.size() > MAX_INPUT_SIZE) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "HMAC key too large"}); } - Hash256 result; unsigned int len = 0; - if (!HMAC(EVP_sha256(), key.data(), static_cast(key.size()), data.data(), data.size(), result.data(), &len) || len != 32) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "HMAC-SHA256 failed"}); } - return result; } Result hkdf_extract_sha256(ByteView salt, ByteView ikm) { - // Validate input sizes to prevent integer truncation if (salt.size() > MAX_INPUT_SIZE || ikm.size() > MAX_INPUT_SIZE) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "HKDF input too large"}); } - - Bytes prk(32); - - EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr); - if (!ctx) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create HKDF context"}); - } - - size_t prk_len = prk.size(); - bool success = - EVP_PKEY_derive_init(ctx) == 1 && - EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256()) == 1 && - EVP_PKEY_CTX_set1_hkdf_salt(ctx, salt.data(), static_cast(salt.size())) == 1 && - EVP_PKEY_CTX_set1_hkdf_key(ctx, ikm.data(), static_cast(ikm.size())) == 1 && - EVP_PKEY_CTX_hkdf_mode(ctx, EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY) == 1 && - EVP_PKEY_derive(ctx, prk.data(), &prk_len) == 1; - - EVP_PKEY_CTX_free(ctx); - - if (!success) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "HKDF-Extract failed"}); - } - - prk.resize(prk_len); - return prk; + return compat::hkdf_extract(EVP_sha256(), salt, ikm); } Result hkdf_expand_sha256(ByteView prk, ByteView info, size_t length) { - // Validate input sizes to prevent integer truncation if (prk.size() > MAX_INPUT_SIZE || info.size() > MAX_INPUT_SIZE) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "HKDF input too large"}); } - - Bytes okm(length); - - EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr); - if (!ctx) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create HKDF context"}); - } - - size_t okm_len = okm.size(); - bool success = - EVP_PKEY_derive_init(ctx) == 1 && - EVP_PKEY_CTX_set_hkdf_md(ctx, EVP_sha256()) == 1 && - EVP_PKEY_CTX_set1_hkdf_key(ctx, prk.data(), static_cast(prk.size())) == 1 && - EVP_PKEY_CTX_add1_hkdf_info(ctx, info.data(), static_cast(info.size())) == 1 && - EVP_PKEY_CTX_hkdf_mode(ctx, EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) == 1 && - EVP_PKEY_derive(ctx, okm.data(), &okm_len) == 1; - - EVP_PKEY_CTX_free(ctx); - - if (!success) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "HKDF-Expand failed"}); - } - - okm.resize(okm_len); - return okm; + return compat::hkdf_expand(EVP_sha256(), prk, info, length); } } // namespace privacy_pass::crypto diff --git a/src/crypto/init.cpp b/src/crypto/init.cpp new file mode 100644 index 0000000..e97ab08 --- /dev/null +++ b/src/crypto/init.cpp @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems +// SPDX-License-Identifier: BSD-2-Clause + +#include +#include "compat.hpp" + +namespace privacy_pass { + +void secure_clear(void* ptr, size_t len) noexcept { + OPENSSL_cleanse(ptr, len); +} + +namespace crypto::detail { + +void backend_init() { compat::backend_init(); } +void backend_shutdown() { compat::backend_shutdown(); } + +} // namespace crypto::detail +} // namespace privacy_pass diff --git a/src/crypto/random.cpp b/src/crypto/random.cpp index 8c0b77a..19edffc 100644 --- a/src/crypto/random.cpp +++ b/src/crypto/random.cpp @@ -6,60 +6,48 @@ namespace privacy_pass::crypto { -// Maximum size for single random generation (prevent integer truncation) constexpr size_t MAX_RANDOM_SIZE = static_cast(INT_MAX); Result random_bytes(size_t count) { - // Validate size to prevent integer truncation if (count > MAX_RANDOM_SIZE) { return std::unexpected(Error{ErrorCode::RANDOM_GENERATION_FAILED, "Requested random size too large"}); } - Bytes result(count); - if (RAND_bytes(result.data(), static_cast(count)) != 1) { return std::unexpected(Error{ErrorCode::RANDOM_GENERATION_FAILED, "Failed to generate random bytes"}); } - return result; } Result random_fill(MutableByteView buffer) { - // Validate size to prevent integer truncation if (buffer.size() > MAX_RANDOM_SIZE) { return std::unexpected(Error{ErrorCode::RANDOM_GENERATION_FAILED, "Buffer size too large"}); } - if (RAND_bytes(buffer.data(), static_cast(buffer.size())) != 1) { return std::unexpected(Error{ErrorCode::RANDOM_GENERATION_FAILED, "Failed to fill buffer with random bytes"}); } - return {}; } Result random_nonce() { Nonce result; - if (RAND_bytes(result.data(), static_cast(result.size())) != 1) { return std::unexpected(Error{ErrorCode::RANDOM_GENERATION_FAILED, "Failed to generate random nonce"}); } - return result; } Result random_u64() { uint64_t result; - if (RAND_bytes(reinterpret_cast(&result), sizeof(result)) != 1) { return std::unexpected(Error{ErrorCode::RANDOM_GENERATION_FAILED, "Failed to generate random uint64"}); } - return result; } diff --git a/src/crypto/voprf.cpp b/src/crypto/voprf.cpp index 2fcd9ed..1040a3a 100644 --- a/src/crypto/voprf.cpp +++ b/src/crypto/voprf.cpp @@ -4,20 +4,16 @@ #include #include -#include -#include -#include -#include -#include +#include "compat.hpp" -#include -#include +#include namespace privacy_pass::crypto { +using namespace detail; + namespace { -// Domain separation tags for OPRF v1, VOPRF mode, P-384/SHA-384. constexpr std::string_view CONTEXT_STRING = "OPRFV1-\x01-P384-SHA384"; constexpr std::string_view DST_H2C = "HashToGroup-OPRFV1-\x01-P384-SHA384"; constexpr std::string_view DST_H2S = "HashToScalar-OPRFV1-\x01-P384-SHA384"; @@ -26,7 +22,6 @@ constexpr std::string_view DST_COMPOSITE = "Composite"; constexpr std::string_view DST_SEED_PREFIX = "Seed-"; constexpr std::string_view DST_FINALIZE = "Finalize"; -// Maximum input size for OpenSSL APIs (prevent integer truncation) constexpr size_t MAX_INPUT_SIZE = static_cast(INT_MAX); // Thread-safe P-384 curve group singleton @@ -34,39 +29,26 @@ class P384Group { public: static EC_GROUP* get() { static P384Group instance; - if (!instance.group_) { - return nullptr; - } return instance.group_; } - - ~P384Group() { - if (group_) { - EC_GROUP_free(group_); - } - } - private: P384Group() { group_ = EC_GROUP_new_by_curve_name(NID_secp384r1); } - + ~P384Group() { + if (group_) EC_GROUP_free(group_); + } P384Group(const P384Group&) = delete; P384Group& operator=(const P384Group&) = delete; - EC_GROUP* group_ = nullptr; }; -// Get P-384 curve group (thread-safe) EC_GROUP* get_p384_group() { return P384Group::get(); } -// Constant-time comparison for cryptographic values bool constant_time_compare(ByteView a, ByteView b) { - if (a.size() != b.size()) { - return false; - } + if (a.size() != b.size()) return false; return CRYPTO_memcmp(a.data(), b.data(), a.size()) == 0; } @@ -80,11 +62,14 @@ void append_u16_len_prefixed(Bytes& out, ByteView data) { out.insert(out.end(), data.begin(), data.end()); } +void append_string(Bytes& out, std::string_view value) { + out.insert(out.end(), value.begin(), value.end()); +} + Result finalize_output(ByteView input, ByteView issued_element) { constexpr size_t MAX_U16 = 0xFFFF; if (input.size() > MAX_U16 || issued_element.size() > MAX_U16) { - return std::unexpected(Error{ErrorCode::INVALID_LENGTH, - "VOPRF finalize input too large"}); + return std::unexpected(Error{ErrorCode::INVALID_LENGTH, "VOPRF finalize input too large"}); } Bytes hash_input; @@ -94,78 +79,49 @@ Result finalize_output(ByteView input, ByteView issued_element) { hash_input.insert(hash_input.end(), DST_FINALIZE.begin(), DST_FINALIZE.end()); auto output = sha384(ByteView(hash_input.data(), hash_input.size())); - if (!output) { - return std::unexpected(output.error()); - } - + if (!output) return std::unexpected(output.error()); return Bytes(output->begin(), output->end()); } -// Serialize EC point to compressed form (per RFC 9497 SerializeElement) Result point_to_bytes(const EC_POINT* point, const EC_GROUP* group) { - BN_CTX* ctx = BN_CTX_new(); + auto ctx = make_bn_ctx(); if (!ctx) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create BN context"}); } - - size_t len = EC_POINT_point2oct(group, point, POINT_CONVERSION_COMPRESSED, - nullptr, 0, ctx); - + size_t len = EC_POINT_point2oct(group, point, POINT_CONVERSION_COMPRESSED, nullptr, 0, ctx.get()); if (len == 0) { - BN_CTX_free(ctx); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get point size"}); } - Bytes result(len); - if (EC_POINT_point2oct(group, point, POINT_CONVERSION_COMPRESSED, - result.data(), len, ctx) != len) { - BN_CTX_free(ctx); + if (EC_POINT_point2oct(group, point, POINT_CONVERSION_COMPRESSED, result.data(), len, ctx.get()) != len) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to serialize point"}); } - - BN_CTX_free(ctx); return result; } -// Deserialize EC point with RFC 9497 P-384 validation. -EC_POINT* bytes_to_point(ByteView data, const EC_GROUP* group) { +UniqueEC_POINT bytes_to_point(ByteView data, const EC_GROUP* group) { if (data.size() != P384_ELEMENT_SIZE || (data[0] != 0x02 && data[0] != 0x03)) { return nullptr; } + auto ctx = make_bn_ctx(); + if (!ctx) return nullptr; - BN_CTX* ctx = BN_CTX_new(); - if (!ctx) { - return nullptr; - } - - EC_POINT* point = EC_POINT_new(group); - if (!point) { - BN_CTX_free(ctx); - return nullptr; - } + auto point = make_ec_point(group); + if (!point) return nullptr; - if (EC_POINT_oct2point(group, point, data.data(), data.size(), ctx) != 1) { - EC_POINT_free(point); - BN_CTX_free(ctx); + if (EC_POINT_oct2point(group, point.get(), data.data(), data.size(), ctx.get()) != 1) { return nullptr; } - - // Validate point is on the curve and not the identity. - if (EC_POINT_is_on_curve(group, point, ctx) != 1 || - EC_POINT_is_at_infinity(group, point) == 1) { - EC_POINT_free(point); - BN_CTX_free(ctx); + if (EC_POINT_is_on_curve(group, point.get(), ctx.get()) != 1 || + EC_POINT_is_at_infinity(group, point.get()) == 1) { return nullptr; } - - BN_CTX_free(ctx); return point; } -// Expand message using XMD (hash to arbitrary length) per RFC 9380 Section 5.3.1 Result expand_message_xmd(ByteView msg, ByteView dst, size_t len_in_bytes) { - const size_t b_in_bytes = 48; // SHA-384 output size - const size_t s_in_bytes = 128; // SHA-384 block size + const size_t b_in_bytes = 48; + const size_t s_in_bytes = 128; if (len_in_bytes > 255 * b_in_bytes || dst.size() > 255) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Invalid expand_message_xmd parameters"}); @@ -173,17 +129,12 @@ Result expand_message_xmd(ByteView msg, ByteView dst, size_t len_in_bytes size_t ell = (len_in_bytes + b_in_bytes - 1) / b_in_bytes; - // DST_prime = DST || I2OSP(len(DST), 1) Bytes dst_prime(dst.begin(), dst.end()); dst_prime.push_back(static_cast(dst.size())); - // Z_pad = I2OSP(0, s_in_bytes) Bytes z_pad(s_in_bytes, 0); - - // l_i_b_str = I2OSP(len_in_bytes, 2) Bytes l_i_b_str = {static_cast(len_in_bytes >> 8), static_cast(len_in_bytes)}; - // msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime Bytes msg_prime; msg_prime.reserve(z_pad.size() + msg.size() + l_i_b_str.size() + 1 + dst_prime.size()); msg_prime.insert(msg_prime.end(), z_pad.begin(), z_pad.end()); @@ -192,13 +143,9 @@ Result expand_message_xmd(ByteView msg, ByteView dst, size_t len_in_bytes msg_prime.push_back(0); msg_prime.insert(msg_prime.end(), dst_prime.begin(), dst_prime.end()); - // b_0 = H(msg_prime) auto b_0 = sha384(ByteView(msg_prime.data(), msg_prime.size())); - if (!b_0) { - return std::unexpected(b_0.error()); - } + if (!b_0) return std::unexpected(b_0.error()); - // b_1 = H(b_0 || I2OSP(1, 1) || DST_prime) Bytes b_1_input; b_1_input.reserve(b_0->size() + 1 + dst_prime.size()); b_1_input.insert(b_1_input.end(), b_0->begin(), b_0->end()); @@ -206,9 +153,7 @@ Result expand_message_xmd(ByteView msg, ByteView dst, size_t len_in_bytes b_1_input.insert(b_1_input.end(), dst_prime.begin(), dst_prime.end()); auto b_1 = sha384(ByteView(b_1_input.data(), b_1_input.size())); - if (!b_1) { - return std::unexpected(b_1.error()); - } + if (!b_1) return std::unexpected(b_1.error()); Bytes uniform_bytes; uniform_bytes.reserve(len_in_bytes); @@ -216,13 +161,11 @@ Result expand_message_xmd(ByteView msg, ByteView dst, size_t len_in_bytes Hash384 b_prev = *b_1; for (size_t i = 2; i <= ell; ++i) { - // strxor(b_0, b_(i-1)) Hash384 xored; for (size_t j = 0; j < b_in_bytes; ++j) { xored[j] = (*b_0)[j] ^ b_prev[j]; } - // b_i = H(strxor(b_0, b_(i-1)) || I2OSP(i, 1) || DST_prime) Bytes b_i_input; b_i_input.reserve(xored.size() + 1 + dst_prime.size()); b_i_input.insert(b_i_input.end(), xored.begin(), xored.end()); @@ -230,9 +173,7 @@ Result expand_message_xmd(ByteView msg, ByteView dst, size_t len_in_bytes b_i_input.insert(b_i_input.end(), dst_prime.begin(), dst_prime.end()); auto b_i = sha384(ByteView(b_i_input.data(), b_i_input.size())); - if (!b_i) { - return std::unexpected(b_i.error()); - } + if (!b_i) return std::unexpected(b_i.error()); uniform_bytes.insert(uniform_bytes.end(), b_i->begin(), b_i->end()); b_prev = *b_i; @@ -242,281 +183,166 @@ Result expand_message_xmd(ByteView msg, ByteView dst, size_t len_in_bytes return uniform_bytes; } -// SSWU map for P-384 per RFC 9380 Appendix F.2 -Result map_to_curve_sswu(const BIGNUM* u, const EC_GROUP* group, BN_CTX* ctx) { - // P-384 constants - // A = -3 (mod p) - // B = b4050a85 0c04b3ab f5413256 5044b0b7 d7bfd8ba 270b3943 2355ffb4 - // a9c7a8a9 acb4b9da 4db97dc6 e2b8a62a cb8dfe7b - // Z = -12 (mod p) - // c1 = (p - 3) / 4 - // c2 = sqrt(-Z) - - BIGNUM* p = BN_new(); - BIGNUM* A = BN_new(); - BIGNUM* B = BN_new(); - BIGNUM* Z = BN_new(); - BIGNUM* tv1 = BN_new(); - BIGNUM* tv2 = BN_new(); - BIGNUM* tv3 = BN_new(); - BIGNUM* tv4 = BN_new(); - BIGNUM* tv5 = BN_new(); - BIGNUM* tv6 = BN_new(); - BIGNUM* x = BN_new(); - BIGNUM* y = BN_new(); - BIGNUM* gx = BN_new(); - BIGNUM* one = BN_new(); - BIGNUM* neg_one = BN_new(); +Result map_to_curve_sswu(const BIGNUM* u, const EC_GROUP* group, BN_CTX* ctx) { + auto p = make_bignum(); + auto A = make_bignum(); + auto B = make_bignum(); + auto Z = make_bignum(); + auto tv1 = make_bignum(); + auto tv2 = make_bignum(); + auto tv3 = make_bignum(); + auto tv4 = make_bignum(); + auto tv5 = make_bignum(); + auto tv6 = make_bignum(); + auto x = make_bignum(); + auto y = make_bignum(); + auto gx = make_bignum(); + auto one = make_bignum(); + auto neg_one = make_bignum(); if (!p || !A || !B || !Z || !tv1 || !tv2 || !tv3 || !tv4 || !tv5 || !tv6 || !x || !y || !gx || !one || !neg_one) { - BN_free(p); BN_free(A); BN_free(B); BN_free(Z); - BN_free(tv1); BN_free(tv2); BN_free(tv3); BN_free(tv4); BN_free(tv5); BN_free(tv6); - BN_free(x); BN_free(y); BN_free(gx); BN_free(one); BN_free(neg_one); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to allocate bignums"}); } - // Get curve parameters - EC_GROUP_get_curve(group, p, A, B, ctx); - BN_set_word(one, 1); - BN_sub(neg_one, p, one); - BN_set_word(Z, 12); - BN_sub(Z, p, Z); // Z = -12 mod p - - // tv1 = u^2 - BN_mod_sqr(tv1, u, p, ctx); - // tv3 = Z * tv1 - BN_mod_mul(tv3, Z, tv1, p, ctx); - // tv5 = tv3^2 - BN_mod_sqr(tv5, tv3, p, ctx); - // tv5 = tv5 + tv3 - BN_mod_add(tv5, tv5, tv3, p, ctx); - // tv4 = tv5 + 1 - BN_mod_add(tv4, tv5, one, p, ctx); - // tv4 = tv4 * B - BN_mod_mul(tv4, tv4, B, p, ctx); - // tv2 = tv3 * B - BN_mod_mul(tv2, tv3, B, p, ctx); - - // Check if tv5 is zero (special case) - BIGNUM* temp = BN_new(); - BN_copy(temp, tv5); - - // tv6 = -A (need to compute denominator) - BN_sub(tv6, p, A); - - if (BN_is_zero(temp)) { - // tv6 = Z * A - BN_mod_mul(tv6, Z, A, p, ctx); + compat::ec_group_get_curve(group, p.get(), A.get(), B.get(), ctx); + BN_set_word(one.get(), 1); + BN_sub(neg_one.get(), p.get(), one.get()); + BN_set_word(Z.get(), 12); + BN_sub(Z.get(), p.get(), Z.get()); + + BN_mod_sqr(tv1.get(), u, p.get(), ctx); + BN_mod_mul(tv3.get(), Z.get(), tv1.get(), p.get(), ctx); + BN_mod_sqr(tv5.get(), tv3.get(), p.get(), ctx); + BN_mod_add(tv5.get(), tv5.get(), tv3.get(), p.get(), ctx); + BN_mod_add(tv4.get(), tv5.get(), one.get(), p.get(), ctx); + BN_mod_mul(tv4.get(), tv4.get(), B.get(), p.get(), ctx); + BN_mod_mul(tv2.get(), tv3.get(), B.get(), p.get(), ctx); + + auto temp = make_bignum(); + BN_copy(temp.get(), tv5.get()); + + BN_sub(tv6.get(), p.get(), A.get()); + + if (BN_is_zero(temp.get())) { + BN_mod_mul(tv6.get(), Z.get(), A.get(), p.get(), ctx); } else { - // tv6 = tv5 * (-A) - BIGNUM* neg_A = BN_new(); - BN_sub(neg_A, p, A); - BN_mod_mul(tv6, tv5, neg_A, p, ctx); - BN_free(neg_A); + auto neg_A = make_bignum(); + BN_sub(neg_A.get(), p.get(), A.get()); + BN_mod_mul(tv6.get(), tv5.get(), neg_A.get(), p.get(), ctx); } - BN_free(temp); - // x = tv4 / tv6 (using modular inverse) - BIGNUM* tv6_inv = BN_mod_inverse(nullptr, tv6, p, ctx); + auto tv6_inv = UniqueBIGNUM(BN_mod_inverse(nullptr, tv6.get(), p.get(), ctx)); if (!tv6_inv) { - BN_free(p); BN_free(A); BN_free(B); BN_free(Z); - BN_free(tv1); BN_free(tv2); BN_free(tv3); BN_free(tv4); BN_free(tv5); BN_free(tv6); - BN_free(x); BN_free(y); BN_free(gx); BN_free(one); BN_free(neg_one); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute inverse"}); } - BN_mod_mul(x, tv4, tv6_inv, p, ctx); - BN_free(tv6_inv); - - // gx = x^2 - BN_mod_sqr(gx, x, p, ctx); - // gx = gx + A - BN_mod_add(gx, gx, A, p, ctx); - // gx = gx * x - BN_mod_mul(gx, gx, x, p, ctx); - // gx = gx + B - BN_mod_add(gx, gx, B, p, ctx); - - // y = sqrt(gx) using Tonelli-Shanks (for p = 3 mod 4, use y = gx^((p+1)/4)) - BIGNUM* exp = BN_new(); - BN_add(exp, p, one); - BN_rshift(exp, exp, 2); // exp = (p+1)/4 - BN_mod_exp(y, gx, exp, p, ctx); - BN_free(exp); - - // Check if y^2 = gx using constant-time comparison - BIGNUM* y_sq = BN_new(); - BN_mod_sqr(y_sq, y, p, ctx); - - // Serialize both for constant-time comparison - int p_bytes = BN_num_bytes(p); + BN_mod_mul(x.get(), tv4.get(), tv6_inv.get(), p.get(), ctx); + + BN_mod_sqr(gx.get(), x.get(), p.get(), ctx); + BN_mod_add(gx.get(), gx.get(), A.get(), p.get(), ctx); + BN_mod_mul(gx.get(), gx.get(), x.get(), p.get(), ctx); + BN_mod_add(gx.get(), gx.get(), B.get(), p.get(), ctx); + + auto exp = make_bignum(); + BN_add(exp.get(), p.get(), one.get()); + BN_rshift(exp.get(), exp.get(), 2); + BN_mod_exp(y.get(), gx.get(), exp.get(), p.get(), ctx); + + auto y_sq = make_bignum(); + BN_mod_sqr(y_sq.get(), y.get(), p.get(), ctx); + + int p_bytes = BN_num_bytes(p.get()); Bytes y_sq_bytes(static_cast(p_bytes)); Bytes gx_bytes(static_cast(p_bytes)); - BN_bn2binpad(y_sq, y_sq_bytes.data(), p_bytes); - BN_bn2binpad(gx, gx_bytes.data(), p_bytes); + BN_bn2binpad(y_sq.get(), y_sq_bytes.data(), p_bytes); + BN_bn2binpad(gx.get(), gx_bytes.data(), p_bytes); bool is_square = constant_time_compare( ByteView(y_sq_bytes.data(), y_sq_bytes.size()), ByteView(gx_bytes.data(), gx_bytes.size())); - BN_free(y_sq); if (!is_square) { - // Use other branch: x = tv3 * x, y = sqrt(Z * u^3 * gx) - BN_mod_mul(x, tv3, x, p, ctx); + BN_mod_mul(x.get(), tv3.get(), x.get(), p.get(), ctx); - // Recompute gx for new x - BN_mod_sqr(gx, x, p, ctx); - BN_mod_add(gx, gx, A, p, ctx); - BN_mod_mul(gx, gx, x, p, ctx); - BN_mod_add(gx, gx, B, p, ctx); + BN_mod_sqr(gx.get(), x.get(), p.get(), ctx); + BN_mod_add(gx.get(), gx.get(), A.get(), p.get(), ctx); + BN_mod_mul(gx.get(), gx.get(), x.get(), p.get(), ctx); + BN_mod_add(gx.get(), gx.get(), B.get(), p.get(), ctx); - exp = BN_new(); - BN_add(exp, p, one); - BN_rshift(exp, exp, 2); - BN_mod_exp(y, gx, exp, p, ctx); - BN_free(exp); + exp = make_bignum(); + BN_add(exp.get(), p.get(), one.get()); + BN_rshift(exp.get(), exp.get(), 2); + BN_mod_exp(y.get(), gx.get(), exp.get(), p.get(), ctx); } - // Ensure y has correct sign (CMOV based on sgn0) - // sgn0(u) = u mod 2 - // sgn0(y) = y mod 2 int sgn0_u = BN_is_odd(u); - int sgn0_y = BN_is_odd(y); + int sgn0_y = BN_is_odd(y.get()); if (sgn0_u != sgn0_y) { - BN_sub(y, p, y); // y = -y + BN_sub(y.get(), p.get(), y.get()); } - // Create point - EC_POINT* point = EC_POINT_new(group); - if (!point || EC_POINT_set_affine_coordinates(group, point, x, y, ctx) != 1) { - EC_POINT_free(point); - BN_free(p); BN_free(A); BN_free(B); BN_free(Z); - BN_free(tv1); BN_free(tv2); BN_free(tv3); BN_free(tv4); BN_free(tv5); BN_free(tv6); - BN_free(x); BN_free(y); BN_free(gx); BN_free(one); BN_free(neg_one); + auto point = make_ec_point(group); + if (!point || EC_POINT_set_affine_coordinates(group, point.get(), x.get(), y.get(), ctx) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create point"}); } - - BN_free(p); BN_free(A); BN_free(B); BN_free(Z); - BN_free(tv1); BN_free(tv2); BN_free(tv3); BN_free(tv4); BN_free(tv5); BN_free(tv6); - BN_free(x); BN_free(y); BN_free(gx); BN_free(one); BN_free(neg_one); - return point; } -// Hash to curve using RFC 9380 compliant SSWU method -Result hash_to_curve(ByteView input, const EC_GROUP* group) { +Result hash_to_curve(ByteView input, const EC_GROUP* group) { if (input.size() > MAX_INPUT_SIZE) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Input too large"}); } - // Per RFC 9380 Section 5.2: hash_to_curve for P-384 - // 1. u = hash_to_field(msg, 2) - // 2. Q0 = map_to_curve(u[0]) - // 3. Q1 = map_to_curve(u[1]) - // 4. R = Q0 + Q1 - // 5. P = clear_cofactor(R) -- cofactor is 1 for P-384 - - // Expand message to get 2 field elements (each 72 bytes for security margin) - size_t L = 72; // ceil((ceil(log2(p)) + k) / 8) where k=128 + size_t L = 72; auto expanded = expand_message_xmd(input, ByteView( reinterpret_cast(DST_H2C.data()), DST_H2C.size()), 2 * L); - if (!expanded) { - return std::unexpected(expanded.error()); - } + if (!expanded) return std::unexpected(expanded.error()); - BN_CTX* ctx = BN_CTX_new(); + auto ctx = make_bn_ctx(); if (!ctx) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create context"}); } - BIGNUM* p = BN_new(); - EC_GROUP_get_curve(group, p, nullptr, nullptr, ctx); - - // u[0] = OS2IP(expand[0:L]) mod p - BIGNUM* u0 = BN_bin2bn(expanded->data(), static_cast(L), nullptr); - BN_mod(u0, u0, p, ctx); - - // u[1] = OS2IP(expand[L:2L]) mod p - BIGNUM* u1 = BN_bin2bn(expanded->data() + L, static_cast(L), nullptr); - BN_mod(u1, u1, p, ctx); - - // Map to curve points - auto Q0_result = map_to_curve_sswu(u0, group, ctx); - if (!Q0_result) { - BN_free(u0); - BN_free(u1); - BN_free(p); - BN_CTX_free(ctx); - return std::unexpected(Q0_result.error()); - } - EC_POINT* Q0 = *Q0_result; - - auto Q1_result = map_to_curve_sswu(u1, group, ctx); - if (!Q1_result) { - EC_POINT_free(Q0); - BN_free(u0); - BN_free(u1); - BN_free(p); - BN_CTX_free(ctx); - return std::unexpected(Q1_result.error()); - } - EC_POINT* Q1 = *Q1_result; - - // R = Q0 + Q1 - EC_POINT* R = EC_POINT_new(group); - if (!R || EC_POINT_add(group, R, Q0, Q1, ctx) != 1) { - EC_POINT_free(Q0); - EC_POINT_free(Q1); - EC_POINT_free(R); - BN_free(u0); - BN_free(u1); - BN_free(p); - BN_CTX_free(ctx); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Point addition failed"}); - } + auto p = make_bignum(); + compat::ec_group_get_curve(group, p.get(), nullptr, nullptr, ctx.get()); + + auto u0 = bin2bn(expanded->data(), static_cast(L)); + BN_mod(u0.get(), u0.get(), p.get(), ctx.get()); - EC_POINT_free(Q0); - EC_POINT_free(Q1); - BN_free(u0); - BN_free(u1); - BN_free(p); - BN_CTX_free(ctx); + auto u1 = bin2bn(expanded->data() + L, static_cast(L)); + BN_mod(u1.get(), u1.get(), p.get(), ctx.get()); - // P-384 has cofactor 1, so no cofactor clearing needed + auto Q0_result = map_to_curve_sswu(u0.get(), group, ctx.get()); + if (!Q0_result) return std::unexpected(Q0_result.error()); + + auto Q1_result = map_to_curve_sswu(u1.get(), group, ctx.get()); + if (!Q1_result) return std::unexpected(Q1_result.error()); + + auto R = make_ec_point(group); + if (!R || EC_POINT_add(group, R.get(), Q0_result->get(), Q1_result->get(), ctx.get()) != 1) { + return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Point addition failed"}); + } return R; } -Result hash_to_scalar(ByteView input, const EC_GROUP* group, BN_CTX* ctx) { +Result hash_to_scalar(ByteView input, const EC_GROUP* group, BN_CTX* ctx) { constexpr size_t L = 72; auto uniform = expand_message_xmd(input, ByteView(reinterpret_cast(DST_H2S.data()), DST_H2S.size()), L); - if (!uniform) { - return std::unexpected(uniform.error()); - } + if (!uniform) return std::unexpected(uniform.error()); - BIGNUM* order = BN_new(); - BIGNUM* scalar = BN_bin2bn(uniform->data(), static_cast(uniform->size()), nullptr); - if (!order || !scalar || EC_GROUP_get_order(group, order, ctx) != 1 || - BN_mod(scalar, scalar, order, ctx) != 1) { - BN_free(order); - BN_free(scalar); + auto order = make_bignum(); + auto scalar = bin2bn(uniform->data(), static_cast(uniform->size())); + if (!order || !scalar || EC_GROUP_get_order(group, order.get(), ctx) != 1 || + BN_mod(scalar.get(), scalar.get(), order.get(), ctx) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to hash to scalar"}); } - - BN_free(order); return scalar; } -void append_string(Bytes& out, std::string_view value) { - out.insert(out.end(), value.begin(), value.end()); -} - -Result compute_composite_scalar( - const EC_GROUP* group, - const EC_POINT* B, - const EC_POINT* C, - const EC_POINT* D, - BN_CTX* ctx) { +Result compute_composite_scalar( + const EC_GROUP* group, const EC_POINT* B, + const EC_POINT* C, const EC_POINT* D, BN_CTX* ctx) { auto Bm = point_to_bytes(B, group); auto Ci = point_to_bytes(C, group); @@ -534,9 +360,7 @@ Result compute_composite_scalar( append_u16_len_prefixed(seed_transcript, ByteView(seed_dst.data(), seed_dst.size())); auto seed = sha384(ByteView(seed_transcript.data(), seed_transcript.size())); - if (!seed) { - return std::unexpected(seed.error()); - } + if (!seed) return std::unexpected(seed.error()); Bytes composite_transcript; append_u16_len_prefixed(composite_transcript, ByteView(seed->data(), seed->size())); @@ -548,42 +372,27 @@ Result compute_composite_scalar( return hash_to_scalar(ByteView(composite_transcript.data(), composite_transcript.size()), group, ctx); } -Result> compute_composites( - const EC_GROUP* group, - const EC_POINT* B, - const EC_POINT* C, - const EC_POINT* D, - BN_CTX* ctx) { +Result> compute_composites( + const EC_GROUP* group, const EC_POINT* B, + const EC_POINT* C, const EC_POINT* D, BN_CTX* ctx) { auto di_result = compute_composite_scalar(group, B, C, D, ctx); - if (!di_result) { - return std::unexpected(di_result.error()); - } - BIGNUM* di = *di_result; + if (!di_result) return std::unexpected(di_result.error()); - EC_POINT* M = EC_POINT_new(group); - EC_POINT* Z = EC_POINT_new(group); + auto M = make_ec_point(group); + auto Z = make_ec_point(group); if (!M || !Z || - EC_POINT_mul(group, M, nullptr, C, di, ctx) != 1 || - EC_POINT_mul(group, Z, nullptr, D, di, ctx) != 1) { - EC_POINT_free(M); - EC_POINT_free(Z); - BN_free(di); + EC_POINT_mul(group, M.get(), nullptr, C, di_result->get(), ctx) != 1 || + EC_POINT_mul(group, Z.get(), nullptr, D, di_result->get(), ctx) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute composites"}); } - - BN_free(di); - return std::make_pair(M, Z); + return std::make_pair(std::move(M), std::move(Z)); } -Result compute_dleq_challenge( - const EC_GROUP* group, - const EC_POINT* B, - const EC_POINT* M, - const EC_POINT* Z, - const EC_POINT* t2, - const EC_POINT* t3, - BN_CTX* ctx) { +Result compute_dleq_challenge( + const EC_GROUP* group, const EC_POINT* B, + const EC_POINT* M, const EC_POINT* Z, + const EC_POINT* t2, const EC_POINT* t3, BN_CTX* ctx) { auto Bm = point_to_bytes(B, group); auto a0 = point_to_bytes(M, group); @@ -605,236 +414,119 @@ Result compute_dleq_challenge( return hash_to_scalar(ByteView(transcript.data(), transcript.size()), group, ctx); } -// Generate DLEQ proof per RFC 9497 Result generate_dleq_proof( - const EC_GROUP* group, - const BIGNUM* k, // private key - const EC_POINT* Y, // public key - const EC_POINT* R, // blinded element - const EC_POINT* Z, // evaluated element - BN_CTX* ctx) { - - BIGNUM* order = BN_new(); - if (!order || EC_GROUP_get_order(group, order, ctx) != 1) { - BN_free(order); + const EC_GROUP* group, const BIGNUM* k, const EC_POINT* Y, + const EC_POINT* R, const EC_POINT* Z, BN_CTX* ctx) { + + auto order = make_bignum(); + if (!order || EC_GROUP_get_order(group, order.get(), ctx) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get group order"}); } auto composites = compute_composites(group, Y, R, Z, ctx); - if (!composites) { - BN_free(order); - return std::unexpected(composites.error()); - } - EC_POINT* M = composites->first; - EC_POINT* composite_Z = composites->second; - - // Generate random scalar t - BIGNUM* t = BN_new(); - if (!BN_rand_range(t, order) || BN_is_zero(t)) { - EC_POINT_free(M); - EC_POINT_free(composite_Z); - BN_free(order); - BN_free(t); + if (!composites) return std::unexpected(composites.error()); + auto& [M, composite_Z] = *composites; + + auto t = make_bignum(); + if (!BN_rand_range(t.get(), order.get()) || BN_is_zero(t.get())) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to generate random scalar"}); } - // A = t * G (G is the generator, implicit when first scalar is non-null) - EC_POINT* A = EC_POINT_new(group); - if (!A || EC_POINT_mul(group, A, t, nullptr, nullptr, ctx) != 1) { - EC_POINT_free(A); - EC_POINT_free(M); - EC_POINT_free(composite_Z); - BN_free(order); - BN_free(t); + auto A = make_ec_point(group); + if (!A || EC_POINT_mul(group, A.get(), t.get(), nullptr, nullptr, ctx) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute A"}); } - // B = t * M - EC_POINT* B = EC_POINT_new(group); - if (!B || EC_POINT_mul(group, B, nullptr, M, t, ctx) != 1) { - EC_POINT_free(A); - EC_POINT_free(B); - EC_POINT_free(M); - EC_POINT_free(composite_Z); - BN_free(order); - BN_free(t); + auto B_pt = make_ec_point(group); + if (!B_pt || EC_POINT_mul(group, B_pt.get(), nullptr, M.get(), t.get(), ctx) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute B"}); } - auto c_result = compute_dleq_challenge(group, Y, M, composite_Z, A, B, ctx); - if (!c_result) { - EC_POINT_free(A); - EC_POINT_free(B); - EC_POINT_free(M); - EC_POINT_free(composite_Z); - BN_free(order); - BN_free(t); - return std::unexpected(c_result.error()); - } - BIGNUM* c = *c_result; + auto c_result = compute_dleq_challenge(group, Y, M.get(), composite_Z.get(), A.get(), B_pt.get(), ctx); + if (!c_result) return std::unexpected(c_result.error()); - // s = t - c * k mod order - BIGNUM* s = BN_new(); - BIGNUM* ck = BN_new(); - BN_mod_mul(ck, c, k, order, ctx); - BN_mod_sub(s, t, ck, order, ctx); + auto s = make_bignum(); + auto ck = make_bignum(); + BN_mod_mul(ck.get(), c_result->get(), k, order.get(), ctx); + BN_mod_sub(s.get(), t.get(), ck.get(), order.get(), ctx); - // Proof = (c, s) Bytes proof(P384_PROOF_SIZE); - int c_len = BN_bn2binpad(c, proof.data(), P384_SCALAR_SIZE); - int s_len = BN_bn2binpad(s, proof.data() + P384_SCALAR_SIZE, P384_SCALAR_SIZE); - - EC_POINT_free(A); - EC_POINT_free(B); - EC_POINT_free(M); - EC_POINT_free(composite_Z); - BN_free(order); - BN_free(t); - BN_free(c); - BN_free(s); - BN_free(ck); + int c_len = BN_bn2binpad(c_result->get(), proof.data(), P384_SCALAR_SIZE); + int s_len = BN_bn2binpad(s.get(), proof.data() + P384_SCALAR_SIZE, P384_SCALAR_SIZE); if (c_len != P384_SCALAR_SIZE || s_len != P384_SCALAR_SIZE) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to serialize proof"}); } - return proof; } -// Verify DLEQ proof per RFC 9497 Result verify_dleq_proof( - const EC_GROUP* group, - const EC_POINT* Y, // public key - const EC_POINT* R, // blinded element - const EC_POINT* Z, // evaluated element - ByteView proof, - BN_CTX* ctx) { + const EC_GROUP* group, const EC_POINT* Y, + const EC_POINT* R, const EC_POINT* Z, + ByteView proof, BN_CTX* ctx) { if (proof.size() != P384_PROOF_SIZE) { return std::unexpected(Error{ErrorCode::VERIFICATION_FAILED, "Invalid proof size"}); } - // Parse proof (c, s) - BIGNUM* c = BN_bin2bn(proof.data(), P384_SCALAR_SIZE, nullptr); - BIGNUM* s = BN_bin2bn(proof.data() + P384_SCALAR_SIZE, P384_SCALAR_SIZE, nullptr); - + auto c = bin2bn(proof.data(), P384_SCALAR_SIZE); + auto s = bin2bn(proof.data() + P384_SCALAR_SIZE, P384_SCALAR_SIZE); if (!c || !s) { - BN_free(c); - BN_free(s); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to parse proof"}); } - // Validate scalars are in range [0, order-1] - BIGNUM* order = BN_new(); - if (!order || EC_GROUP_get_order(group, order, ctx) != 1) { - BN_free(c); - BN_free(s); - BN_free(order); + auto order = make_bignum(); + if (!order || EC_GROUP_get_order(group, order.get(), ctx) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get group order"}); } - if (BN_cmp(c, order) >= 0 || BN_cmp(s, order) >= 0) { - BN_free(c); - BN_free(s); - BN_free(order); + if (BN_cmp(c.get(), order.get()) >= 0 || BN_cmp(s.get(), order.get()) >= 0) { return std::unexpected(Error{ErrorCode::VERIFICATION_FAILED, "Proof scalars out of range"}); } - BN_free(order); auto composites = compute_composites(group, Y, R, Z, ctx); - if (!composites) { - BN_free(c); - BN_free(s); - return std::unexpected(composites.error()); - } - EC_POINT* M = composites->first; - EC_POINT* composite_Z = composites->second; - - // A' = s * G + c * Y (G is the generator, implicit in EC_POINT_mul with non-null first scalar) - EC_POINT* A_prime = EC_POINT_new(group); - if (!A_prime || EC_POINT_mul(group, A_prime, s, Y, c, ctx) != 1) { - EC_POINT_free(A_prime); - EC_POINT_free(M); - EC_POINT_free(composite_Z); - BN_free(c); - BN_free(s); + if (!composites) return std::unexpected(composites.error()); + auto& [M, composite_Z] = *composites; + + auto A_prime = make_ec_point(group); + if (!A_prime || EC_POINT_mul(group, A_prime.get(), s.get(), Y, c.get(), ctx) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute A'"}); } - // B' = s * M + c * Z - EC_POINT* sM = EC_POINT_new(group); - EC_POINT* cZ = EC_POINT_new(group); - EC_POINT* B_prime = EC_POINT_new(group); + auto sM = make_ec_point(group); + auto cZ = make_ec_point(group); + auto B_prime = make_ec_point(group); if (!sM || !cZ || !B_prime || - EC_POINT_mul(group, sM, nullptr, M, s, ctx) != 1 || - EC_POINT_mul(group, cZ, nullptr, composite_Z, c, ctx) != 1 || - EC_POINT_add(group, B_prime, sM, cZ, ctx) != 1) { - EC_POINT_free(A_prime); - EC_POINT_free(sM); - EC_POINT_free(cZ); - EC_POINT_free(B_prime); - EC_POINT_free(M); - EC_POINT_free(composite_Z); - BN_free(c); - BN_free(s); + EC_POINT_mul(group, sM.get(), nullptr, M.get(), s.get(), ctx) != 1 || + EC_POINT_mul(group, cZ.get(), nullptr, composite_Z.get(), c.get(), ctx) != 1 || + EC_POINT_add(group, B_prime.get(), sM.get(), cZ.get(), ctx) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute B'"}); } - auto c_prime_result = compute_dleq_challenge(group, Y, M, composite_Z, A_prime, B_prime, ctx); - if (!c_prime_result) { - EC_POINT_free(A_prime); - EC_POINT_free(sM); - EC_POINT_free(cZ); - EC_POINT_free(B_prime); - EC_POINT_free(M); - EC_POINT_free(composite_Z); - BN_free(c); - BN_free(s); - return std::unexpected(c_prime_result.error()); - } - BIGNUM* c_prime = *c_prime_result; + auto c_prime = compute_dleq_challenge(group, Y, M.get(), composite_Z.get(), A_prime.get(), B_prime.get(), ctx); + if (!c_prime) return std::unexpected(c_prime.error()); - // Verify c == c' using constant-time comparison - // Serialize both to fixed-size byte arrays for constant-time comparison Bytes c_bytes(P384_SCALAR_SIZE); Bytes c_prime_bytes(P384_SCALAR_SIZE); - BN_bn2binpad(c, c_bytes.data(), P384_SCALAR_SIZE); - BN_bn2binpad(c_prime, c_prime_bytes.data(), P384_SCALAR_SIZE); + BN_bn2binpad(c.get(), c_bytes.data(), P384_SCALAR_SIZE); + BN_bn2binpad(c_prime->get(), c_prime_bytes.data(), P384_SCALAR_SIZE); - bool valid = constant_time_compare( + return constant_time_compare( ByteView(c_bytes.data(), c_bytes.size()), ByteView(c_prime_bytes.data(), c_prime_bytes.size())); - - EC_POINT_free(A_prime); - EC_POINT_free(sM); - EC_POINT_free(cZ); - EC_POINT_free(B_prime); - EC_POINT_free(M); - EC_POINT_free(composite_Z); - BN_free(c); - BN_free(s); - BN_free(c_prime); - - return valid; } } // namespace // VoprfPublicKey implementation struct VoprfPublicKey::Impl { - EC_POINT* point = nullptr; + UniqueEC_POINT point; EC_GROUP* group = nullptr; TokenKeyId cached_key_id{}; bool key_id_computed = false; Impl() : group(get_p384_group()) {} - - ~Impl() { - if (point) { - EC_POINT_free(point); - } - } }; VoprfPublicKey::VoprfPublicKey() : impl_(std::make_unique()) {} @@ -847,12 +539,10 @@ Result VoprfPublicKey::from_bytes(ByteView data) { if (!key.impl_->group) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get curve group"}); } - key.impl_->point = bytes_to_point(data, key.impl_->group); if (!key.impl_->point) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Failed to parse public key"}); } - return key; } @@ -863,24 +553,17 @@ Result VoprfPublicKey::to_bytes() const { if (!impl_->group) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get curve group"}); } - - return point_to_bytes(impl_->point, impl_->group); + return point_to_bytes(impl_->point.get(), impl_->group); } Result VoprfPublicKey::key_id() const { - if (impl_->key_id_computed) { - return impl_->cached_key_id; - } + if (impl_->key_id_computed) return impl_->cached_key_id; auto bytes = to_bytes(); - if (!bytes) { - return std::unexpected(bytes.error()); - } + if (!bytes) return std::unexpected(bytes.error()); auto hash = sha256(ByteView(bytes->data(), bytes->size())); - if (!hash) { - return std::unexpected(hash.error()); - } + if (!hash) return std::unexpected(hash.error()); impl_->cached_key_id = *hash; impl_->key_id_computed = true; @@ -893,16 +576,10 @@ bool VoprfPublicKey::is_valid() const noexcept { // VoprfPrivateKey implementation struct VoprfPrivateKey::Impl { - BIGNUM* scalar = nullptr; + UniqueSecureBIGNUM scalar; EC_GROUP* group = nullptr; Impl() : group(get_p384_group()) {} - - ~Impl() { - if (scalar) { - BN_clear_free(scalar); - } - } }; VoprfPrivateKey::VoprfPrivateKey() : impl_(std::make_unique()) {} @@ -916,48 +593,30 @@ Result> VoprfPrivateKey::generate() { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get curve group"}); } - BN_CTX* ctx = BN_CTX_new(); - if (!ctx) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create context"}); - } - - BIGNUM* order = BN_new(); - if (!order || EC_GROUP_get_order(private_key.impl_->group, order, ctx) != 1) { - BN_free(order); - BN_CTX_free(ctx); + auto ctx = make_bn_ctx(); + auto order = make_bignum(); + if (!ctx || !order || EC_GROUP_get_order(private_key.impl_->group, order.get(), ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get group order"}); } - private_key.impl_->scalar = BN_new(); - if (!BN_rand_range(private_key.impl_->scalar, order) || - BN_is_zero(private_key.impl_->scalar)) { - BN_free(order); - BN_CTX_free(ctx); + private_key.impl_->scalar = make_secure_bignum(); + if (!BN_rand_range(private_key.impl_->scalar.get(), order.get()) || + BN_is_zero(private_key.impl_->scalar.get())) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Key generation failed"}); } - // Compute public key: Y = k * G - EC_POINT* pub_point = EC_POINT_new(private_key.impl_->group); + auto pub_point = make_ec_point(private_key.impl_->group); if (!pub_point || - EC_POINT_mul(private_key.impl_->group, pub_point, private_key.impl_->scalar, - nullptr, nullptr, ctx) != 1) { - EC_POINT_free(pub_point); - BN_free(order); - BN_CTX_free(ctx); + EC_POINT_mul(private_key.impl_->group, pub_point.get(), private_key.impl_->scalar.get(), + nullptr, nullptr, ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Public key computation failed"}); } VoprfPublicKey public_key; if (!public_key.impl_->group) { - EC_POINT_free(pub_point); - BN_free(order); - BN_CTX_free(ctx); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get curve group"}); } - public_key.impl_->point = pub_point; - - BN_free(order); - BN_CTX_free(ctx); + public_key.impl_->point = std::move(pub_point); return std::make_pair(std::move(private_key), std::move(public_key)); } @@ -972,23 +631,18 @@ Result VoprfPrivateKey::from_bytes(ByteView data) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get curve group"}); } - key.impl_->scalar = BN_bin2bn(data.data(), static_cast(data.size()), nullptr); + key.impl_->scalar = bin2bn_secure(data.data(), static_cast(data.size())); if (!key.impl_->scalar) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Failed to parse private key"}); } - BN_CTX* ctx = BN_CTX_new(); - BIGNUM* order = BN_new(); - if (!ctx || !order || EC_GROUP_get_order(key.impl_->group, order, ctx) != 1 || - BN_is_zero(key.impl_->scalar) || BN_cmp(key.impl_->scalar, order) >= 0) { - BN_CTX_free(ctx); - BN_free(order); + auto ctx = make_bn_ctx(); + auto order = make_bignum(); + if (!ctx || !order || EC_GROUP_get_order(key.impl_->group, order.get(), ctx.get()) != 1 || + BN_is_zero(key.impl_->scalar.get()) || BN_cmp(key.impl_->scalar.get(), order.get()) >= 0) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Invalid private key scalar"}); } - BN_CTX_free(ctx); - BN_free(order); - return key; } @@ -996,13 +650,10 @@ Result VoprfPrivateKey::to_bytes() const { if (!impl_->scalar) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Key not initialized"}); } - SecureBytes result(P384_SCALAR_SIZE); - int len = BN_bn2binpad(impl_->scalar, result.data(), P384_SCALAR_SIZE); - if (len != P384_SCALAR_SIZE) { + if (BN_bn2binpad(impl_->scalar.get(), result.data(), P384_SCALAR_SIZE) != P384_SCALAR_SIZE) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to serialize private key"}); } - return result; } @@ -1014,28 +665,18 @@ Result VoprfPrivateKey::public_key() const { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get curve group"}); } - BN_CTX* ctx = BN_CTX_new(); - if (!ctx) { - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create context"}); - } - - EC_POINT* pub_point = EC_POINT_new(impl_->group); - if (!pub_point || - EC_POINT_mul(impl_->group, pub_point, impl_->scalar, nullptr, nullptr, ctx) != 1) { - EC_POINT_free(pub_point); - BN_CTX_free(ctx); + auto ctx = make_bn_ctx(); + auto pub_point = make_ec_point(impl_->group); + if (!ctx || !pub_point || + EC_POINT_mul(impl_->group, pub_point.get(), impl_->scalar.get(), nullptr, nullptr, ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Public key computation failed"}); } - BN_CTX_free(ctx); - VoprfPublicKey public_key; if (!public_key.impl_->group) { - EC_POINT_free(pub_point); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get curve group"}); } - public_key.impl_->point = pub_point; - + public_key.impl_->point = std::move(pub_point); return public_key; } @@ -1063,76 +704,34 @@ Result VoprfClient::blind(ByteView input) const { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get curve group"}); } - // Hash input to curve point auto h_result = hash_to_curve(input, group); - if (!h_result) { - return std::unexpected(h_result.error()); - } - EC_POINT* P = *h_result; + if (!h_result) return std::unexpected(h_result.error()); - BN_CTX* ctx = BN_CTX_new(); - if (!ctx) { - EC_POINT_free(P); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create context"}); - } - - // Generate random blinding scalar r - BIGNUM* order = BN_new(); - if (!order || EC_GROUP_get_order(group, order, ctx) != 1) { - BN_free(order); - BN_CTX_free(ctx); - EC_POINT_free(P); + auto ctx = make_bn_ctx(); + auto order = make_bignum(); + if (!ctx || !order || EC_GROUP_get_order(group, order.get(), ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get group order"}); } - BIGNUM* r = BN_new(); - if (!BN_rand_range(r, order) || BN_is_zero(r)) { - BN_free(r); - BN_free(order); - BN_CTX_free(ctx); - EC_POINT_free(P); + auto r = make_secure_bignum(); + if (!BN_rand_range(r.get(), order.get()) || BN_is_zero(r.get())) { return std::unexpected(Error{ErrorCode::BLINDING_FAILED, "Failed to generate blinding scalar"}); } - // Compute blinded element: R = r * P - EC_POINT* R = EC_POINT_new(group); - if (!R || EC_POINT_mul(group, R, nullptr, P, r, ctx) != 1) { - EC_POINT_free(R); - BN_free(r); - BN_free(order); - BN_CTX_free(ctx); - EC_POINT_free(P); + auto R = make_ec_point(group); + if (!R || EC_POINT_mul(group, R.get(), nullptr, h_result->get(), r.get(), ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::BLINDING_FAILED, "Failed to compute blinded element"}); } - // Serialize results VoprfFinalizationData result; - - // Store blinding scalar result.blind_scalar.resize(P384_SCALAR_SIZE); - BN_bn2binpad(r, result.blind_scalar.data(), P384_SCALAR_SIZE); - - // Store blinded element - auto blinded_bytes = point_to_bytes(R, group); - if (!blinded_bytes) { - EC_POINT_free(R); - BN_free(r); - BN_free(order); - BN_CTX_free(ctx); - EC_POINT_free(P); - return std::unexpected(blinded_bytes.error()); - } - result.blinded_element = std::move(*blinded_bytes); + BN_bn2binpad(r.get(), result.blind_scalar.data(), P384_SCALAR_SIZE); - // Store input for finalization + auto blinded_bytes = point_to_bytes(R.get(), group); + if (!blinded_bytes) return std::unexpected(blinded_bytes.error()); + result.blinded_element = std::move(*blinded_bytes); result.input.assign(input.begin(), input.end()); - EC_POINT_free(R); - EC_POINT_free(P); - BN_free(r); - BN_free(order); - BN_CTX_free(ctx); - return result; } @@ -1145,7 +744,6 @@ Result VoprfClient::finalize( return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get curve group"}); } - // Validate input sizes if (evaluation.evaluated_element.size() > MAX_INPUT_SIZE || evaluation.proof.size() != P384_PROOF_SIZE || finalization_data.blind_scalar.size() > MAX_INPUT_SIZE || @@ -1153,121 +751,60 @@ Result VoprfClient::finalize( return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Invalid input sizes"}); } - // Deserialize evaluated element - EC_POINT* Z = bytes_to_point( - ByteView(evaluation.evaluated_element.data(), evaluation.evaluated_element.size()), - group); + auto Z = bytes_to_point( + ByteView(evaluation.evaluated_element.data(), evaluation.evaluated_element.size()), group); if (!Z) { return std::unexpected(Error{ErrorCode::UNBLINDING_FAILED, "Invalid evaluated element"}); } - // Deserialize blinded element (R) for DLEQ verification - EC_POINT* R = bytes_to_point( - ByteView(finalization_data.blinded_element.data(), finalization_data.blinded_element.size()), - group); + auto R = bytes_to_point( + ByteView(finalization_data.blinded_element.data(), finalization_data.blinded_element.size()), group); if (!R) { - EC_POINT_free(Z); return std::unexpected(Error{ErrorCode::UNBLINDING_FAILED, "Invalid blinded element"}); } - BN_CTX* ctx = BN_CTX_new(); + auto ctx = make_bn_ctx(); if (!ctx) { - EC_POINT_free(Z); - EC_POINT_free(R); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create context"}); } - // Get public key for DLEQ verification auto pub_bytes = impl_->public_key.to_bytes(); - if (!pub_bytes) { - BN_CTX_free(ctx); - EC_POINT_free(Z); - EC_POINT_free(R); - return std::unexpected(pub_bytes.error()); - } + if (!pub_bytes) return std::unexpected(pub_bytes.error()); - EC_POINT* Y = bytes_to_point(ByteView(pub_bytes->data(), pub_bytes->size()), group); + auto Y = bytes_to_point(ByteView(pub_bytes->data(), pub_bytes->size()), group); if (!Y) { - BN_CTX_free(ctx); - EC_POINT_free(Z); - EC_POINT_free(R); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to parse public key"}); } - // Verify DLEQ proof before accepting the evaluation auto verify_result = verify_dleq_proof( - group, Y, R, Z, - ByteView(evaluation.proof.data(), evaluation.proof.size()), - ctx); - - if (!verify_result) { - EC_POINT_free(Y); - EC_POINT_free(R); - EC_POINT_free(Z); - BN_CTX_free(ctx); - return std::unexpected(verify_result.error()); - } - + group, Y.get(), R.get(), Z.get(), + ByteView(evaluation.proof.data(), evaluation.proof.size()), ctx.get()); + if (!verify_result) return std::unexpected(verify_result.error()); if (!*verify_result) { - EC_POINT_free(Y); - EC_POINT_free(R); - EC_POINT_free(Z); - BN_CTX_free(ctx); return std::unexpected(Error{ErrorCode::VERIFICATION_FAILED, "DLEQ proof verification failed - evaluation may be malicious"}); } - EC_POINT_free(Y); - EC_POINT_free(R); + auto r = bin2bn_secure(finalization_data.blind_scalar.data(), + static_cast(finalization_data.blind_scalar.size())); - // Recover blinding scalar - BIGNUM* r = BN_bin2bn(finalization_data.blind_scalar.data(), - static_cast(finalization_data.blind_scalar.size()), nullptr); - - // Compute r^-1 - BIGNUM* order = BN_new(); - if (!order || EC_GROUP_get_order(group, order, ctx) != 1) { - BN_clear_free(r); - BN_free(order); - BN_CTX_free(ctx); - EC_POINT_free(Z); + auto order = make_bignum(); + if (!order || EC_GROUP_get_order(group, order.get(), ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get group order"}); } - BIGNUM* r_inv = BN_mod_inverse(nullptr, r, order, ctx); + auto r_inv = UniqueBIGNUM(BN_mod_inverse(nullptr, r.get(), order.get(), ctx.get())); if (!r_inv) { - BN_clear_free(r); - BN_free(order); - BN_CTX_free(ctx); - EC_POINT_free(Z); return std::unexpected(Error{ErrorCode::UNBLINDING_FAILED, "Failed to compute inverse"}); } - // Compute unblinded result: N = r^-1 * Z - EC_POINT* N = EC_POINT_new(group); - if (!N || EC_POINT_mul(group, N, nullptr, Z, r_inv, ctx) != 1) { - EC_POINT_free(N); - BN_free(r_inv); - BN_clear_free(r); - BN_free(order); - BN_CTX_free(ctx); - EC_POINT_free(Z); + auto N = make_ec_point(group); + if (!N || EC_POINT_mul(group, N.get(), nullptr, Z.get(), r_inv.get(), ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::UNBLINDING_FAILED, "Failed to unblind"}); } - // Serialize output point - auto output_point = point_to_bytes(N, group); - - EC_POINT_free(N); - EC_POINT_free(Z); - BN_free(r_inv); - BN_clear_free(r); - BN_free(order); - BN_CTX_free(ctx); - - if (!output_point) { - return std::unexpected(output_point.error()); - } + auto output_point = point_to_bytes(N.get(), group); + if (!output_point) return std::unexpected(output_point.error()); return finalize_output( ByteView(finalization_data.input.data(), finalization_data.input.size()), @@ -1293,114 +830,52 @@ Result VoprfServer::blind_evaluate(ByteView blinded_element) co if (!group) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get curve group"}); } - - // Validate input size if (blinded_element.size() > MAX_INPUT_SIZE) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Blinded element too large"}); } - // Deserialize blinded element - EC_POINT* R = bytes_to_point(blinded_element, group); + auto R = bytes_to_point(blinded_element, group); if (!R) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Invalid blinded element"}); } - BN_CTX* ctx = BN_CTX_new(); + auto ctx = make_bn_ctx(); if (!ctx) { - EC_POINT_free(R); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create context"}); } - // Get private scalar auto scalar_bytes = impl_->private_key.to_bytes(); - if (!scalar_bytes) { - BN_CTX_free(ctx); - EC_POINT_free(R); - return std::unexpected(scalar_bytes.error()); - } + if (!scalar_bytes) return std::unexpected(scalar_bytes.error()); - if (scalar_bytes->size() > MAX_INPUT_SIZE) { - BN_CTX_free(ctx); - EC_POINT_free(R); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Scalar too large"}); - } - - BIGNUM* k = BN_bin2bn(scalar_bytes->data(), - static_cast(scalar_bytes->size()), nullptr); + auto k = bin2bn_secure(scalar_bytes->data(), static_cast(scalar_bytes->size())); - // Compute Z = k * R - EC_POINT* Z = EC_POINT_new(group); - if (!Z || EC_POINT_mul(group, Z, nullptr, R, k, ctx) != 1) { - EC_POINT_free(Z); - BN_clear_free(k); - BN_CTX_free(ctx); - EC_POINT_free(R); + auto Z = make_ec_point(group); + if (!Z || EC_POINT_mul(group, Z.get(), nullptr, R.get(), k.get(), ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Evaluation failed"}); } - // Get public key for DLEQ proof auto pub_key_result = impl_->private_key.public_key(); - if (!pub_key_result) { - EC_POINT_free(Z); - EC_POINT_free(R); - BN_clear_free(k); - BN_CTX_free(ctx); - return std::unexpected(pub_key_result.error()); - } + if (!pub_key_result) return std::unexpected(pub_key_result.error()); auto pub_bytes = pub_key_result->to_bytes(); - if (!pub_bytes) { - EC_POINT_free(Z); - EC_POINT_free(R); - BN_clear_free(k); - BN_CTX_free(ctx); - return std::unexpected(pub_bytes.error()); - } + if (!pub_bytes) return std::unexpected(pub_bytes.error()); - EC_POINT* Y = bytes_to_point(ByteView(pub_bytes->data(), pub_bytes->size()), group); + auto Y = bytes_to_point(ByteView(pub_bytes->data(), pub_bytes->size()), group); if (!Y) { - EC_POINT_free(Z); - EC_POINT_free(R); - BN_clear_free(k); - BN_CTX_free(ctx); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get public key point"}); } - // Generate DLEQ proof per RFC 9497 - auto proof_result = generate_dleq_proof(group, k, Y, R, Z, ctx); - if (!proof_result) { - EC_POINT_free(Y); - EC_POINT_free(Z); - EC_POINT_free(R); - BN_clear_free(k); - BN_CTX_free(ctx); - return std::unexpected(proof_result.error()); - } + auto proof_result = generate_dleq_proof(group, k.get(), Y.get(), R.get(), Z.get(), ctx.get()); + if (!proof_result) return std::unexpected(proof_result.error()); - // Serialize evaluated element - auto z_bytes = point_to_bytes(Z, group); - if (!z_bytes) { - EC_POINT_free(Y); - EC_POINT_free(Z); - BN_clear_free(k); - BN_CTX_free(ctx); - EC_POINT_free(R); - return std::unexpected(z_bytes.error()); - } + auto z_bytes = point_to_bytes(Z.get(), group); + if (!z_bytes) return std::unexpected(z_bytes.error()); VoprfEvaluation result; result.evaluated_element = std::move(*z_bytes); result.proof = std::move(*proof_result); - // Clear sensitive data scalar_bytes->clear(); - - EC_POINT_free(Y); - EC_POINT_free(Z); - EC_POINT_free(R); - BN_clear_free(k); - BN_CTX_free(ctx); - return result; } @@ -1409,77 +884,37 @@ Result VoprfServer::verify_finalize(ByteView input, ByteView output) const if (!group) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to get curve group"}); } - - // Validate input sizes if (input.size() > MAX_INPUT_SIZE) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Input too large"}); } - // Hash input to curve auto h_result = hash_to_curve(input, group); - if (!h_result) { - return std::unexpected(h_result.error()); - } - EC_POINT* P = *h_result; + if (!h_result) return std::unexpected(h_result.error()); - BN_CTX* ctx = BN_CTX_new(); + auto ctx = make_bn_ctx(); if (!ctx) { - EC_POINT_free(P); return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to create context"}); } - // Get private scalar auto scalar_bytes = impl_->private_key.to_bytes(); - if (!scalar_bytes) { - BN_CTX_free(ctx); - EC_POINT_free(P); - return std::unexpected(scalar_bytes.error()); - } - - if (scalar_bytes->size() > MAX_INPUT_SIZE) { - BN_CTX_free(ctx); - EC_POINT_free(P); - return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Scalar too large"}); - } + if (!scalar_bytes) return std::unexpected(scalar_bytes.error()); - BIGNUM* k = BN_bin2bn(scalar_bytes->data(), - static_cast(scalar_bytes->size()), nullptr); + auto k = bin2bn_secure(scalar_bytes->data(), static_cast(scalar_bytes->size())); - // Compute expected output: k * P - EC_POINT* expected = EC_POINT_new(group); - if (!expected || EC_POINT_mul(group, expected, nullptr, P, k, ctx) != 1) { - EC_POINT_free(expected); - BN_clear_free(k); - BN_CTX_free(ctx); - EC_POINT_free(P); + auto expected = make_ec_point(group); + if (!expected || EC_POINT_mul(group, expected.get(), nullptr, h_result->get(), k.get(), ctx.get()) != 1) { return std::unexpected(Error{ErrorCode::VERIFICATION_FAILED, "Failed to compute expected output"}); } - // Hash expected point - auto expected_bytes = point_to_bytes(expected, group); - if (!expected_bytes) { - EC_POINT_free(expected); - BN_clear_free(k); - BN_CTX_free(ctx); - EC_POINT_free(P); - return std::unexpected(expected_bytes.error()); - } + auto expected_bytes = point_to_bytes(expected.get(), group); + if (!expected_bytes) return std::unexpected(expected_bytes.error()); auto expected_hash = finalize_output(input, ByteView(expected_bytes->data(), expected_bytes->size())); - // Clear sensitive data scalar_bytes->clear(); - EC_POINT_free(expected); - EC_POINT_free(P); - BN_clear_free(k); - BN_CTX_free(ctx); - - if (!expected_hash) { - return std::unexpected(expected_hash.error()); - } + if (!expected_hash) return std::unexpected(expected_hash.error()); - // Use constant-time comparison to prevent timing attacks return constant_time_compare(output, ByteView(expected_hash->data(), expected_hash->size())); } diff --git a/tests/test_blind_rsa.cpp b/tests/test_blind_rsa.cpp index 97c64a7..aa6ae8a 100644 --- a/tests/test_blind_rsa.cpp +++ b/tests/test_blind_rsa.cpp @@ -194,7 +194,13 @@ TEST_SUITE("Blind RSA") { } } + // BoringSSL cannot parse RSA-PSS OID (1.2.840.113549.1.1.10) SPKI that + // OpenSSL generates. These test vectors use that format, so skip on BoringSSL. TEST_CASE("RFC 9578 public-verifiable token vectors verify") { +#ifdef PRIVACY_PASS_WITH_BORINGSSL + MESSAGE("Skipped: BoringSSL does not support RSA-PSS OID in SPKI"); + return; +#endif const std::array files{ "pub_verif_rfc9578.go.json", "pub_verif_rfc9578.rust.json", diff --git a/tests/test_crypto_provider.cpp b/tests/test_crypto_provider.cpp new file mode 100644 index 0000000..0dd6a97 --- /dev/null +++ b/tests/test_crypto_provider.cpp @@ -0,0 +1,597 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems +// SPDX-License-Identifier: BSD-2-Clause + +// Tests that verify crypto provider abstraction works correctly +// regardless of which backend (OpenSSL or BoringSSL) is active. + +#include +#include +#include +#include +#include + +using namespace privacy_pass; +using namespace privacy_pass::crypto; + +TEST_SUITE("Crypto Provider") { + TEST_CASE("Backend identification") { +#if defined(PRIVACY_PASS_WITH_OPENSSL) + MESSAGE("Running with OpenSSL backend"); + constexpr bool has_backend = true; +#elif defined(PRIVACY_PASS_WITH_BORINGSSL) + MESSAGE("Running with BoringSSL backend"); + constexpr bool has_backend = true; +#else + constexpr bool has_backend = false; + FAIL("No crypto backend defined"); +#endif + CHECK(has_backend); + } + + TEST_CASE("secure_clear works") { + std::vector buf = {0xDE, 0xAD, 0xBE, 0xEF}; + privacy_pass::secure_clear(buf.data(), buf.size()); + // After clearing, all bytes should be zero + for (auto b : buf) { + CHECK(b == 0); + } + } + + TEST_CASE("SecureBytes clears on destruction") { + uint8_t* raw_ptr = nullptr; + size_t raw_size = 0; + { + SecureBytes sb(32); + // Fill with known pattern + for (size_t i = 0; i < sb.size(); ++i) { + sb[i] = static_cast(i + 1); + } + raw_ptr = sb.data(); + raw_size = sb.size(); + CHECK(raw_size == 32); + CHECK(raw_ptr[0] == 1); + } + // After destruction, we can't safely dereference raw_ptr, but the + // test verifies SecureBytes doesn't crash during destruction + CHECK(raw_ptr != nullptr); + } + + TEST_CASE("SHA-256 known answer test") { + // SHA-256("abc") = ba7816bf 8f01cfea 414140de 5dae2223 b00361a3 96177a9c b410ff61 f20015ad + std::string input = "abc"; + auto result = sha256(ByteView( + reinterpret_cast(input.data()), input.size())); + REQUIRE(result.has_value()); + CHECK((*result)[0] == 0xba); + CHECK((*result)[1] == 0x78); + CHECK((*result)[2] == 0x16); + CHECK((*result)[3] == 0xbf); + CHECK((*result)[31] == 0xad); + } + + TEST_CASE("SHA-384 known answer test") { + // SHA-384("abc") = cb00753f45a35e8b b5a03d699ac65007 272c32ab0eded163 + // 1a8b605a43ff5bed 8086072ba1e7cc23 58baeca134c825a7 + std::string input = "abc"; + auto result = sha384(ByteView( + reinterpret_cast(input.data()), input.size())); + REQUIRE(result.has_value()); + CHECK((*result)[0] == 0xcb); + CHECK((*result)[1] == 0x00); + CHECK((*result)[2] == 0x75); + CHECK((*result)[3] == 0x3f); + CHECK((*result)[47] == 0xa7); + } + + TEST_CASE("HMAC-SHA256 known answer test") { + // RFC 4231 Test Case 2 + // Key = "Jefe" (4 bytes) + // Data = "what do ya want for nothing?" + // HMAC-SHA-256 = 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 + std::string key_str = "Jefe"; + std::string data_str = "what do ya want for nothing?"; + + auto result = hmac_sha256( + ByteView(reinterpret_cast(key_str.data()), key_str.size()), + ByteView(reinterpret_cast(data_str.data()), data_str.size())); + + REQUIRE(result.has_value()); + CHECK((*result)[0] == 0x5b); + CHECK((*result)[1] == 0xdc); + CHECK((*result)[2] == 0xc1); + CHECK((*result)[3] == 0x46); + CHECK((*result)[31] == 0x43); + } + + TEST_CASE("HKDF extract/expand round-trip") { + std::vector ikm(32, 0x0b); + std::vector salt(16, 0x00); + std::vector info = {0xf0, 0xf1, 0xf2, 0xf3}; + + auto prk = hkdf_extract_sha256( + ByteView(salt.data(), salt.size()), + ByteView(ikm.data(), ikm.size())); + REQUIRE(prk.has_value()); + CHECK(prk->size() == 32); + + auto okm = hkdf_expand_sha256( + ByteView(prk->data(), prk->size()), + ByteView(info.data(), info.size()), + 42); + REQUIRE(okm.has_value()); + CHECK(okm->size() == 42); + + // Same inputs should produce same output (deterministic) + auto okm2 = hkdf_expand_sha256( + ByteView(prk->data(), prk->size()), + ByteView(info.data(), info.size()), + 42); + REQUIRE(okm2.has_value()); + CHECK(*okm == *okm2); + } + + TEST_CASE("Random bytes are unique") { + auto a = random_bytes(32); + auto b = random_bytes(32); + REQUIRE(a.has_value()); + REQUIRE(b.has_value()); + CHECK(*a != *b); + } + + TEST_CASE("Random fill covers buffer") { + std::array buffer{}; + auto result = random_fill(MutableByteView(buffer.data(), buffer.size())); + REQUIRE(result.has_value()); + + // Check that it's not all zeros (probabilistically) + int nonzero = 0; + for (auto b : buffer) { + if (b != 0) ++nonzero; + } + CHECK(nonzero > 0); + } + + TEST_CASE("Random nonce uniqueness") { + auto n1 = random_nonce(); + auto n2 = random_nonce(); + auto n3 = random_nonce(); + REQUIRE(n1.has_value()); + REQUIRE(n2.has_value()); + REQUIRE(n3.has_value()); + CHECK(*n1 != *n2); + CHECK(*n2 != *n3); + CHECK(*n1 != *n3); + } + + TEST_CASE("Random u64 uniqueness") { + auto v1 = random_u64(); + auto v2 = random_u64(); + REQUIRE(v1.has_value()); + REQUIRE(v2.has_value()); + CHECK(*v1 != *v2); + } + + TEST_CASE("Incremental hasher matches one-shot") { + auto data = random_bytes(1024); + REQUIRE(data.has_value()); + + // One-shot + auto one_shot = sha256(ByteView(data->data(), data->size())); + REQUIRE(one_shot.has_value()); + + // Incremental in 3 parts + Sha256Hasher hasher; + hasher.update(ByteView(data->data(), 100)); + hasher.update(ByteView(data->data() + 100, 500)); + hasher.update(ByteView(data->data() + 600, 424)); + auto incremental = hasher.finalize(); + REQUIRE(incremental.has_value()); + + CHECK(*one_shot == *incremental); + } + + TEST_CASE("SHA-384 incremental hasher matches one-shot") { + auto data = random_bytes(2048); + REQUIRE(data.has_value()); + + auto one_shot = sha384(ByteView(data->data(), data->size())); + REQUIRE(one_shot.has_value()); + + Sha384Hasher hasher; + hasher.update(ByteView(data->data(), 1024)); + hasher.update(ByteView(data->data() + 1024, 1024)); + auto incremental = hasher.finalize(); + REQUIRE(incremental.has_value()); + + CHECK(*one_shot == *incremental); + } + + TEST_CASE("Hash empty input") { + auto h256 = sha256(ByteView{}); + REQUIRE(h256.has_value()); + CHECK(h256->size() == 32); + + auto h384 = sha384(ByteView{}); + REQUIRE(h384.has_value()); + CHECK(h384->size() == 48); + } + + TEST_CASE("Blind RSA full protocol") { + // Generate keypair + auto keypair = BlindRsaPrivateKey::generate(); + REQUIRE(keypair.has_value()); + auto& [priv_key, pub_key] = *keypair; + + CHECK(priv_key.is_valid()); + CHECK(pub_key.is_valid()); + + // Key ID + auto key_id = pub_key.key_id(); + REQUIRE(key_id.has_value()); + CHECK(key_id->size() == 32); + + // Cached key ID should be the same + auto key_id2 = pub_key.key_id(); + REQUIRE(key_id2.has_value()); + CHECK(*key_id == *key_id2); + + // SPKI round-trip + auto spki = pub_key.to_spki(); + REQUIRE(spki.has_value()); + auto pub_key2 = BlindRsaPublicKey::from_spki(ByteView(spki->data(), spki->size())); + REQUIRE(pub_key2.has_value()); + CHECK(pub_key2->is_valid()); + + // Blind/Sign/Finalize/Verify + auto msg = random_bytes(98); + REQUIRE(msg.has_value()); + + auto blinding = pub_key.blind(ByteView(msg->data(), msg->size())); + REQUIRE(blinding.has_value()); + CHECK(blinding->blinded_msg.size() == RSA_MODULUS_SIZE); + CHECK(blinding->inverse.size() == RSA_MODULUS_SIZE); + + auto blind_sig = priv_key.blind_sign( + ByteView(blinding->blinded_msg.data(), blinding->blinded_msg.size())); + REQUIRE(blind_sig.has_value()); + CHECK(blind_sig->size() == RSA_MODULUS_SIZE); + + auto sig = pub_key.finalize( + ByteView(blind_sig->data(), blind_sig->size()), + *blinding, + ByteView(msg->data(), msg->size())); + REQUIRE(sig.has_value()); + CHECK(sig->size() == RSA_MODULUS_SIZE); + + // Inverse should be cleared after finalize + CHECK(blinding->inverse.empty()); + + auto verified = pub_key.verify( + ByteView(msg->data(), msg->size()), + ByteView(sig->data(), sig->size())); + REQUIRE(verified.has_value()); + CHECK(*verified == true); + + // Wrong message should fail verification + auto wrong_msg = random_bytes(98); + REQUIRE(wrong_msg.has_value()); + auto wrong_verify = pub_key.verify( + ByteView(wrong_msg->data(), wrong_msg->size()), + ByteView(sig->data(), sig->size())); + REQUIRE(wrong_verify.has_value()); + CHECK(*wrong_verify == false); + } + + TEST_CASE("Blind RSA direct sign and verify") { + auto keypair = BlindRsaPrivateKey::generate(); + REQUIRE(keypair.has_value()); + + auto msg = random_bytes(64); + REQUIRE(msg.has_value()); + + auto sig = keypair->first.sign(ByteView(msg->data(), msg->size())); + REQUIRE(sig.has_value()); + + auto verified = keypair->second.verify( + ByteView(msg->data(), msg->size()), + ByteView(sig->data(), sig->size())); + REQUIRE(verified.has_value()); + CHECK(*verified == true); + } + + TEST_CASE("Blind RSA PKCS8 round-trip") { + auto keypair = BlindRsaPrivateKey::generate(); + REQUIRE(keypair.has_value()); + + auto pkcs8 = keypair->first.to_pkcs8(); + REQUIRE(pkcs8.has_value()); + CHECK(!pkcs8->empty()); + + auto restored = BlindRsaPrivateKey::from_pkcs8(pkcs8->view()); + REQUIRE(restored.has_value()); + CHECK(restored->is_valid()); + + // Sign with restored key, verify with original public key + auto msg = random_bytes(32); + REQUIRE(msg.has_value()); + + auto sig = restored->sign(ByteView(msg->data(), msg->size())); + REQUIRE(sig.has_value()); + + auto verified = keypair->second.verify( + ByteView(msg->data(), msg->size()), + ByteView(sig->data(), sig->size())); + REQUIRE(verified.has_value()); + CHECK(*verified == true); + } + + TEST_CASE("Blind RSA from_components") { + auto keypair = BlindRsaPrivateKey::generate(); + REQUIRE(keypair.has_value()); + + // Get SPKI, then reconstruct via components + auto spki = keypair->second.to_spki(); + REQUIRE(spki.has_value()); + + // We can at least verify from_spki round-trips + auto pub2 = BlindRsaPublicKey::from_spki(ByteView(spki->data(), spki->size())); + REQUIRE(pub2.has_value()); + + auto kid1 = keypair->second.key_id(); + auto kid2 = pub2->key_id(); + REQUIRE(kid1.has_value()); + REQUIRE(kid2.has_value()); + CHECK(*kid1 == *kid2); + } + + TEST_CASE("Blind RSA invalid inputs") { + SUBCASE("empty SPKI") { + auto result = BlindRsaPublicKey::from_spki(ByteView{}); + CHECK(!result.has_value()); + } + + SUBCASE("garbage SPKI") { + std::vector garbage(256, 0xFF); + auto result = BlindRsaPublicKey::from_spki(ByteView(garbage.data(), garbage.size())); + CHECK(!result.has_value()); + } + + SUBCASE("empty PKCS8") { + auto result = BlindRsaPrivateKey::from_pkcs8(ByteView{}); + CHECK(!result.has_value()); + } + + SUBCASE("blind_sign wrong size") { + auto keypair = BlindRsaPrivateKey::generate(); + REQUIRE(keypair.has_value()); + + std::vector wrong_size(128, 0x42); + auto result = keypair->first.blind_sign( + ByteView(wrong_size.data(), wrong_size.size())); + CHECK(!result.has_value()); + } + + SUBCASE("uninitialized key operations") { + BlindRsaPublicKey pub; + CHECK(!pub.is_valid()); + + auto blind_result = pub.blind(ByteView{}); + CHECK(!blind_result.has_value()); + + BlindRsaPrivateKey priv; + CHECK(!priv.is_valid()); + } + } + + TEST_CASE("VOPRF full protocol") { + // Generate keypair + auto keypair = VoprfPrivateKey::generate(); + REQUIRE(keypair.has_value()); + auto& [priv_key, pub_key] = *keypair; + + CHECK(priv_key.is_valid()); + CHECK(pub_key.is_valid()); + + // Key ID + auto key_id = pub_key.key_id(); + REQUIRE(key_id.has_value()); + CHECK(key_id->size() == 32); + + // Serialize/deserialize public key round-trip + auto pub_bytes = pub_key.to_bytes(); + REQUIRE(pub_bytes.has_value()); + CHECK(pub_bytes->size() == P384_ELEMENT_SIZE); + + auto pub_key2 = VoprfPublicKey::from_bytes(ByteView(pub_bytes->data(), pub_bytes->size())); + REQUIRE(pub_key2.has_value()); + CHECK(pub_key2->is_valid()); + + // Serialize/deserialize private key round-trip + auto priv_bytes = priv_key.to_bytes(); + REQUIRE(priv_bytes.has_value()); + CHECK(priv_bytes->size() == P384_SCALAR_SIZE); + + auto priv_key2 = VoprfPrivateKey::from_bytes(priv_bytes->view()); + REQUIRE(priv_key2.has_value()); + CHECK(priv_key2->is_valid()); + + // Client blind → Server evaluate → Client finalize + auto client_key = VoprfPublicKey::from_bytes(ByteView(pub_bytes->data(), pub_bytes->size())); + REQUIRE(client_key.has_value()); + VoprfClient client(std::move(*client_key)); + + auto server_key = VoprfPrivateKey::from_bytes(priv_bytes->view()); + REQUIRE(server_key.has_value()); + VoprfServer server(std::move(*server_key)); + + auto input = random_bytes(64); + REQUIRE(input.has_value()); + + auto blind_data = client.blind(ByteView(input->data(), input->size())); + REQUIRE(blind_data.has_value()); + CHECK(blind_data->blinded_element.size() == P384_ELEMENT_SIZE); + CHECK(blind_data->blind_scalar.size() == P384_SCALAR_SIZE); + + auto evaluation = server.blind_evaluate( + ByteView(blind_data->blinded_element.data(), blind_data->blinded_element.size())); + REQUIRE(evaluation.has_value()); + CHECK(evaluation->evaluated_element.size() == P384_ELEMENT_SIZE); + CHECK(evaluation->proof.size() == P384_PROOF_SIZE); + + auto output = client.finalize(*blind_data, *evaluation); + REQUIRE(output.has_value()); + CHECK(output->size() == P384_OUTPUT_SIZE); + + // Server-side verification + auto verified = server.verify_finalize( + ByteView(input->data(), input->size()), + ByteView(output->data(), output->size())); + REQUIRE(verified.has_value()); + CHECK(*verified == true); + + // Wrong input should fail verification + auto wrong_input = random_bytes(64); + REQUIRE(wrong_input.has_value()); + auto wrong_verify = server.verify_finalize( + ByteView(wrong_input->data(), wrong_input->size()), + ByteView(output->data(), output->size())); + REQUIRE(wrong_verify.has_value()); + CHECK(*wrong_verify == false); + } + + TEST_CASE("VOPRF deterministic output") { + // Same key + same input should produce same output + auto keypair = VoprfPrivateKey::generate(); + REQUIRE(keypair.has_value()); + + auto pub_bytes = keypair->second.to_bytes(); + REQUIRE(pub_bytes.has_value()); + auto priv_bytes = keypair->first.to_bytes(); + REQUIRE(priv_bytes.has_value()); + + auto input = random_bytes(32); + REQUIRE(input.has_value()); + + // Run protocol twice + Bytes output1, output2; + for (int i = 0; i < 2; ++i) { + auto ck = VoprfPublicKey::from_bytes(ByteView(pub_bytes->data(), pub_bytes->size())); + REQUIRE(ck.has_value()); + VoprfClient client(std::move(*ck)); + + auto sk = VoprfPrivateKey::from_bytes(priv_bytes->view()); + REQUIRE(sk.has_value()); + VoprfServer server(std::move(*sk)); + + auto blind_data = client.blind(ByteView(input->data(), input->size())); + REQUIRE(blind_data.has_value()); + + auto eval = server.blind_evaluate( + ByteView(blind_data->blinded_element.data(), blind_data->blinded_element.size())); + REQUIRE(eval.has_value()); + + auto out = client.finalize(*blind_data, *eval); + REQUIRE(out.has_value()); + + if (i == 0) output1 = *out; + else output2 = *out; + } + + // Both outputs should be the same (VOPRF is a PRF, deterministic given key+input) + CHECK(output1 == output2); + } + + TEST_CASE("VOPRF invalid inputs") { + SUBCASE("invalid public key bytes") { + std::vector garbage(P384_ELEMENT_SIZE, 0xFF); + auto result = VoprfPublicKey::from_bytes(ByteView(garbage.data(), garbage.size())); + CHECK(!result.has_value()); + } + + SUBCASE("wrong size public key") { + std::vector wrong_size(32, 0x02); + auto result = VoprfPublicKey::from_bytes(ByteView(wrong_size.data(), wrong_size.size())); + CHECK(!result.has_value()); + } + + SUBCASE("invalid private key scalar") { + // Zero scalar should be rejected + std::vector zero_scalar(P384_SCALAR_SIZE, 0x00); + auto result = VoprfPrivateKey::from_bytes(ByteView(zero_scalar.data(), zero_scalar.size())); + CHECK(!result.has_value()); + } + + SUBCASE("wrong size private key") { + std::vector wrong_size(32, 0x01); + auto result = VoprfPrivateKey::from_bytes(ByteView(wrong_size.data(), wrong_size.size())); + CHECK(!result.has_value()); + } + + SUBCASE("uninitialized key operations") { + VoprfPublicKey pub; + CHECK(!pub.is_valid()); + + auto bytes = pub.to_bytes(); + CHECK(!bytes.has_value()); + + VoprfPrivateKey priv; + CHECK(!priv.is_valid()); + } + + SUBCASE("invalid blinded element") { + auto keypair = VoprfPrivateKey::generate(); + REQUIRE(keypair.has_value()); + + auto priv_bytes = keypair->first.to_bytes(); + REQUIRE(priv_bytes.has_value()); + auto server_key = VoprfPrivateKey::from_bytes(priv_bytes->view()); + REQUIRE(server_key.has_value()); + VoprfServer server(std::move(*server_key)); + + std::vector garbage(P384_ELEMENT_SIZE, 0xFF); + auto result = server.blind_evaluate(ByteView(garbage.data(), garbage.size())); + CHECK(!result.has_value()); + } + } + + TEST_CASE("VOPRF multiple evaluations with same key") { + auto keypair = VoprfPrivateKey::generate(); + REQUIRE(keypair.has_value()); + + auto pub_bytes = keypair->second.to_bytes(); + REQUIRE(pub_bytes.has_value()); + auto priv_bytes = keypair->first.to_bytes(); + REQUIRE(priv_bytes.has_value()); + + auto sk = VoprfPrivateKey::from_bytes(priv_bytes->view()); + REQUIRE(sk.has_value()); + VoprfServer server(std::move(*sk)); + + // Evaluate multiple inputs + for (int i = 0; i < 5; ++i) { + auto ck = VoprfPublicKey::from_bytes(ByteView(pub_bytes->data(), pub_bytes->size())); + REQUIRE(ck.has_value()); + VoprfClient client(std::move(*ck)); + + auto input = random_bytes(32 + i * 10); + REQUIRE(input.has_value()); + + auto blind_data = client.blind(ByteView(input->data(), input->size())); + REQUIRE(blind_data.has_value()); + + auto eval = server.blind_evaluate( + ByteView(blind_data->blinded_element.data(), blind_data->blinded_element.size())); + REQUIRE(eval.has_value()); + + auto output = client.finalize(*blind_data, *eval); + REQUIRE(output.has_value()); + CHECK(output->size() == P384_OUTPUT_SIZE); + + auto verified = server.verify_finalize( + ByteView(input->data(), input->size()), + ByteView(output->data(), output->size())); + REQUIRE(verified.has_value()); + CHECK(*verified == true); + } + } +} From 39a2b5ab63e945c42cfbd2fb90026afc64760abc Mon Sep 17 00:00:00 2001 From: Suhas Date: Sun, 12 Jul 2026 21:42:09 -0700 Subject: [PATCH 2/5] Add OpenSSL 1.1 and 3.x multi-version testing support Add just recipes and CI matrix for testing against both OpenSSL versions. --- .github/workflows/ci.yml | 28 ++++++++++++++++++---- README.md | 17 +++++++++---- docs/crypto_backend.md | 52 ++++++++++++++++++++++++++++++++++++++-- justfile | 24 ++++++++++++++++--- 4 files changed, 108 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8d7613..5419bd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,29 +8,49 @@ on: jobs: build-linux: - name: Linux + name: Linux (${{ matrix.openssl }} ${{ matrix.build_type }}) runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: build_type: [Release, Debug] + openssl: [openssl3] + include: + - openssl: openssl1.1 + build_type: Release + - openssl: openssl1.1 + build_type: Debug steps: - uses: actions/checkout@v4 - - name: Install dependencies + - name: Install OpenSSL 3.x + if: matrix.openssl == 'openssl3' run: | sudo apt-get update sudo apt-get install -y libssl-dev + - name: Build and install OpenSSL 1.1 + if: matrix.openssl == 'openssl1.1' + run: | + sudo apt-get update + sudo apt-get install -y build-essential + curl -fsSL https://www.openssl.org/source/openssl-1.1.1w.tar.gz | tar xz + cd openssl-1.1.1w + ./config --prefix=/opt/openssl-1.1 --openssldir=/opt/openssl-1.1 no-shared + make -j$(nproc) + sudo make install_sw + cd .. + rm -rf openssl-1.1.1w + - name: Configure CMake run: | cmake -B build \ -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ - -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" \ -DPRIVACY_PASS_BUILD_TESTS=ON \ -DPRIVACY_PASS_BUILD_BENCHMARKS=ON \ - -DPRIVACY_PASS_BUILD_MOQ=ON + -DPRIVACY_PASS_BUILD_MOQ=ON \ + ${{ matrix.openssl == 'openssl1.1' && '-DOPENSSL_ROOT_DIR=/opt/openssl-1.1' || '' }} - name: Build run: cmake --build build --parallel diff --git a/README.md b/README.md index 89888ef..4fa04df 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ C++ implementation of the Privacy Pass protocol ([RFC9576](https://datatracker.i ## Building -Requires CMake 3.20+, C++23 compiler, and OpenSSL 3.x or BoringSSL. +Requires CMake 3.20+, C++23 compiler, and OpenSSL (1.1 or 3.x) or BoringSSL. See [docs/crypto_backend.md](docs/crypto_backend.md) for multi-backend setup. ```bash @@ -40,9 +40,18 @@ just moq=OFF build # Build without MOQ extension ## Testing ```bash -just test -# or -./build/privacy_pass_tests +just test # Test with system OpenSSL +just test-openssl3 # Test with OpenSSL 3.x +just test-openssl11 # Test with OpenSSL 1.1 +just test-boringssl # Test with BoringSSL +just test-all-crypto # Test all three variants +``` + +Custom OpenSSL paths: + +```bash +just test-openssl11 dir=/path/to/openssl-1.1 +just test-openssl3 dir=/path/to/openssl-3.0 ``` ## License diff --git a/docs/crypto_backend.md b/docs/crypto_backend.md index 13f2f69..4b68a64 100644 --- a/docs/crypto_backend.md +++ b/docs/crypto_backend.md @@ -1,10 +1,18 @@ # Crypto Backend Selection -privacypass-cpp supports **OpenSSL 3.x** and **BoringSSL** as crypto backends, selected at compile time. +privacypass-cpp supports **OpenSSL 1.1+** (including 3.x) and **BoringSSL** as crypto backends, selected at compile time. + +## Supported OpenSSL Versions + +| Version | Status | Notes | +|---------|--------|-------| +| OpenSSL 1.1.1 | Supported | Uses legacy RSA APIs | +| OpenSSL 3.x | Supported (default) | Uses EVP APIs where available | +| BoringSSL | Supported | API differences isolated in compat layer | ## Quick Start -### OpenSSL (default) +### OpenSSL (default — uses system OpenSSL) ```bash cmake -B build @@ -12,6 +20,18 @@ cmake --build build -j$(nproc) ./build/privacy_pass_tests ``` +### OpenSSL with specific version + +```bash +# Point at a specific OpenSSL installation +cmake -B build -DOPENSSL_ROOT_DIR=/path/to/openssl +cmake --build build -j$(nproc) + +# Or via just +just test-openssl11 dir=/usr/local/opt/openssl@1.1 +just test-openssl3 dir=/usr/local/opt/openssl@3 +``` + ### BoringSSL Build BoringSSL from source first: @@ -72,6 +92,34 @@ All API differences between OpenSSL and BoringSSL are isolated in `compat.hpp`. - **RSA-PSS SPKI**: OpenSSL uses OID `1.2.840.113549.1.1.10` (rsaPSS) for RSA-PSS keys. BoringSSL uses plain RSA OID `1.2.840.113549.1.1.1`. SPKI bytes generated by one backend may not be parseable by the other. - **Key generation**: Keys generated by either backend are functionally interoperable at the protocol level (blinded messages, signatures, tokens all work cross-backend). +## Testing Across Backends + +Use `just` to test against all supported crypto variants: + +```bash +just test-openssl # System OpenSSL (whatever version is installed) +just test-openssl11 # OpenSSL 1.1 (default: /usr/local/opt/openssl@1.1) +just test-openssl3 # OpenSSL 3.x (default: /usr/local/opt/openssl@3) +just test-boringssl # BoringSSL +just test-all-crypto # All three in sequence +``` + +Override paths as needed: + +```bash +just test-openssl11 dir=/opt/openssl-1.1 +just test-openssl3 dir=/opt/openssl-3.2 +just test-all-crypto openssl11_dir=/opt/openssl-1.1 openssl3_dir=/opt/openssl-3.2 +``` + +You can also set the `OPENSSL_DIR` environment variable for ad-hoc use: + +```bash +OPENSSL_DIR=/opt/openssl-1.1 just test +``` + +CI automatically tests both OpenSSL 1.1.1 and 3.x on Linux (built from source for 1.1). + ## Benchmarking Run benchmarks with backend labels: diff --git a/justfile b/justfile index 456637a..89c3b64 100644 --- a/justfile +++ b/justfile @@ -12,6 +12,10 @@ backend := "openssl" # BoringSSL path (override with: just boringssl_dir=/path/to/boringssl ...) boringssl_dir := env("BORINGSSL_DIR", "../boringssl") +# OpenSSL path (override with: just openssl_dir=/path/to/openssl ...) +# Leave empty to use system default +openssl_dir := env("OPENSSL_DIR", "") + # Resolved build directory per backend build_dir := if backend == "boringssl" { "build-boringssl" } else { "build" } @@ -25,7 +29,7 @@ configure: -DPRIVACY_PASS_BUILD_MOQ={{moq}} \ -DPRIVACY_PASS_ENABLE_SANITIZERS={{sanitizers}} \ -DPRIVACY_PASS_CRYPTO_BACKEND={{backend}} \ - {{ if backend == "boringssl" { "-DOPENSSL_ROOT_DIR=" + boringssl_dir + " -DOPENSSL_INCLUDE_DIR=" + boringssl_dir + "/include -DOPENSSL_CRYPTO_LIBRARY=" + boringssl_dir + "/build/libcrypto.a -DOPENSSL_SSL_LIBRARY=" + boringssl_dir + "/build/libssl.a" } else { "" } }} + {{ if backend == "boringssl" { "-DOPENSSL_ROOT_DIR=" + boringssl_dir + " -DOPENSSL_INCLUDE_DIR=" + boringssl_dir + "/include -DOPENSSL_CRYPTO_LIBRARY=" + boringssl_dir + "/build/libcrypto.a -DOPENSSL_SSL_LIBRARY=" + boringssl_dir + "/build/libssl.a" } else if openssl_dir != "" { "-DOPENSSL_ROOT_DIR=" + openssl_dir } else { "" } }} # ── Build ──────────────────────────────────────────────────────────────────── @@ -79,19 +83,33 @@ bench-json: build # ── Multi-backend ──────────────────────────────────────────────────────────── -# Build and test with OpenSSL +# Build and test with OpenSSL (system default) test-openssl: just backend=openssl test +# Build and test with OpenSSL 1.1 (requires openssl_dir or OPENSSL_DIR set) +test-openssl11 dir="/usr/local/opt/openssl@1.1": + just backend=openssl openssl_dir={{dir}} test + +# Build and test with OpenSSL 3.x (requires openssl_dir or OPENSSL_DIR set) +test-openssl3 dir="/usr/local/opt/openssl@3": + just backend=openssl openssl_dir={{dir}} test + # Build and test with BoringSSL test-boringssl: just backend=boringssl test -# Build and test both backends +# Build and test all backends (system OpenSSL + BoringSSL) test-all: just backend=openssl test just backend=boringssl test +# Build and test all crypto variants (OpenSSL 1.1, OpenSSL 3.x, BoringSSL) +test-all-crypto openssl11_dir="/usr/local/opt/openssl@1.1" openssl3_dir="/usr/local/opt/openssl@3": + just test-openssl11 dir={{openssl11_dir}} + just test-openssl3 dir={{openssl3_dir}} + just test-boringssl + # Run provider benchmarks on both backends bench-all: just backend=openssl bench-provider From efed70f20bac078dc83db306bfc6a97336e5421c Mon Sep 17 00:00:00 2001 From: Suhas Date: Sun, 12 Jul 2026 21:50:54 -0700 Subject: [PATCH 3/5] Add BoringSSL to CI test matrix Builds BoringSSL from source and tests in both Release and Debug modes. --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5419bd4..f5ae389 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,6 +116,47 @@ jobs: - name: Test run: ctest --test-dir build --output-on-failure -C ${{ matrix.build_type }} + build-boringssl: + name: Linux (BoringSSL ${{ matrix.build_type }}) + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + build_type: [Release, Debug] + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential ninja-build + + - name: Clone and build BoringSSL + run: | + git clone --depth 1 https://boringssl.googlesource.com/boringssl /tmp/boringssl + cmake -B /tmp/boringssl/build -S /tmp/boringssl -GNinja -DCMAKE_BUILD_TYPE=Release + cmake --build /tmp/boringssl/build --target crypto ssl + + - name: Configure CMake + run: | + cmake -B build \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DPRIVACY_PASS_CRYPTO_BACKEND=boringssl \ + -DOPENSSL_ROOT_DIR=/tmp/boringssl \ + -DOPENSSL_INCLUDE_DIR=/tmp/boringssl/include \ + -DOPENSSL_CRYPTO_LIBRARY=/tmp/boringssl/build/libcrypto.a \ + -DOPENSSL_SSL_LIBRARY=/tmp/boringssl/build/libssl.a \ + -DPRIVACY_PASS_BUILD_TESTS=ON \ + -DPRIVACY_PASS_BUILD_BENCHMARKS=ON \ + -DPRIVACY_PASS_BUILD_MOQ=ON + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest --test-dir build --output-on-failure + sanitizers: runs-on: ubuntu-24.04 steps: From 410e1ea401303d1d20a9ba4715c559d513f5d7f4 Mon Sep 17 00:00:00 2001 From: Suhas Date: Sun, 12 Jul 2026 22:23:39 -0700 Subject: [PATCH 4/5] Fix OpenSSL 1.1 and MSVC compatibility in crypto compat layer Add version-conditional code paths for OpenSSL 1.1 and MSVC deprecation warning suppression. --- src/crypto/common.hpp | 5 ++++ src/crypto/compat.hpp | 57 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/crypto/common.hpp b/src/crypto/common.hpp index 0af97ec..e85177a 100644 --- a/src/crypto/common.hpp +++ b/src/crypto/common.hpp @@ -13,6 +13,9 @@ #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#elif defined(_MSC_VER) +#pragma warning(push) +#pragma warning(disable: 4996) #endif #include @@ -96,4 +99,6 @@ inline UniqueSecureBIGNUM bin2bn_secure(const uint8_t* data, int len) { #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop +#elif defined(_MSC_VER) +#pragma warning(pop) #endif diff --git a/src/crypto/compat.hpp b/src/crypto/compat.hpp index 21b1f4c..b7de071 100644 --- a/src/crypto/compat.hpp +++ b/src/crypto/compat.hpp @@ -12,6 +12,9 @@ #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#elif defined(_MSC_VER) +#pragma warning(push) +#pragma warning(disable: 4996) // deprecated declarations #endif #include "common.hpp" @@ -27,8 +30,12 @@ #include #include #else +#include +#if OPENSSL_VERSION_NUMBER >= 0x30000000L +#define PRIVACY_PASS_OPENSSL3 #include #include +#endif #include #endif @@ -145,7 +152,7 @@ inline Result validate_rsa_params(const EVP_PKEY* pkey, return std::unexpected(Error{ErrorCode::INVALID_KEY, "RSA key must be RSA-2048 with exponent 65537"}); } -#else +#elif defined(PRIVACY_PASS_OPENSSL3) if (EVP_PKEY_base_id(pkey) != EVP_PKEY_RSA_PSS) { return std::unexpected(Error{ErrorCode::INVALID_KEY, "Not an RSASSA-PSS key"}); } @@ -183,6 +190,24 @@ inline Result validate_rsa_params(const EVP_PKEY* pkey, return std::unexpected(Error{ErrorCode::INVALID_KEY, "RSASSA-PSS key must use SHA-384 and MGF1-SHA-384"}); } +#else + // OpenSSL 1.1: use low-level RSA API + int pkey_type = EVP_PKEY_base_id(pkey); + if (pkey_type != EVP_PKEY_RSA && pkey_type != EVP_PKEY_RSA_PSS) { + return std::unexpected(Error{ErrorCode::INVALID_KEY, "Not an RSA key"}); + } + const RSA* rsa = EVP_PKEY_get0_RSA(const_cast(pkey)); + if (!rsa) { + return std::unexpected(Error{ErrorCode::INVALID_KEY, "Failed to get RSA key"}); + } + const BIGNUM* n = nullptr; + const BIGNUM* e = nullptr; + RSA_get0_key(rsa, &n, &e, nullptr); + if (!n || !e || BN_num_bits(n) != expected_bits || + !BN_is_word(e, expected_e)) { + return std::unexpected(Error{ErrorCode::INVALID_KEY, + "RSA key must be RSA-2048 with exponent 65537"}); + } #endif return {}; } @@ -260,13 +285,15 @@ inline Result marshal_public_key(const EVP_PKEY* pkey) { OPENSSL_free(data); return result; #else - int len = i2d_PUBKEY(pkey, nullptr); + // OpenSSL 1.1 i2d_PUBKEY doesn't accept const EVP_PKEY* + auto* mutable_pkey = const_cast(pkey); + int len = i2d_PUBKEY(mutable_pkey, nullptr); if (len <= 0) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute SPKI size"}); } Bytes result(static_cast(len)); uint8_t* p = result.data(); - if (i2d_PUBKEY(pkey, &p) != len) { + if (i2d_PUBKEY(mutable_pkey, &p) != len) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to encode SPKI"}); } return result; @@ -290,13 +317,15 @@ inline Result marshal_private_key(const EVP_PKEY* pkey) { OPENSSL_free(data); return result; #else - int len = i2d_PrivateKey(pkey, nullptr); + // OpenSSL 1.1 i2d_PrivateKey doesn't accept const EVP_PKEY* + auto* mutable_pkey = const_cast(pkey); + int len = i2d_PrivateKey(mutable_pkey, nullptr); if (len <= 0) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to compute PKCS#8 size"}); } SecureBytes result(static_cast(len)); uint8_t* p = result.data(); - if (i2d_PrivateKey(pkey, &p) != len) { + if (i2d_PrivateKey(mutable_pkey, &p) != len) { return std::unexpected(Error{ErrorCode::CRYPTO_ERROR, "Failed to encode PKCS#8"}); } return result; @@ -306,7 +335,8 @@ inline Result marshal_private_key(const EVP_PKEY* pkey) { // ── RSA key construction from components ───────────────────────────────────── inline UniqueEVP_PKEY rsa_public_key_from_components(const BIGNUM* n, const BIGNUM* e) { -#ifdef PRIVACY_PASS_WITH_BORINGSSL +#if defined(PRIVACY_PASS_WITH_BORINGSSL) || !defined(PRIVACY_PASS_OPENSSL3) + // BoringSSL and OpenSSL 1.1: use low-level RSA API RSA* rsa = RSA_new(); if (!rsa) return nullptr; BIGNUM* n_dup = BN_dup(n); @@ -324,6 +354,7 @@ inline UniqueEVP_PKEY rsa_public_key_from_components(const BIGNUM* n, const BIGN } return pkey; // rsa ownership transferred to pkey #else + // OpenSSL 3.x: use EVP_PKEY_fromdata with OSSL_PARAM OSSL_PARAM_BLD* bld = OSSL_PARAM_BLD_new(); if (!bld) return nullptr; @@ -397,8 +428,9 @@ inline UniqueEVP_PKEY generate_rsa_pss_keypair(int bits, [[maybe_unused]] int sa // ── RSA get key parameters ─────────────────────────────────────────────────── inline bool rsa_get_bn_param(const EVP_PKEY* pkey, const char* name, UniqueBIGNUM& out) { -#ifdef PRIVACY_PASS_WITH_BORINGSSL - const RSA* rsa = EVP_PKEY_get0_RSA(pkey); +#if defined(PRIVACY_PASS_WITH_BORINGSSL) || !defined(PRIVACY_PASS_OPENSSL3) + // BoringSSL and OpenSSL 1.1: use low-level RSA API + const RSA* rsa = EVP_PKEY_get0_RSA(const_cast(pkey)); if (!rsa) return false; const BIGNUM* n = nullptr; const BIGNUM* e = nullptr; @@ -420,8 +452,9 @@ inline bool rsa_get_bn_param(const EVP_PKEY* pkey, const char* name, UniqueBIGNU inline bool rsa_get_secure_bn_param(const EVP_PKEY* pkey, const char* name, UniqueSecureBIGNUM& out) { -#ifdef PRIVACY_PASS_WITH_BORINGSSL - const RSA* rsa = EVP_PKEY_get0_RSA(pkey); +#if defined(PRIVACY_PASS_WITH_BORINGSSL) || !defined(PRIVACY_PASS_OPENSSL3) + // BoringSSL and OpenSSL 1.1: use low-level RSA API + const RSA* rsa = EVP_PKEY_get0_RSA(const_cast(pkey)); if (!rsa) return false; const BIGNUM* n = nullptr; const BIGNUM* e = nullptr; @@ -441,7 +474,7 @@ inline bool rsa_get_secure_bn_param(const EVP_PKEY* pkey, const char* name, } // Portable param name constants -#ifdef PRIVACY_PASS_WITH_BORINGSSL +#if defined(PRIVACY_PASS_WITH_BORINGSSL) || !defined(PRIVACY_PASS_OPENSSL3) constexpr const char* PARAM_RSA_N = "n"; constexpr const char* PARAM_RSA_E = "e"; constexpr const char* PARAM_RSA_D = "d"; @@ -485,4 +518,6 @@ inline void backend_shutdown() { #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop +#elif defined(_MSC_VER) +#pragma warning(pop) #endif From bd8ce1fa357da21268d483e7232c9baa40c2b496 Mon Sep 17 00:00:00 2001 From: Suhas Date: Sun, 12 Jul 2026 22:32:22 -0700 Subject: [PATCH 5/5] Fix MSVC build: treat doctest headers as system includes Prevents third-party header warnings from triggering /WX errors on Windows. --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f72eb4..86f1f68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -290,6 +290,10 @@ if(PRIVACY_PASS_BUILD_TESTS) doctest::doctest nlohmann_json::nlohmann_json ) + # Treat third-party headers as system includes to suppress warnings on MSVC + target_include_directories(privacy_pass_tests SYSTEM PRIVATE + ${doctest_SOURCE_DIR}/doctest + ) target_compile_definitions(privacy_pass_tests PRIVATE PRIVACY_PASS_TEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/tests/test_data" )