Skip to content

Add an automatically generated OpenSSL backend for the Crypto Refresh and PQC code#2392

Open
kaie wants to merge 11 commits into
rnpgp:mainfrom
kaie:pqc-openssl
Open

Add an automatically generated OpenSSL backend for the Crypto Refresh and PQC code#2392
kaie wants to merge 11 commits into
rnpgp:mainfrom
kaie:pqc-openssl

Conversation

@kaie

@kaie kaie commented May 28, 2026

Copy link
Copy Markdown
Contributor

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.

@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@c57f486). Learn more about missing BASE report.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kaie

kaie commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks a lot for merging the base branch!

I've rebased and updated the PR.

@ronaldtse
ronaldtse requested review from Copilot and ni4 July 22, 2026 11:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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::SecureBytes to 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.

Comment on lines +36 to +79
/* 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;
}
Comment on lines +244 to +260
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;
}
Comment on lines +254 to +257
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)));
Comment on lines +264 to +267
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)));
Comment on lines +115 to +121
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);
}
Comment thread src/lib/CMakeLists.txt
Comment on lines +34 to 44
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)
kaie and others added 9 commits July 22, 2026 22:35
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants