diff --git a/CHANGELOG.md b/CHANGELOG.md index a007b8d..a208571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] +### Added + - adds support for JWE-like encryption test cases (`jweenc`, `jweencsha1`/`jweencoaepsha1`, `jweencsha256`/`jweencoaepsha256`): RSA OAEP key wrapping of an AES session key, followed by AES GCM content encryption, performed on a PKCS#11 token + ## 3.16.0 - 2025-02-26 ### Added - Docker buildx recipes and scripts diff --git a/src/Makefile.am b/src/Makefile.am index c6ec904..6432906 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -43,6 +43,7 @@ p11perftest_SOURCES = p11benchmark.cpp p11benchmark.hpp \ p11oaepdec.cpp p11oaepdec.hpp \ p11oaepenc.cpp p11oaepenc.hpp \ p11jwe.cpp p11jwe.hpp \ + p11jweenc.cpp p11jweenc.hpp \ p11ecdsasig.cpp p11ecdsasig.hpp \ p11des3ecb.cpp p11des3ecb.hpp \ p11des3cbc.cpp p11des3cbc.hpp \ diff --git a/src/p11jweenc.cpp b/src/p11jweenc.cpp new file mode 100644 index 0000000..12e6772 --- /dev/null +++ b/src/p11jweenc.cpp @@ -0,0 +1,220 @@ +// -*- mode: c++; c-file-style:"stroustrup"; -*- + +// +// Copyright (c) 2026 Mastercard +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// p11jweenc: JWE-like encryption (RSA OAEP wrap + AES GCM content encryption) + +#include +#include +#include +#include +#include +#include "p11jweenc.hpp" + + +P11JWEEncryptBenchmark::P11JWEEncryptBenchmark(const std::string &label, + const Implementation::Vendor vendor, + const HashAlg hashalg, + const SymAlg symalg) : + P11Benchmark( "JWE(RFC7516) encryption: RSA PKCS OAEP", label, ObjectClass::PublicKey, vendor ), + m_symalg(symalg), + m_hashalg(hashalg) +{ + + using namespace std::literals; + + auto newname = "JWE(RFC7516) encryption: RSA PKCS OAEP("s; + + switch(m_hashalg) { + case HashAlg::SHA1: + newname += "SHA1)"s; + break; + + case HashAlg::SHA256: + newname += "SHA256)"s; + break; + } + + newname += " + AES GCM"s; + switch(m_symalg) { + case SymAlg::GCM128: + newname += "128"s; + break; + + case SymAlg::GCM192: + newname += "192"s; + break; + + case SymAlg::GCM256: + newname += "256"s; + break; + } + + rename(newname); +} + + +P11JWEEncryptBenchmark::P11JWEEncryptBenchmark(const P11JWEEncryptBenchmark & other) : + P11Benchmark(other), m_symalg(other.m_symalg), m_hashalg(other.m_hashalg) +{ } + + +inline P11JWEEncryptBenchmark *P11JWEEncryptBenchmark::clone() const { + return new P11JWEEncryptBenchmark{*this}; +} + + +void P11JWEEncryptBenchmark::setup_gcm_iv() +{ + switch(flavour()) { + case Implementation::Vendor::generic: + { + m_iv.resize(12); + + // shuffle IV to avoid reusing values between iterations + std::random_device rd; + std::mt19937 g(rd()); + std::shuffle(m_iv.begin(), m_iv.end(), g); + + m_gcm_params.pIv = m_iv.data(); + m_gcm_params.ulIvLen = m_iv.size(); + m_gcm_params.ulIvBits = m_iv.size() << 3; + break; + } + + case Implementation::Vendor::luna: + m_iv.resize(16); + + // Luna appends IV to ciphertext when not provided + m_gcm_params.pIv = nullptr; + m_gcm_params.ulIvLen = 0; + m_gcm_params.ulIvBits = 0; + break; + + case Implementation::Vendor::utimaco: + case Implementation::Vendor::entrust: + case Implementation::Vendor::marvell: + { + m_iv.resize(12); + std::fill(m_iv.begin(), m_iv.end(), 0); + + m_gcm_params.pIv = m_iv.data(); + m_gcm_params.ulIvLen = m_iv.size(); + m_gcm_params.ulIvBits = m_iv.size() << 3; + break; + } + + default: + std::cerr << "Unsupported flavour for GCM" << std::endl; + throw std::string("Unsupported architecture"); + } +} + + +void P11JWEEncryptBenchmark::prepare(Session &session, Object &obj, std::optional threadindex) +{ + (void) session; + (void) threadindex; + + m_objhandle = obj.handle(); + + auto modulus = obj.get_attribute_value(AttributeType::Modulus); + m_modulus_size_bytes = modulus.size(); + + if(m_wrapped.size() < m_modulus_size_bytes) { + m_wrapped.resize(m_modulus_size_bytes); + } + + m_encrypted.resize(m_payload.size() + 32); + + switch(m_hashalg) { + case HashAlg::SHA1: + m_rsa_pkcs_oaep_params.hashAlg = CKM_SHA_1; + m_rsa_pkcs_oaep_params.mgf = CKG_MGF1_SHA1; + break; + + case HashAlg::SHA256: + m_rsa_pkcs_oaep_params.hashAlg = CKM_SHA256; + m_rsa_pkcs_oaep_params.mgf = CKG_MGF1_SHA256; + break; + } + + setup_gcm_iv(); +} + + +void P11JWEEncryptBenchmark::crashtestdummy(Session &session) +{ + Byte btrue = CK_TRUE; + Byte bfalse = CK_FALSE; + Mechanism mech_aes_key_gen { CKM_AES_KEY_GEN, nullptr, 0 }; + Ulong keylen; + + switch(m_symalg) { + case SymAlg::GCM128: + keylen = 128/8; + break; + + case SymAlg::GCM192: + keylen = 192/8; + break; + + case SymAlg::GCM256: + keylen = 256/8; + break; + + default: + std::cerr << "Invalid keylen, aborting" << std::endl; + throw std::string("Invalid keylen"); + } + + std::array aeskeytemplate { + { + { static_cast(AttributeType::Token), &bfalse, sizeof(Byte) }, + { static_cast(AttributeType::Private), &btrue, sizeof(Byte) }, + { static_cast(AttributeType::Encrypt), &btrue, sizeof(Byte) }, + { static_cast(AttributeType::Decrypt), &btrue, sizeof(Byte) }, + { static_cast(AttributeType::Extractable), &btrue, sizeof(Byte) }, + { static_cast(AttributeType::ValueLen), &keylen, sizeof(Ulong) } + } + }; + + ObjectHandle symkey_handle; + + session.module()->C_GenerateKey(session.handle(), &mech_aes_key_gen, aeskeytemplate.data(), aeskeytemplate.size(), &symkey_handle ); + + if(m_wrapped.size() < m_modulus_size_bytes) { + m_wrapped.resize(m_modulus_size_bytes); + } + + Ulong wrapped_size = m_wrapped.size(); + session.module()->C_WrapKey( session.handle(), &m_mech_rsa_pkcs_oaep, m_objhandle, symkey_handle, m_wrapped.data(), &wrapped_size); + m_wrapped.resize(wrapped_size); + + setup_gcm_iv(); + + if(m_encrypted.size() < m_payload.size() + 32) { + m_encrypted.resize(m_payload.size() + 32); + } + + Ulong returned_len = m_encrypted.size(); + session.module()->C_EncryptInit(session.handle(), &m_mech_aes_gcm, symkey_handle); + session.module()->C_Encrypt(session.handle(), m_payload.data(), m_payload.size(), m_encrypted.data(), &returned_len); + m_encrypted.resize(returned_len); + + session.module()->C_DestroyObject(session.handle(), symkey_handle); +} diff --git a/src/p11jweenc.hpp b/src/p11jweenc.hpp new file mode 100644 index 0000000..9c864af --- /dev/null +++ b/src/p11jweenc.hpp @@ -0,0 +1,105 @@ +// -*- mode: c++; c-file-style:"stroustrup"; -*- + +// +// Copyright (c) 2026 Mastercard +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// p11jweenc: JWE-like encryption (RSA OAEP wrap + AES GCM content encryption) + +#if !defined P11JWEENC_HPP +#define P11JWEENC_HPP + +#include "p11benchmark.hpp" + +// ============================================================================ +// TEST CASE: JWE Encryption (RSA-OAEP + AES-GCM) +// ============================================================================ +// +// DESCRIPTION: +// Measures the cost of producing a JWE-like payload: generate a fresh AES +// content-encryption key (CEK), wrap it with the provided RSA public key +// using OAEP, encrypt the payload with AES-GCM, then destroy the CEK. +// +// OPTIONS / VARIANTS: +// - Hash algorithm for OAEP: SHA1 or SHA256 +// - AES-GCM key size: 128, 192, or 256 bits +// +// FLAVOUR HANDLING: +// Vendor-specific behaviours for GCM IV handling are aligned with the +// existing JWE decrypt benchmark (see p11jwe.hpp/cpp). +// ============================================================================ + +class P11JWEEncryptBenchmark : public P11Benchmark +{ +public: + enum class SymAlg : size_t { + GCM256 = 256/8, + GCM192 = 192/8, + GCM128 = 128/8 + }; + + enum class HashAlg : size_t { + SHA1, + SHA256 + }; + +private: + SymAlg m_symalg; + HashAlg m_hashalg; + std::vector m_wrapped; + std::vector m_encrypted; + std::vector m_iv; + ObjectHandle m_objhandle; + size_t m_modulus_size_bytes { 0 }; + + CK_RSA_PKCS_OAEP_PARAMS m_rsa_pkcs_oaep_params { + CKM_SHA_1, + CKG_MGF1_SHA1, + CKZ_DATA_SPECIFIED, + nullptr, + 0L + }; + + Mechanism m_mech_rsa_pkcs_oaep { CKM_RSA_PKCS_OAEP, &m_rsa_pkcs_oaep_params, sizeof(m_rsa_pkcs_oaep_params) }; + + CK_GCM_PARAMS m_gcm_params { + nullptr, + 0, + 0, + nullptr, + 0, + 128 + }; + + Mechanism m_mech_aes_gcm { CKM_AES_GCM, &m_gcm_params, sizeof m_gcm_params }; + + void setup_gcm_iv(); + + virtual void prepare(Session &session, Object &obj, std::optional threadindex) override; + virtual void crashtestdummy(Session &session) override; + virtual P11JWEEncryptBenchmark *clone() const override; + +public: + + P11JWEEncryptBenchmark(const std::string &name, + const Implementation::Vendor vendor = Implementation::Vendor::generic, + const HashAlg hashalg = HashAlg::SHA1, + const SymAlg symalg = SymAlg::GCM256); + + P11JWEEncryptBenchmark(const P11JWEEncryptBenchmark & other); +}; + + +#endif // P11JWEENC_HPP diff --git a/src/p11perftest.cpp b/src/p11perftest.cpp index 0d1717a..f6457bb 100644 --- a/src/p11perftest.cpp +++ b/src/p11perftest.cpp @@ -58,6 +58,7 @@ #include "p11oaepenc.hpp" #include "p11oaepunw.hpp" #include "p11jwe.hpp" +#include "p11jweenc.hpp" #include "p11ecdsasig.hpp" #include "p11ecdh1derive.hpp" #include "p11xorkeydataderive.hpp" @@ -113,7 +114,7 @@ int main(int argc, char **argv) po::options_description envvars("environment variables"); // default coverage: RSA, ECDSA, HMAC, DES and AES - const auto default_tests {"rsa,rsapss,ecdsa,ecdh,hmac,des,aes,xorder,rand,find,jwe,oaep,oaepenc,oaepunw"}; + const auto default_tests {"rsa,rsapss,ecdsa,ecdh,hmac,des,aes,xorder,rand,find,jwe,jweenc,oaep,oaepenc,oaepunw"}; const auto default_vectors {"8,16,64,256,1024,4096"}; const auto default_keysizes{"rsa2048,rsa3072,rsa4096,ecnistp256,ecnistp384,ecnistp521,hmac160,hmac256,hmac512,des128,des192,aes128,aes192,aes256"}; const auto default_flavour{"generic"}; @@ -149,7 +150,8 @@ int main(int argc, char **argv) " - oaep = oaepsha1 + oaepsha256\n" " - oaepuwn = oaepunwsha1 + oaepunwsha256\n" " - oaepenc = oaepencsha1 + oaepencsha256\n" - " - jwe = jweoaepsha1 + jweoaepsha256") + " - jwe = jweoaepsha1 + jweoaepsha256\n" + " - jweenc = jweencoaepsha1 + jweencoaepsha256") ("vectors,v", po::value< std::string >()->default_value(default_vectors), "test vectors to use") ("keysizes,k", po::value< std::string >()->default_value(default_keysizes), "key sizes or curves to use") ("flavour,f", po::value< std::string >()->default_value(default_flavour), help_text_flavour.c_str() ) @@ -350,6 +352,9 @@ int main(int argc, char **argv) || tests.contains("jwe") || tests.contains("jweoaepsha1") || tests.contains("jweoaepsha256") + || tests.contains("jweenc") + || tests.contains("jweencoaepsha1") + || tests.contains("jweencoaepsha256") || tests.contains("oaep") || tests.contains("oaepsha1") || tests.contains("oaepsha256") @@ -611,6 +616,62 @@ int main(int argc, char **argv) if(keysizes.contains("rsa4096") && has_key("rsa-4096")) benchmarks.emplace_front( new P11OAEPUnwrapBenchmark("rsa-4096", vendor, P11OAEPUnwrapBenchmark::HashAlg::SHA256) ); } + // JWE-like encryption ( RSA OAEP + AES GCM ) + // AES keys are generated per iteration; we only check RSA key presence + if(tests.contains("jweenc") || tests.contains("jweencoaepsha1")) { + if(keysizes.contains("rsa2048") && has_key("rsa-2048")) { + if(keysizes.contains("aes128") ) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-2048", vendor, P11JWEEncryptBenchmark::HashAlg::SHA1, P11JWEEncryptBenchmark::SymAlg::GCM128) ); + if(keysizes.contains("aes192")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-2048", vendor, P11JWEEncryptBenchmark::HashAlg::SHA1, P11JWEEncryptBenchmark::SymAlg::GCM192) ); + if(keysizes.contains("aes256")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-2048", vendor, P11JWEEncryptBenchmark::HashAlg::SHA1, P11JWEEncryptBenchmark::SymAlg::GCM256) ); + } + if(keysizes.contains("rsa3072") && has_key("rsa-3072")) { + if(keysizes.contains("aes128")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-3072", vendor, P11JWEEncryptBenchmark::HashAlg::SHA1, P11JWEEncryptBenchmark::SymAlg::GCM128) ); + if(keysizes.contains("aes192")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-3072", vendor, P11JWEEncryptBenchmark::HashAlg::SHA1, P11JWEEncryptBenchmark::SymAlg::GCM192) ); + if(keysizes.contains("aes256")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-3072", vendor, P11JWEEncryptBenchmark::HashAlg::SHA1, P11JWEEncryptBenchmark::SymAlg::GCM256) ); + } + if(keysizes.contains("rsa4096") && has_key("rsa-4096")) { + if(keysizes.contains("aes128")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-4096", vendor, P11JWEEncryptBenchmark::HashAlg::SHA1, P11JWEEncryptBenchmark::SymAlg::GCM128) ); + if(keysizes.contains("aes192")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-4096", vendor, P11JWEEncryptBenchmark::HashAlg::SHA1, P11JWEEncryptBenchmark::SymAlg::GCM192) ); + if(keysizes.contains("aes256")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-4096", vendor, P11JWEEncryptBenchmark::HashAlg::SHA1, P11JWEEncryptBenchmark::SymAlg::GCM256) ); + } + } + + if(tests.contains("jweenc") || tests.contains("jweencoaepsha256")) { + if(keysizes.contains("rsa2048") && has_key("rsa-2048")) { + if(keysizes.contains("aes128")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-2048", vendor, P11JWEEncryptBenchmark::HashAlg::SHA256, P11JWEEncryptBenchmark::SymAlg::GCM128) ); + if(keysizes.contains("aes192")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-2048", vendor, P11JWEEncryptBenchmark::HashAlg::SHA256, P11JWEEncryptBenchmark::SymAlg::GCM192) ); + if(keysizes.contains("aes256")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-2048", vendor, P11JWEEncryptBenchmark::HashAlg::SHA256, P11JWEEncryptBenchmark::SymAlg::GCM256) ); + } + if(keysizes.contains("rsa3072") && has_key("rsa-3072")) { + if(keysizes.contains("aes128")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-3072", vendor, P11JWEEncryptBenchmark::HashAlg::SHA256, P11JWEEncryptBenchmark::SymAlg::GCM128) ); + if(keysizes.contains("aes192")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-3072", vendor, P11JWEEncryptBenchmark::HashAlg::SHA256, P11JWEEncryptBenchmark::SymAlg::GCM192) ); + if(keysizes.contains("aes256")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-3072", vendor, P11JWEEncryptBenchmark::HashAlg::SHA256, P11JWEEncryptBenchmark::SymAlg::GCM256) ); + } + if(keysizes.contains("rsa4096") && has_key("rsa-4096")) { + if(keysizes.contains("aes128")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-4096", vendor, P11JWEEncryptBenchmark::HashAlg::SHA256, P11JWEEncryptBenchmark::SymAlg::GCM128) ); + if(keysizes.contains("aes192")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-4096", vendor, P11JWEEncryptBenchmark::HashAlg::SHA256, P11JWEEncryptBenchmark::SymAlg::GCM192) ); + if(keysizes.contains("aes256")) + benchmarks.emplace_front( new P11JWEEncryptBenchmark("rsa-4096", vendor, P11JWEEncryptBenchmark::HashAlg::SHA256, P11JWEEncryptBenchmark::SymAlg::GCM256) ); + } + } + // JWE ( RSA OAEP + AES GCM ) // for JWE, we don't need to check has_key("") for AES, as these are session keys generated on the fly for each iteration of the benchmark, // and not persistent keys generated beforehand. We only check for the presence of RSA keys, which are needed for the key encryption step of JWE. diff --git a/src/testcoverage.cpp b/src/testcoverage.cpp index 95456da..47a7c00 100644 --- a/src/testcoverage.cpp +++ b/src/testcoverage.cpp @@ -104,6 +104,20 @@ TestCoverage::TestCoverage(std::string tocover) m_algo_coverage.insert(AlgoCoverage::jweoaepsha256); break; + case "jweenc"_hash: + m_algo_coverage.insert(AlgoCoverage::jweenc); + break; + + case "jweencoaepsha1"_hash: + case "jweencsha1"_hash: + m_algo_coverage.insert(AlgoCoverage::jweencoaepsha1); + break; + + case "jweencoaepsha256"_hash: + case "jweencsha256"_hash: + m_algo_coverage.insert(AlgoCoverage::jweencoaepsha256); + break; + case "oaep"_hash: m_algo_coverage.insert(AlgoCoverage::oaep); break; @@ -234,6 +248,20 @@ bool TestCoverage::contains(std::string algo) return contains(AlgoCoverage::jweoaepsha256); break; + case "jweenc"_hash: + return contains(AlgoCoverage::jweenc); + break; + + case "jweencoaepsha1"_hash: + case "jweencsha1"_hash: + return contains(AlgoCoverage::jweencoaepsha1); + break; + + case "jweencoaepsha256"_hash: + case "jweencsha256"_hash: + return contains(AlgoCoverage::jweencoaepsha256); + break; + case "oaep"_hash: return contains(AlgoCoverage::oaep); break; diff --git a/src/testcoverage.hpp b/src/testcoverage.hpp index 01a818a..2eef4dd 100644 --- a/src/testcoverage.hpp +++ b/src/testcoverage.hpp @@ -43,6 +43,9 @@ struct TestCoverage jwe, // JWE decryption (RFC7516) jweoaepsha1, // subset with OAEP(SHA1) jweoaepsha256, // subset with OAEP(SHA256) + jweenc, // JWE-like encryption (RSA OAEP wrap + AES GCM) + jweencoaepsha1, // subset with OAEP(SHA1) + jweencoaepsha256, // subset with OAEP(SHA256) oaep, // PKCS#1 OAEP decryption (all hashing algorithms) oaepsha1, // PKCS#1 OAEP decryption (SHA1) oaepsha256, // PKCS#1 OAEP decryption (SHA256)