Signet Forge 0.1.0
C++20 Parquet library with AI-native extensions
DEMO
Loading...
Searching...
No Matches
hsm_client_stub.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright 2026 Johnson Ogundeji
3// See LICENSE_COMMERCIAL for full terms.
4#pragma once
5
6#if !defined(SIGNET_ENABLE_COMMERCIAL) || !SIGNET_ENABLE_COMMERCIAL
7#error "signet/crypto/hsm_client_stub.hpp requires SIGNET_ENABLE_COMMERCIAL=ON (AGPL-3.0 commercial tier). See LICENSE_COMMERCIAL."
8#endif
9
10// ---------------------------------------------------------------------------
11// hsm_client_stub.hpp -- HSM Integration Testing Stub
12//
13// Gap T-8: HSM integration testing stubs for PME key wrapping validation.
14//
15// Provides a test-only IKmsClient implementation that simulates HSM-backed
16// key wrapping without requiring actual HSM hardware. Uses NIST SP 800-38F
17// AES Key Wrap (RFC 3394) semantics with a software-only KEK.
18//
19// This stub enables:
20// - End-to-end PME key wrapping tests without HSM infrastructure
21// - Validation of IKmsClient contract compliance
22// - Regression testing for DEK/KEK lifecycle
23// - CI pipeline crypto integration tests
24//
25// NOT for production use — keys are stored in process memory.
26//
27// References:
28// - NIST SP 800-38F: AES Key Wrap Specification
29// - RFC 3394: AES Key Wrap Algorithm
30// - PARQUET-1178 §3: Modular Encryption Key Management
31//
32// Header-only. Part of the signet::forge crypto module.
33// ---------------------------------------------------------------------------
34
37#include "signet/error.hpp"
38
39#include <array>
40#include <cstdint>
41#include <cstring>
42#include <string>
43#include <unordered_map>
44#include <vector>
45
46namespace signet::forge::crypto {
47
48// ---------------------------------------------------------------------------
49// AES Key Wrap (NIST SP 800-38F / RFC 3394) — software implementation
50// ---------------------------------------------------------------------------
51namespace detail::aes_key_wrap {
52
54static constexpr std::array<uint8_t, 8> DEFAULT_IV = {
55 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6
56};
57
63[[nodiscard]] inline expected<std::vector<uint8_t>> wrap(
64 const std::array<uint8_t, 32>& kek,
65 const std::vector<uint8_t>& plaintext)
66{
67 if (plaintext.size() < 16 || (plaintext.size() % 8) != 0) {
69 "AES Key Wrap: plaintext must be >= 16 bytes and a multiple of 8"};
70 }
71
72 const size_t n = plaintext.size() / 8; // Number of 64-bit blocks
73 Aes256 cipher(kek.data());
74
75 // Initialize: A = IV, R[1..n] = plaintext blocks
76 std::array<uint8_t, 8> A{};
77 std::memcpy(A.data(), DEFAULT_IV.data(), 8);
78
79 std::vector<uint8_t> R(plaintext.begin(), plaintext.end());
80
81 // Wrap rounds: 6 * n iterations
82 for (size_t j = 0; j < 6; ++j) {
83 for (size_t i = 0; i < n; ++i) {
84 // B = AES(K, A || R[i])
85 std::array<uint8_t, 16> block{};
86 std::memcpy(block.data(), A.data(), 8);
87 std::memcpy(block.data() + 8, R.data() + i * 8, 8);
88
89 cipher.encrypt_block(block.data());
90 const auto& encrypted = block;
91
92 // A = MSB(64, B) ^ t where t = n*j + i + 1
93 uint64_t t = n * j + i + 1;
94 std::memcpy(A.data(), encrypted.data(), 8);
95 // XOR t into A (big-endian)
96 for (int k = 7; k >= 0 && t > 0; --k) {
97 A[static_cast<size_t>(k)] ^= static_cast<uint8_t>(t & 0xFF);
98 t >>= 8;
99 }
100
101 // R[i] = LSB(64, B)
102 std::memcpy(R.data() + i * 8, encrypted.data() + 8, 8);
103 }
104 }
105
106 // Output: A || R[1..n]
107 std::vector<uint8_t> result(8 + R.size());
108 std::memcpy(result.data(), A.data(), 8);
109 std::memcpy(result.data() + 8, R.data(), R.size());
110 return result;
111}
112
119 const std::array<uint8_t, 32>& kek,
120 const std::vector<uint8_t>& ciphertext)
121{
122 if (ciphertext.size() < 24 || (ciphertext.size() % 8) != 0) {
124 "AES Key Unwrap: ciphertext must be >= 24 bytes and a multiple of 8"};
125 }
126
127 const size_t n = (ciphertext.size() / 8) - 1;
128 Aes256 cipher(kek.data());
129
130 // Initialize: A = C[0], R[1..n] = C[1..n]
131 std::array<uint8_t, 8> A{};
132 std::memcpy(A.data(), ciphertext.data(), 8);
133
134 std::vector<uint8_t> R(ciphertext.begin() + 8, ciphertext.end());
135
136 // Unwrap rounds: 6 * n iterations in reverse
137 for (int j = 5; j >= 0; --j) {
138 for (int ii = static_cast<int>(n) - 1; ii >= 0; --ii) {
139 size_t i = static_cast<size_t>(ii);
140
141 // A ^ t
142 uint64_t t = n * static_cast<size_t>(j) + i + 1;
143 std::array<uint8_t, 8> A_xor = A;
144 for (int k = 7; k >= 0 && t > 0; --k) {
145 A_xor[static_cast<size_t>(k)] ^= static_cast<uint8_t>(t & 0xFF);
146 t >>= 8;
147 }
148
149 // B = AES^-1(K, (A^t) || R[i])
150 std::array<uint8_t, 16> block{};
151 std::memcpy(block.data(), A_xor.data(), 8);
152 std::memcpy(block.data() + 8, R.data() + i * 8, 8);
153
154 cipher.decrypt_block(block.data());
155
156 // A = MSB(64, B)
157 std::memcpy(A.data(), block.data(), 8);
158 // R[i] = LSB(64, B)
159 std::memcpy(R.data() + i * 8, block.data() + 8, 8);
160 }
161 }
162
163 // Integrity check: A must equal the default IV
164 if (std::memcmp(A.data(), DEFAULT_IV.data(), 8) != 0) {
166 "AES Key Unwrap: integrity check failed — wrong KEK or corrupted data"};
167 }
168
169 return R;
170}
171
172} // namespace detail::aes_key_wrap
173
174// ---------------------------------------------------------------------------
175// HsmClientStub — IKmsClient implementation for testing
176// ---------------------------------------------------------------------------
177
190class HsmClientStub : public IKmsClient {
191public:
192 HsmClientStub() = default;
193
196 const std::string& key_id,
197 const std::vector<uint8_t>& kek)
198 {
199 if (kek.size() != 32) {
201 "HSM stub: KEK must be exactly 32 bytes (AES-256)"};
202 }
203 std::array<uint8_t, 32> key{};
204 std::memcpy(key.data(), kek.data(), 32);
205 keks_[key_id] = key;
206 return {};
207 }
208
210 void register_kek(const std::string& key_id,
211 const std::array<uint8_t, 32>& kek) {
212 keks_[key_id] = kek;
213 }
214
216 [[nodiscard]] bool has_kek(const std::string& key_id) const {
217 return keks_.find(key_id) != keks_.end();
218 }
219
221 [[nodiscard]] size_t kek_count() const { return keks_.size(); }
222
223 // --- IKmsClient interface ---
224
226 const std::vector<uint8_t>& dek,
227 const std::string& master_key_id) const override
228 {
229 auto it = keks_.find(master_key_id);
230 if (it == keks_.end()) {
232 "HSM stub: KEK not found: " + master_key_id};
233 }
234
235 // Reject DEKs that are not valid for AES Key Wrap (H-5).
236 // Silent padding would cause unwrap to return extra bytes.
237 if (dek.size() < 16 || (dek.size() % 8) != 0) {
239 "HSM stub: DEK must be >= 16 bytes and a multiple of 8"};
240 }
241
242 return detail::aes_key_wrap::wrap(it->second, dek);
243 }
244
246 const std::vector<uint8_t>& wrapped_dek,
247 const std::string& master_key_id) const override
248 {
249 auto it = keks_.find(master_key_id);
250 if (it == keks_.end()) {
252 "HSM stub: KEK not found: " + master_key_id};
253 }
254
255 return detail::aes_key_wrap::unwrap(it->second, wrapped_dek);
256 }
257
258private:
259 std::unordered_map<std::string, std::array<uint8_t, 32>> keks_;
260};
261
262} // namespace signet::forge::crypto
AES-256 block cipher implementation (FIPS-197).
AES-256 block cipher (FIPS-197).
Definition aes_core.hpp:253
void encrypt_block(uint8_t block[BLOCK_SIZE]) const
Encrypt a single 16-byte block in-place (FIPS-197 Section 5.1).
Definition aes_core.hpp:280
void decrypt_block(uint8_t block[BLOCK_SIZE]) const
Decrypt a single 16-byte block in-place (FIPS-197 Section 5.3).
Definition aes_core.hpp:328
Test stub implementing IKmsClient using software AES Key Wrap.
expected< void > register_kek(const std::string &key_id, const std::vector< uint8_t > &kek)
Register a KEK by ID. The key must be exactly 32 bytes (AES-256).
bool has_kek(const std::string &key_id) const
Check if a KEK is registered.
expected< std::vector< uint8_t > > unwrap_key(const std::vector< uint8_t > &wrapped_dek, const std::string &master_key_id) const override
Unwrap (decrypt) a wrapped DEK using the KEK identified by master_key_id.
size_t kek_count() const
Number of registered KEKs.
void register_kek(const std::string &key_id, const std::array< uint8_t, 32 > &kek)
Register a KEK from a raw 32-byte array.
expected< std::vector< uint8_t > > wrap_key(const std::vector< uint8_t > &dek, const std::string &master_key_id) const override
Wrap (encrypt) a DEK under the KEK identified by master_key_id.
Abstract KMS client interface for DEK/KEK key wrapping.
A lightweight result type that holds either a success value of type T or an Error.
Definition error.hpp:145
Key material, encryption configuration, and TLV serialization for Parquet Modular Encryption (PME).
expected< std::vector< uint8_t > > wrap(const std::array< uint8_t, 32 > &kek, const std::vector< uint8_t > &plaintext)
AES Key Wrap — wraps plaintext key material under a KEK.
expected< std::vector< uint8_t > > unwrap(const std::array< uint8_t, 32 > &kek, const std::vector< uint8_t > &ciphertext)
AES Key Unwrap — recovers plaintext key material from wrapped form.
@ ENCRYPTION_ERROR
An encryption or decryption operation failed (bad key, tampered ciphertext, PME error).
@ INVALID_ARGUMENT
A caller-supplied argument is outside the valid range or violates a precondition.
Lightweight error value carrying an ErrorCode and a human-readable message.
Definition error.hpp:101