Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
220 changes: 220 additions & 0 deletions src/p11jweenc.cpp
Original file line number Diff line number Diff line change
@@ -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 <string>
#include <array>
#include <iostream>
#include <random>
#include <algorithm>
#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);
Comment on lines +88 to +91

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

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

In the generic flavour, m_iv is resized but never populated with non-zero data before std::shuffle(). Shuffling a freshly resized vector (all zeros) does not produce a random IV, so the IV ends up constant across iterations despite the comment. Populate the IV with random bytes (or another deterministic-but-unique strategy) before use.

Suggested change
// 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);
// populate IV with random bytes to avoid reusing values between iterations
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<unsigned int> dist(0, 255);
for (auto &b : m_iv) {
b = static_cast<decltype(b)>(dist(gen));
}

Copilot uses AI. Check for mistakes.

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<size_t> 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<Attribute,6> aeskeytemplate {
{
{ static_cast<CK_ATTRIBUTE_TYPE>(AttributeType::Token), &bfalse, sizeof(Byte) },
{ static_cast<CK_ATTRIBUTE_TYPE>(AttributeType::Private), &btrue, sizeof(Byte) },
{ static_cast<CK_ATTRIBUTE_TYPE>(AttributeType::Encrypt), &btrue, sizeof(Byte) },
{ static_cast<CK_ATTRIBUTE_TYPE>(AttributeType::Decrypt), &btrue, sizeof(Byte) },
{ static_cast<CK_ATTRIBUTE_TYPE>(AttributeType::Extractable), &btrue, sizeof(Byte) },
{ static_cast<CK_ATTRIBUTE_TYPE>(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);
Comment on lines +214 to +217

Copilot AI Mar 25, 2026

Copy link

Choose a reason for hiding this comment

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

m_encrypted.resize(returned_len) shrinks the output buffer on every iteration. Because the next iteration re-expands the buffer (see the size check just above), this can introduce repeated reallocations inside the timed region and skew benchmark results. Consider keeping the buffer at a fixed max size and tracking returned_len separately (or avoid shrinking the vector in the hot path).

Copilot uses AI. Check for mistakes.

session.module()->C_DestroyObject(session.handle(), symkey_handle);

@covertmatthew covertmatthew Apr 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I noticed that in p11jwe.cpp the timer is suspended at various points before destroying the test object and returning, e.g.

suspend_timer();

As noted in P11JWEBenchmark::crashtestdummy, there are some housekeeping steps that should not be timed. Should there be a similar consideration here for P11JWEEncryptBenchmark::crashtestdummy? Maybe before the resizing calls?

}
105 changes: 105 additions & 0 deletions src/p11jweenc.hpp
Original file line number Diff line number Diff line change
@@ -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<uint8_t> m_wrapped;
std::vector<uint8_t> m_encrypted;
std::vector<uint8_t> 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<size_t> 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
Loading
Loading