Add an automatically generated OpenSSL backend for the Crypto Refresh and PQC code#2392
Add an automatically generated OpenSSL backend for the Crypto Refresh and PQC code#2392kaie wants to merge 11 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2392 +/- ##
=======================================
Coverage ? 85.39%
=======================================
Files ? 126
Lines ? 22792
Branches ? 0
=======================================
Hits ? 19464
Misses ? 3328
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Thanks a lot for merging the base branch! I've rebased and updated the PR. |
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds an OpenSSL backend implementation for Crypto Refresh + PQC functionality (parity with the existing Botan-based implementation), and adjusts build/test scaffolding accordingly.
Changes:
- Adds OpenSSL implementations for HKDF, (EC)KEMs, and PQC algorithms (ML-KEM, ML-DSA, SLH-DSA).
- Introduces
rnp::SecureBytesto avoid Botan types in shared headers and support secure wiping across backends. - Updates CMake feature gating and adjusts tests to account for OpenSSL backend limitations (e.g., AEAD support).
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tests/pqc-crossbackend-test.sh | Adds a Botan↔OpenSSL PQC round-trip interoperability script. |
| src/tests/ffi-enc.cpp | Updates AEAD test matrix to reflect backend capability differences. |
| src/tests/cli_tests.py | Adjusts PQC CLI tests to avoid EAX when not available. |
| src/lib/crypto/x25519_x448_ossl.cpp | Adds OpenSSL X25519/X448 encrypt/decrypt + RFC3394 wrap/unwrap helpers. |
| src/lib/crypto/sphincsplus_ossl.cpp | Adds OpenSSL SLH-DSA (SPHINCS+) implementation using EVP. |
| src/lib/crypto/sphincsplus.h | Removes Botan types from header (uses SecureBytes). |
| src/lib/crypto/sphincsplus.cpp | Refactors Botan implementation to avoid Botan types in header. |
| src/lib/crypto/secure_bytes.h | Introduces rnp::SecureBytes and secure_wipe() declaration. |
| src/lib/crypto/secure_bytes.cpp | Implements rnp::secure_wipe() utility. |
| src/lib/crypto/kyber_ossl.cpp | Adds OpenSSL ML-KEM implementation using EVP_PKEY + fromdata/encapsulate APIs. |
| src/lib/crypto/kyber_ecdh_composite.cpp | Adds OpenSSL RFC3394 wrap/unwrap path for composite KEM. |
| src/lib/crypto/kyber_common.cpp | Switches Kyber private key storage to SecureBytes. |
| src/lib/crypto/kyber.h | Removes Botan types from header (uses SecureBytes). |
| src/lib/crypto/kyber.cpp | Refactors Botan implementation to avoid Botan types in header. |
| src/lib/crypto/hkdf_ossl.hpp | Declares OpenSSL HKDF implementation class. |
| src/lib/crypto/hkdf_ossl.cpp | Implements OpenSSL HKDF using EVP_KDF. |
| src/lib/crypto/hkdf.cpp | Wires HKDF factory for OpenSSL backend. |
| src/lib/crypto/exdsa_ecdhkem_ossl.cpp | Adds OpenSSL implementation for EXDSA/ECDHKEM functionality. |
| src/lib/crypto/exdsa_ecdhkem.h | Removes Botan types from header (uses SecureBytes). |
| src/lib/crypto/exdsa_ecdhkem.cpp | Refactors Botan implementation to avoid Botan types in header. |
| src/lib/crypto/ed25519_ed448_ossl.cpp | Adds OpenSSL Ed25519/Ed448 keygen/sign/verify/validate. |
| src/lib/crypto/ec_ossl.cpp | Adds ec_generate_native() routing for refresh/PQC curves on OpenSSL. |
| src/lib/crypto/dilithium_ossl.cpp | Adds OpenSSL ML-DSA (Dilithium) implementation using EVP. |
| src/lib/crypto/dilithium_exdsa_composite.cpp | Adjusts composite key ownership patterns (unique_ptr construction). |
| src/lib/crypto/dilithium_common.cpp | Switches Dilithium private key storage to SecureBytes. |
| src/lib/crypto/dilithium.h | Removes Botan types from header (uses SecureBytes). |
| src/lib/crypto/dilithium.cpp | Refactors Botan implementation to avoid Botan types in header. |
| src/lib/CMakeLists.txt | Updates backend feature gating and adds OpenSSL PQC/refresh source lists. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /* Defined in secure_bytes.cpp as a non-inlineable function so the compiler cannot | ||
| eliminate the zeroing as a dead store. */ | ||
| void secure_wipe(void *ptr, size_t len); | ||
|
|
||
| /* Drop-in replacement for Botan::secure_vector<uint8_t> that carries no Botan dependency | ||
| in its header. Memory is zeroed on destruction (and before any move-from). */ | ||
| class SecureBytes { | ||
| std::vector<uint8_t> data_; | ||
|
|
||
| public: | ||
| SecureBytes() = default; | ||
|
|
||
| SecureBytes(const uint8_t *first, const uint8_t *last) : data_(first, last) | ||
| { | ||
| } | ||
| SecureBytes(const uint8_t *data, size_t size) : data_(data, data + size) | ||
| { | ||
| } | ||
|
|
||
| SecureBytes(const std::vector<uint8_t> &v) : data_(v) | ||
| { | ||
| } | ||
| SecureBytes(std::vector<uint8_t> &&v) noexcept : data_(std::move(v)) | ||
| { | ||
| } | ||
|
|
||
| ~SecureBytes() | ||
| { | ||
| secure_wipe(data_.data(), data_.size()); | ||
| } | ||
|
|
||
| SecureBytes(const SecureBytes &) = default; | ||
| SecureBytes &operator=(const SecureBytes &) = default; | ||
|
|
||
| SecureBytes(SecureBytes &&o) noexcept : data_(std::move(o.data_)) | ||
| { | ||
| } | ||
| SecureBytes & | ||
| operator=(SecureBytes &&o) noexcept | ||
| { | ||
| secure_wipe(data_.data(), data_.size()); | ||
| data_ = std::move(o.data_); | ||
| return *this; | ||
| } |
| x25519_validate_key_native(rnp::RNG *rng, const pgp_x25519_key_t *key, bool secret) | ||
| { | ||
| /* mirror Botan version: use priv field for the public key check */ | ||
| rnp::ossl::evp::PKey pub( | ||
| EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL, key->priv.data(), key->priv.size())); | ||
| if (!pub) { | ||
| return RNP_ERROR_BAD_PARAMETERS; | ||
| } | ||
| if (secret) { | ||
| rnp::ossl::evp::PKey priv(EVP_PKEY_new_raw_private_key( | ||
| EVP_PKEY_X25519, NULL, key->priv.data(), key->priv.size())); | ||
| if (!priv) { | ||
| return RNP_ERROR_BAD_PARAMETERS; | ||
| } | ||
| } | ||
| return RNP_SUCCESS; | ||
| } |
| kyber_key_ = std::unique_ptr<pgp_kyber_private_key_t>( | ||
| new pgp_kyber_private_key_t(kyber_key_encoded, pk_alg_to_kyber_id(pk_alg))); | ||
| ecdh_key_ = std::unique_ptr<ecdh_kem_private_key_t>( | ||
| new ecdh_kem_private_key_t(ecdh_key_encoded, pk_alg_to_curve_id(pk_alg))); |
| dilithium_key_ = std::unique_ptr<pgp_dilithium_private_key_t>( | ||
| new pgp_dilithium_private_key_t(dilithium_key_encoded, pk_alg_to_dilithium_id(pk_alg))); | ||
| exdsa_key_ = std::unique_ptr<exdsa_private_key_t>( | ||
| new exdsa_private_key_t(exdsa_key_encoded, pk_alg_to_curve_id(pk_alg))); |
| size_t pub_len = 0; | ||
| EVP_PKEY_get_raw_public_key(pkey.get(), NULL, &pub_len); | ||
| std::vector<uint8_t> pub(pub_len); | ||
| if (EVP_PKEY_get_raw_public_key(pkey.get(), pub.data(), &pub_len) <= 0) { | ||
| RNP_LOG("failed to get ML-KEM public key: %s", rnp::ossl::latest_err()); | ||
| throw rnp::rnp_exception(RNP_ERROR_GENERIC); | ||
| } |
| if(ENABLE_CRYPTO_REFRESH AND CRYPTO_BACKEND_BOTAN) | ||
| find_package(Botan 3.6.0 REQUIRED) | ||
| set(CRYPTO_BACKEND_BOTAN3 1) | ||
| else() | ||
| if (CRYPTO_BACKEND_BOTAN3) | ||
| find_package(Botan 3.0.0 REQUIRED) | ||
| elseif (CRYPTO_BACKEND_BOTAN) | ||
| # botan-config.cmake from Botan distribution doesn't support version 2.x, | ||
| # botan-config.cmake from Botan distribution doesn't support version 2.x, | ||
| # so use -DCRYPTO_BACKEND=botan3 to configure | ||
| find_package(Botan 2.14.0 REQUIRED) | ||
| if(Botan_VERSION VERSION_GREATER_EQUAL 3.0.0) |
| OSSL_RNPK="$REPO_ROOT/build-ossl/src/rnpkeys/rnpkeys" | ||
| PASSWORD="pqc-roundtrip-pw" | ||
|
|
||
| WORKDIR=$(mktemp -d) |
…ackend) Each PQC class (ML-KEM, ML-DSA, SLH-DSA, exdsa/ecdh-kem) previously exposed Botan types (Botan::secure_vector, botan_key() methods) in its header, which prevented those headers from compiling without Botan installed. - Add rnp::SecureBytes (secure_bytes.h/cpp): a backend-agnostic drop-in for Botan::secure_vector<uint8_t> that zeroes memory on destruction. - In each header: replace Botan includes with "crypto/secure_bytes.h"; replace Botan::secure_vector<uint8_t> private key members with rnp::SecureBytes; replace Botan::unlock(key_) with key_.unlock(); remove botan_key() / botan_key_*() private method declarations. - In each .cpp: move the Botan includes here; convert the removed class methods to file-scope static functions (e.g. kyber_privkey_from_bytes, x25519_privkey_from_bytes). No behaviour change; Botan build compiles and passes all tests unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
std::make_unique is a C++14 feature; the project builds with -std=c++11. Replace all std::make_unique<T>(...) with std::unique_ptr<T>(new T(...)). (The same issue exists in kyber_ecdh_composite.cpp and is fixed there as part of the OpenSSL AES key-wrap changes in the next commit.) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… SLH-DSA Implements ENABLE_PQC and ENABLE_CRYPTO_REFRESH for the OpenSSL backend (previously guarded by openssl_nope). Requires OpenSSL 3.5+; configure-time algorithm checks disable the feature if any PQC algorithm is absent. New files: - hkdf_ossl.hpp/cpp: HKDF via EVP_KDF "HKDF" - ed25519_ed448_ossl.cpp: Ed25519/Ed448 keygen, sign, verify, validate - x25519_x448_ossl.cpp: X25519/X448 KEM encap/decap for ECDH-KEM - exdsa_ecdhkem_ossl.cpp: ECDH-KEM and ECDSA-exdsa for NIST/brainpool curves (fixes two pre-existing bugs in the OpenSSL EC backend: EVP_PKEY_KEYPAIR -> EVP_PKEY_PRIVATE_KEY in load_nist_privkey, EVP_PKEY_check -> EVP_PKEY_private_check in is_valid for private-key-only objects) - kyber_ossl.cpp: ML-KEM encap/decap using OSSL_PKEY_PARAM_ML_KEM_SEED - dilithium_ossl.cpp: ML-DSA sign/verify using OSSL_PKEY_PARAM_ML_DSA_SEED - sphincsplus_ossl.cpp: SLH-DSA sign/verify via EVP_PKEY_new_raw_private_key_ex Modified: - CMakeLists.txt: resolve_feature_state for ENABLE_CRYPTO_REFRESH and ENABLE_PQC; add new source files; remove openssl_nope guards - hkdf.cpp: route to Hkdf_OpenSSL::create() instead of #error - ec_ossl.cpp: add ECDH encap/decap helpers needed by X25519/X448 KEM - kyber_ecdh_composite.cpp: AES-256 key wrap/unwrap via EVP_aes_256_wrap() for OpenSSL; also fix std::make_unique -> std::unique_ptr<>(new) for C++11 - ffi-enc.cpp: mark non-AES+AEAD and non-AES+PKESKv6 combos as expected-fail under CRYPTO_BACKEND_OPENSSL (OpenSSL only supports AES-OCB/EAX) - cli_tests.py: fall back from EAX to OCB when RNP_AEAD_EAX is unavailable Key format compatibility with Botan: - ML-KEM: 64-byte seed (OSSL_PKEY_PARAM_ML_KEM_SEED) — matches Botan - ML-DSA: 32-byte seed (OSSL_PKEY_PARAM_ML_DSA_SEED) — matches Botan - SLH-DSA: 4n raw bytes via get_raw_private_key — matches Botan All 301 OpenSSL-backend tests pass. Cross-backend round-trip tests pass for all 7 PQC composite key types (algos 25-28, 31-33) in both directions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
src/tests/pqc-crossbackend-test.sh exercises keyring interoperability between the Botan and OpenSSL backends for all seven PQC composite key types (ML-DSA+Ed25519, ML-DSA+Ed448, ML-DSA+ECDSA-P384/P521, and the three SLH-DSA variants). Each algorithm is tested in both directions: Botan generates / OpenSSL decrypts+verifies, and vice versa. Requires build/ (Botan, ENABLE_CRYPTO_REFRESH=ON) and build-ossl/ (OpenSSL 3.5+, same flags) to be present; see the file header for the cmake invocations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- kyber_ossl.cpp, dilithium_ossl.cpp: zero seed_buf[] with secure_wipe() before returning from keygen; Botan used secure_vector which zeroes automatically, leaving the stack buffer un-wiped was a deviation - sphincsplus_ossl.cpp: zero the temporary priv std::vector with secure_wipe() after copying into SecureBytes; same reasoning - exdsa_ecdhkem_ossl.cpp (verify): replace raw ECDSA_SIG * with rnp::ossl::ECDSASig RAII wrapper (already used in the sign path); also check the return value of the encoding i2d_ECDSA_SIG call - kyber_ecdh_composite.cpp: replace raw EVP_CIPHER_CTX * with rnp::ossl::evp::CipherCtx in both AES-256 key wrap and unwrap paths; add ossl_utils.hpp include for the OpenSSL build Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
secure_bytes.cpp was added to the OpenSSL backend's CRYPTO_SOURCES list but omitted from the equivalent Botan section, causing a link error for rnp::secure_wipe when building the Botan backend with ENABLE_PQC or ENABLE_CRYPTO_REFRESH enabled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Argon2 S2K added on main for v6 SKESK and secret-key protection is implemented only for the Botan backend, so enabling ENABLE_CRYPTO_REFRESH with OpenSSL failed to link the shared librnp (undefined pgp_s2k_argon2). Implement pgp_s2k_argon2 via the OpenSSL 3.2+ ARGON2ID KDF, enumerate KDFs in findopensslfeatures to detect its availability, and require HKDF;ED448;X448;ARGON2ID for ENABLE_CRYPTO_REFRESH on OpenSSL, matching the Botan backend requirements. Derivation runs single-threaded as the default OpenSSL library context has no thread pool; this doesn't affect the derived key, which depends on lanes only. Also make the OpenSSL AEAD associated-data storage dynamically sized: v6 secret-key AEAD uses the whole public key material as AD, which exceeds the fixed 32-byte buffer and aborted key decryption or v6 key generation. Skip EAX-only RFC 9580 test vectors and modes on backends without EAX support (OpenSSL supports OCB only).
Reformat the OSSL_PARAM setup in pgp_s2k_argon2 as demanded by the CI clang-format v11 checker (line-wrap of the SALT construct call and declaration alignment after an intervening comment).
When findopensslfeatures returns no output for a feature category (notably
kdfs on OpenSSL < 3.0, where list_kdfs() is a no-op behind the
OPENSSL_VERSION_NUMBER >= 0x30000000L guard), feature_val is empty.
The unquoted expansions collapse string(TOUPPER ${feature_val} feature_val)
to a single-argument call and string(REPLACE "\n" ";" feature_val ${feature_val})
to three arguments, both of which are hard CMake errors that abort configure
on every OpenSSL-backend leg (RHEL 8, debian-11, macos openssl@1.1, msys2).
Quoting the expansion turns empty into a single empty-string argument,
which both commands handle gracefully.
These changes are based on top of PR #2355, which provides an implementation that uses the Botan backend, but does not provide a corresponding implementation for using the OpenSSL backend.
The patches that I'm providing here in addition have been machine generated by Claude Code.
The use of automatic generation was chosen by me, because I made the assumption that the human creative work that decided how to use crypto primitives to implement the OpenPGP functionality was already done and is contained in the base pull request, and that the implementation of the OpenSSL backend code could inspect the code that uses Botan, and produce equivalent code that uses OpenSSL.
The first patch removes some botan specific code from code that is shared across backends and replaces it with a new helper class for secure wiping of bytes. I reviewed that code and it looks correct to me.
The code builds and passes the existing RNP tests on Linux. I haven't built nor ran tests on other platforms.
A script is provided that tests the interoperability of data across the two backends.
The additional patch that addresses code review issues weren't based on a human review, but based on a re-review by the AI itself.
I'm hereby providing the code as is, but I want to make it clear that I couldn't have written the code to use OpenSSL myself in the time I have available, as I don't have experience with writing code for OpenSSL.
The code is provided in the hope it is useful, in the hope that the PQC functionality can be made available for all users of RNP regardless of which backend cryptography library is preferred, and I suggest that the code should get reviewed for correctness before considering to adopt it.
Regarding copyright headers, for all files that were newly written, Ribose was added as the only copyright holder.
However, those files were automatically created by using the equivalent code files as a template. So they are derivative work
For all files that were originally contributed by MTG, but which were modified by this work, Ribose was added as an additional copyright holder.
I'd like to leave it to Ribose to decide what the correct copyright section should be and fix those sections, if necessary.