cryptnox-sdk-esp32 1.0.0
ESP32 SDK for Cryptnox Hardware Wallet
Loading...
Searching...
No Matches
test_cw_secure_channel.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: LGPL-3.0-or-later
3 * Copyright (c) 2026 Cryptnox SA
4 */
5
6#include "unity.h"
7#include "CW_SecureChannel.h"
8#include "CW_Defs.h"
9#include "CW_Utils.h"
10#include "CW_Platform.h"
12#include <string.h>
13#include <stdio.h>
14
15/******************************************************************
16 * 1. Size / offset constants
17 ******************************************************************/
18
19#define SC_AES_BLOCK_BYTES (16U)
20#define SC_AES_KEY_BYTES (32U)
21#define SC_EC_COORD_BYTES (32U)
22#define SC_EC_PUBKEY_BYTES (64U)
23
24/* Certificate layout: 'C'[1] + nonce[8] + 0x04[1] + X[32] + Y[32] = 74 bytes */
25#define SC_CERT_MARKER_BYTES (1U)
26#define SC_CERT_NONCE_BYTES (8U)
27#define SC_CERT_KEY65_BYTES (65U)
28#define SC_CERT_TOTAL_BYTES (74U)
29#define SC_CERT_KEY_OFFSET (SC_CERT_MARKER_BYTES + SC_CERT_NONCE_BYTES)
30
31/* APDU response sizes (data + 2-byte SW) */
32#define SC_SW_BYTES (2U)
33#define SC_SELECT_RESP_BYTES (26U)
34#define SC_GET_CERT_RESP_BYTES (148U)
35#define SC_OPEN_SC_RESP_BYTES (34U)
36#define SC_MUTUAL_AUTH_RESP_BYTES (66U)
37#define SC_SALT_BYTES (32U)
38#define SC_IV_BYTES (16U)
39#define SC_SHA512_OUT_BYTES (64U)
40
41/* Secure messaging APDU layout (outgoing) */
42#define SC_APDU_HEADER_LEN (4U)
43#define SC_APDU_LC_OFFSET (4U)
44#define SC_APDU_MAC_OFFSET (5U) /* header[4] + Lc[1] */
45#define SC_APDU_MAC_BYTES (16U)
46
47/* Card response used in the AES-CBC round-trip test */
48#define SC_CARD_RESP_PAYLOAD_BYTES (4U)
49#define SC_CARD_RESP_TOTAL_BYTES (SC_CARD_RESP_PAYLOAD_BYTES + SC_SW_BYTES)
50
51/* Mock scripting limits */
52#define MOCK_MAX_SCRIPTS (8U)
53#define MOCK_MAX_RESP_BYTES (255U)
54#define MOCK_UART_BAUD_RATE (115200UL)
55
56/* Known P-256 public key (NIST FIPS 186-4 test vector) used as the
57 * card's ephemeral key in mock-transport tests.
58 * 64 bytes = X[32] || Y[32] (no 0x04 prefix). */
60 /* X */
61 0x60U, 0xfeU, 0xd4U, 0xbaU, 0x25U, 0x5aU, 0x9dU, 0x31U,
62 0xc9U, 0x61U, 0xebU, 0x74U, 0xc6U, 0x35U, 0x6dU, 0x68U,
63 0xc0U, 0x49U, 0xb8U, 0x92U, 0x3bU, 0x61U, 0xfaU, 0x6cU,
64 0xe6U, 0x69U, 0x62U, 0x2eU, 0x60U, 0xf2U, 0x9fU, 0xb6U,
65 /* Y */
66 0x79U, 0x03U, 0xfeU, 0x10U, 0x08U, 0xb8U, 0xbcU, 0x99U,
67 0xa4U, 0x1aU, 0xe9U, 0xe9U, 0x56U, 0x28U, 0xbcU, 0x64U,
68 0xf2U, 0xf1U, 0xb2U, 0x0cU, 0x2dU, 0x7eU, 0x9fU, 0x51U,
69 0x77U, 0xa3U, 0xc2U, 0x94U, 0xd4U, 0x46U, 0x22U, 0x99U
70};
71
72/* Known Kenc / Kmac for the AES-CBC round-trip test (AES-256 key from
73 * NIST SP 800-38A, extended to 32 bytes for the second key). */
74static const uint8_t K_TEST_KENC[SC_AES_KEY_BYTES] = {
75 0x60U, 0x3dU, 0xebU, 0x10U, 0x15U, 0xcaU, 0x71U, 0xbeU,
76 0x2bU, 0x73U, 0xaeU, 0xf0U, 0x85U, 0x7dU, 0x77U, 0x81U,
77 0x1fU, 0x35U, 0x2cU, 0x07U, 0x3bU, 0x61U, 0x08U, 0xd7U,
78 0x2dU, 0x98U, 0x10U, 0xa3U, 0x09U, 0x14U, 0xdfU, 0xf4U
79};
80static const uint8_t K_TEST_KMAC[SC_AES_KEY_BYTES] = {
81 0x00U, 0x01U, 0x02U, 0x03U, 0x04U, 0x05U, 0x06U, 0x07U,
82 0x08U, 0x09U, 0x0aU, 0x0bU, 0x0cU, 0x0dU, 0x0eU, 0x0fU,
83 0x10U, 0x11U, 0x12U, 0x13U, 0x14U, 0x15U, 0x16U, 0x17U,
84 0x18U, 0x19U, 0x1aU, 0x1bU, 0x1cU, 0x1dU, 0x1eU, 0x1fU
85};
86
87/* Card response payload returned by the reflective mock (4 bytes + SW). */
89 0xdeU, 0xadU, 0xbeU, 0xefU, 0x90U, 0x00U
90};
91
92/******************************************************************
93 * 2. MockLogger — no-op implementation
94 ******************************************************************/
95
96class MockLogger : public CW_Logger {
97public:
98 bool begin(unsigned long = MOCK_UART_BAUD_RATE) override {
99 return true;
100 }
101 void print(const __FlashStringHelper*) override {
102 }
103 void print(const char*) override {
104 }
105 void print(char) override {
106 }
107 void print(uint8_t, int = DEC) override {
108 }
109 void print(uint16_t, int = DEC) override {
110 }
111 void print(uint32_t, int = DEC) override {
112 }
113 void print(int, int = DEC) override {
114 }
115 void println() override {
116 }
117 void println(const __FlashStringHelper*) override {
118 }
119 void println(const char*) override {
120 }
121 void println(char) override {
122 }
123 void println(uint8_t, int = DEC) override {
124 }
125 void println(uint16_t, int = DEC) override {
126 }
127 void println(uint32_t, int = DEC) override {
128 }
129 void println(int, int = DEC) override {
130 }
131 ~MockLogger() override {
132 }
133};
134
135/******************************************************************
136 * 3. MockPlatform — no-op sleep_ms for tests
137 ******************************************************************/
138
139class MockPlatform : public CW_Platform {
140public:
141 void sleep_ms(uint32_t /*ms*/) override {
142 }
143 ~MockPlatform() override {
144 }
145};
146
147/******************************************************************
148 * 4. ScriptedMockNfcTransport — returns pre-loaded response buffers
149 ******************************************************************/
150
153 uint8_t len;
155};
156
157class ScriptedMockNfcTransport : public CW_NfcTransport {
158public:
160 uint8_t scriptCount = 0U;
161 uint8_t callIdx = 0U;
162
163 void reset() {
164 scriptCount = 0U;
165 callIdx = 0U;
166 (void)memset(scripts, 0, sizeof(scripts));
167 }
168
169 void addScript(const uint8_t* data, uint8_t len, bool succeed = true) {
171 (void)memcpy(scripts[scriptCount].data, data, static_cast<size_t>(len));
172 scripts[scriptCount].len = len;
173 scripts[scriptCount].succeed = succeed;
174 scriptCount++;
175 }
176 }
177
178 bool begin() override {
179 return true;
180 }
181 bool inListPassiveTarget() override {
182 return true;
183 }
184 void resetReader() override {
185 }
186 bool printFirmwareVersion() override {
187 return true;
188 }
189
190 bool sendAPDU(const uint8_t* apdu, uint8_t apduLen,
191 uint8_t* response, uint8_t& responseLen) override {
192 bool result = false;
193 (void)apdu;
194 (void)apduLen;
195
196 if (callIdx < scriptCount) {
197 const MockScriptEntry& e = scripts[callIdx];
198 callIdx++;
199 if (e.succeed) {
200 (void)memcpy(response, e.data, static_cast<size_t>(e.len));
201 responseLen = e.len;
202 result = true;
203 }
204 }
205 return result;
206 }
207
209 }
210};
211
212/******************************************************************
213 * 4. ReflectiveMockNfcTransport — computes a valid encrypted response
214 * for the AES-CBC-MAC round-trip test.
215 *
216 * Expected incoming APDU layout (from CW_SecureChannel::aesCbcEncrypt):
217 * header[4] | Lc[1] | MAC[16] | ciphertext[N]
218 *
219 * The mock extracts the sent MAC, encrypts K_CARD_RESP_PLAINTEXT using
220 * the session Kenc with that MAC as IV (matching what aesCbcDecrypt
221 * expects), and wraps it in the response MAC.
222 ******************************************************************/
223
224class ReflectiveMockNfcTransport : public CW_NfcTransport {
225public:
226 const CW_SecureSession* session = nullptr;
227 CW_CryptoProvider* crypto = nullptr;
228
229 bool begin() override {
230 return true;
231 }
232 bool inListPassiveTarget() override {
233 return true;
234 }
235 void resetReader() override {
236 }
237 bool printFirmwareVersion() override {
238 return true;
239 }
240
241 bool sendAPDU(const uint8_t* apdu, uint8_t apduLen,
242 uint8_t* response, uint8_t& responseLen) override {
243 bool result = false;
244
245 if ((apdu != NULL) &&
246 (apduLen > static_cast<uint8_t>(SC_APDU_MAC_OFFSET + SC_APDU_MAC_BYTES))) {
247 /* Step 1: extract sentMAC — used as IV when the channel decrypts the response */
248 uint8_t sentMacIv[SC_AES_BLOCK_BYTES] = { 0U };
249 (void)memcpy(sentMacIv, apdu + SC_APDU_MAC_OFFSET, SC_AES_BLOCK_BYTES);
250
251 /* Step 2: encrypt K_CARD_RESP_PLAINTEXT using Kenc, sentMAC as IV, bit-padding */
252 uint8_t cipherResp[SC_AES_BLOCK_BYTES * 2U] = { 0U };
253 uint8_t encIv[SC_AES_BLOCK_BYTES] = { 0U };
254 (void)memcpy(encIv, sentMacIv, SC_AES_BLOCK_BYTES);
255
256 uint16_t cipherRespLen = crypto->aesCbcEncrypt(
258 static_cast<uint16_t>(sizeof(K_CARD_RESP_PLAINTEXT)),
259 cipherResp,
260 session->aesKey,
261 static_cast<uint8_t>(sizeof(session->aesKey)),
262 encIv,
263 true);
264
265 /* Step 3: build the response MAC input:
266 * [totalDataLen as 1st byte of a 16-byte zero block] || [cipherResp] */
267 uint8_t totalDataLen = static_cast<uint8_t>(SC_AES_BLOCK_BYTES + cipherRespLen);
268 uint8_t macInput[SC_AES_BLOCK_BYTES + SC_AES_BLOCK_BYTES * 2U] = { 0U };
269 macInput[0U] = totalDataLen;
270 (void)memcpy(macInput + SC_AES_BLOCK_BYTES, cipherResp,
271 static_cast<size_t>(cipherRespLen));
272
273 uint16_t macInputLen = static_cast<uint16_t>(SC_AES_BLOCK_BYTES) + cipherRespLen;
274
275 uint8_t macZeroIv[SC_AES_BLOCK_BYTES] = { 0U };
276 uint8_t macOut[SC_AES_BLOCK_BYTES + SC_AES_BLOCK_BYTES * 2U] = { 0U };
277 uint16_t macOutLen = crypto->aesCbcEncrypt(
278 macInput,
279 macInputLen,
280 macOut,
281 session->macKey,
282 static_cast<uint8_t>(sizeof(session->macKey)),
283 macZeroIv,
284 false);
285
286 /* Step 4: responseMac = last 16 bytes of macOut */
287 const uint8_t* responseMac =
288 macOut + macOutLen - static_cast<uint16_t>(SC_AES_BLOCK_BYTES);
289
290 /* Step 5: assemble response = responseMac[16] || cipherResp[N] || SW[2] */
291 uint8_t totalRespLen = static_cast<uint8_t>(
292 static_cast<uint16_t>(SC_AES_BLOCK_BYTES) + cipherRespLen + SC_SW_BYTES);
293
294 (void)memcpy(response, responseMac, SC_AES_BLOCK_BYTES);
295 (void)memcpy(response + SC_AES_BLOCK_BYTES, cipherResp,
296 static_cast<size_t>(cipherRespLen));
297 response[SC_AES_BLOCK_BYTES + cipherRespLen] = 0x90U;
298 response[SC_AES_BLOCK_BYTES + cipherRespLen + 1U] = 0x00U;
299 responseLen = totalRespLen;
300 result = true;
301 }
302
303 return result;
304 }
305
307 }
308};
309
310/******************************************************************
311 * 5. Static instances shared across tests
312 ******************************************************************/
313
319
320/******************************************************************
321 * 6. checkStatusWord tests (section numbering kept for diff clarity)
322 ******************************************************************/
323
324TEST_CASE("checkStatusWord: SW 0x9000 returns true", "[secure_channel]")
325{
326 static const uint8_t resp[] = {
327 0x01U, 0x02U, 0x03U, 0x04U, 0x90U, 0x00U
328 };
329 CW_SecureChannel channel(s_scriptedTransport, s_logger, s_crypto, s_platform);
330
331 bool ok = channel.checkStatusWord(resp, static_cast<uint8_t>(sizeof(resp)),
332 0x90U, 0x00U);
333
334 TEST_ASSERT_TRUE(ok);
335}
336
337TEST_CASE("checkStatusWord: SW mismatch returns false", "[secure_channel]")
338{
339 static const uint8_t resp[] = {
340 0x01U, 0x02U, 0x6aU, 0x82U
341 };
342 CW_SecureChannel channel(s_scriptedTransport, s_logger, s_crypto, s_platform);
343
344 bool ok = channel.checkStatusWord(resp, static_cast<uint8_t>(sizeof(resp)),
345 0x90U, 0x00U);
346
347 TEST_ASSERT_FALSE(ok);
348}
349
350TEST_CASE("checkStatusWord: response shorter than 2 bytes returns false", "[secure_channel]")
351{
352 static const uint8_t resp[] = { 0x90U };
353 CW_SecureChannel channel(s_scriptedTransport, s_logger, s_crypto, s_platform);
354
355 bool ok = channel.checkStatusWord(resp, static_cast<uint8_t>(sizeof(resp)),
356 0x90U, 0x00U);
357
358 TEST_ASSERT_FALSE(ok);
359}
360
361/******************************************************************
362 * 7. extractCardEphemeralKey tests
363 ******************************************************************/
364
365TEST_CASE("extractCardEphemeralKey: extracts 64-byte key from synthetic certificate",
366 "[secure_channel]")
367{
368 /* Build a 74-byte synthetic certificate:
369 * cert[0] = 'C' marker
370 * cert[1..8] = nonce (arbitrary)
371 * cert[9] = 0x04 (uncompressed prefix — dropped by the extractor)
372 * cert[10..41] = X coordinate (known pattern)
373 * cert[42..73] = Y coordinate (known pattern) */
374 uint8_t cert[SC_CERT_TOTAL_BYTES] = { 0U };
375 cert[0U] = 0x43U; /* 'C' */
376 for (uint8_t i = 0U; i < static_cast<uint8_t>(SC_CERT_NONCE_BYTES); i++) {
377 cert[static_cast<size_t>(SC_CERT_MARKER_BYTES) + i] = static_cast<uint8_t>(i + 1U);
378 }
379 cert[SC_CERT_KEY_OFFSET] = 0x04U; /* uncompressed prefix */
380 for (uint8_t i = 1U; i < static_cast<uint8_t>(SC_CERT_KEY65_BYTES); i++) {
381 cert[static_cast<size_t>(SC_CERT_KEY_OFFSET) + i] = static_cast<uint8_t>(0xA0U + i);
382 }
383
384 uint8_t pubKey[SC_EC_PUBKEY_BYTES] = { 0U };
385 uint8_t fullKey65[SC_CERT_KEY65_BYTES] = { 0U };
386
387 CW_SecureChannel channel(s_scriptedTransport, s_logger, s_crypto, s_platform);
388 bool ok = channel.extractCardEphemeralKey(cert, pubKey, fullKey65);
389
390 TEST_ASSERT_TRUE(ok);
391
392 /* fullKey65[0] must be the 0x04 prefix */
393 TEST_ASSERT_EQUAL_HEX8(0x04U, fullKey65[0U]);
394
395 /* pubKey = fullKey65[1..64] = cert[10..73] */
396 TEST_ASSERT_EQUAL_HEX8_ARRAY(cert + SC_CERT_KEY_OFFSET + 1U, pubKey,
398}
399
400TEST_CASE("extractCardEphemeralKey: null cert pointer returns false", "[secure_channel]")
401{
402 uint8_t pubKey[SC_EC_PUBKEY_BYTES] = { 0U };
403
404 CW_SecureChannel channel(s_scriptedTransport, s_logger, s_crypto, s_platform);
405 bool ok = channel.extractCardEphemeralKey(NULL, pubKey, NULL);
406
407 TEST_ASSERT_FALSE(ok);
408}
409
410/******************************************************************
411 * 8. Protocol-flow tests with ScriptedMockNfcTransport
412 ******************************************************************/
413
414TEST_CASE("selectApdu: succeeds when transport returns SW 0x9000", "[secure_channel]")
415{
416 uint8_t resp[SC_SELECT_RESP_BYTES] = { 0U };
417 resp[SC_SELECT_RESP_BYTES - 2U] = 0x90U;
418 resp[SC_SELECT_RESP_BYTES - 1U] = 0x00U;
419
420 s_scriptedTransport.reset();
421 s_scriptedTransport.addScript(resp, static_cast<uint8_t>(sizeof(resp)));
422
423 CW_SecureChannel channel(s_scriptedTransport, s_logger, s_crypto, s_platform);
424 bool ok = channel.selectApdu();
425
426 TEST_ASSERT_TRUE(ok);
427}
428
429TEST_CASE("selectApdu: fails when transport returns error SW", "[secure_channel]")
430{
431 uint8_t resp[SC_SELECT_RESP_BYTES] = { 0U };
432 resp[SC_SELECT_RESP_BYTES - 2U] = 0x6aU;
433 resp[SC_SELECT_RESP_BYTES - 1U] = 0x82U;
434
435 s_scriptedTransport.reset();
436 s_scriptedTransport.addScript(resp, static_cast<uint8_t>(sizeof(resp)));
437
438 CW_SecureChannel channel(s_scriptedTransport, s_logger, s_crypto, s_platform);
439 bool ok = channel.selectApdu();
440
441 TEST_ASSERT_FALSE(ok);
442}
443
444TEST_CASE("getCardCertificate: extracts 146 certificate bytes from mock response",
445 "[secure_channel]")
446{
447 /* Build a 148-byte scripted response:
448 * bytes[0..145] = certificate data
449 * bytes[146..147] = SW 0x90 0x00 */
450 uint8_t resp[SC_GET_CERT_RESP_BYTES] = { 0U };
451 for (uint8_t i = 0U; i < static_cast<uint8_t>(SC_GET_CERT_RESP_BYTES - 2U); i++) {
452 resp[i] = static_cast<uint8_t>(i + 1U);
453 }
454 resp[SC_GET_CERT_RESP_BYTES - 2U] = 0x90U;
455 resp[SC_GET_CERT_RESP_BYTES - 1U] = 0x00U;
456
457 s_scriptedTransport.reset();
458 s_scriptedTransport.addScript(resp, static_cast<uint8_t>(sizeof(resp)));
459
460 uint8_t certBuf[SC_GET_CERT_RESP_BYTES] = { 0U };
461 uint8_t certLen = 0U;
462
463 CW_SecureChannel channel(s_scriptedTransport, s_logger, s_crypto, s_platform);
464 bool ok = channel.getCardCertificate(certBuf, certLen);
465
466 TEST_ASSERT_TRUE(ok);
467 TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(SC_GET_CERT_RESP_BYTES - 2U), certLen);
468 TEST_ASSERT_EQUAL_HEX8_ARRAY(resp, certBuf, certLen);
469}
470
471TEST_CASE("openSecureChannel: extracts 32-byte salt from mock response", "[secure_channel]")
472{
473 /* Build 34-byte scripted response: 32-byte salt + SW */
474 uint8_t resp[SC_OPEN_SC_RESP_BYTES] = { 0U };
475 for (uint8_t i = 0U; i < static_cast<uint8_t>(SC_SALT_BYTES); i++) {
476 resp[i] = static_cast<uint8_t>(0xA0U + i);
477 }
478 resp[SC_OPEN_SC_RESP_BYTES - 2U] = 0x90U;
479 resp[SC_OPEN_SC_RESP_BYTES - 1U] = 0x00U;
480
481 s_scriptedTransport.reset();
482 s_scriptedTransport.addScript(resp, static_cast<uint8_t>(sizeof(resp)));
483
484 uint8_t salt[SC_SALT_BYTES] = { 0U };
485 uint8_t clientPub[SC_EC_PUBKEY_BYTES] = { 0U };
486 uint8_t clientPriv[SC_EC_COORD_BYTES] = { 0U };
487 CW_Curve curve = CW_CURVE_SECP256R1;
488
489 CW_SecureChannel channel(s_scriptedTransport, s_logger, s_crypto, s_platform);
490 bool ok = channel.openSecureChannel(salt, clientPub, clientPriv, curve);
491
492 TEST_ASSERT_TRUE(ok);
493
494 /* Salt must match the first 32 bytes of the scripted response */
495 TEST_ASSERT_EQUAL_HEX8_ARRAY(resp, salt, SC_SALT_BYTES);
496
497 /* Client keypair must have been generated (non-zero) */
498 const uint8_t zeroPub[SC_EC_PUBKEY_BYTES] = { 0U };
499 const uint8_t zeroPriv[SC_EC_COORD_BYTES] = { 0U };
500 TEST_ASSERT_FALSE(CW_Utils::secure_compare(clientPub, zeroPub, SC_EC_PUBKEY_BYTES));
501 TEST_ASSERT_FALSE(CW_Utils::secure_compare(clientPriv, zeroPriv, SC_EC_COORD_BYTES));
502}
503
504TEST_CASE("mutuallyAuthenticate: sets session IV to first 16 bytes of mock response",
505 "[secure_channel]")
506{
507 /* Scripted 66-byte response for MUTUALLY AUTHENTICATE:
508 * bytes[0..15] = rolling IV (known pattern)
509 * bytes[16..63] = dummy encrypted data
510 * bytes[64..65] = SW 0x90 0x00 */
511 uint8_t resp[SC_MUTUAL_AUTH_RESP_BYTES] = { 0U };
512 for (uint8_t i = 0U; i < static_cast<uint8_t>(SC_IV_BYTES); i++) {
513 resp[i] = static_cast<uint8_t>(0xC0U + i);
514 }
515 resp[SC_MUTUAL_AUTH_RESP_BYTES - 2U] = 0x90U;
516 resp[SC_MUTUAL_AUTH_RESP_BYTES - 1U] = 0x00U;
517
518 s_scriptedTransport.reset();
519 s_scriptedTransport.addScript(resp, static_cast<uint8_t>(sizeof(resp)));
520
521 /* Use the known P-256 public key as the card's ephemeral key.
522 * Any valid point on secp256r1 works here — ECDH must not fail. */
523 uint8_t cardEphemeralPub[SC_EC_PUBKEY_BYTES] = { 0U };
524 (void)memcpy(cardEphemeralPub, K_CARD_EPHEMERAL_PUB, SC_EC_PUBKEY_BYTES);
525
526 uint8_t clientPub[SC_EC_PUBKEY_BYTES] = { 0U };
527 uint8_t clientPriv[SC_EC_COORD_BYTES] = { 0U };
528 const uint8_t salt[SC_SALT_BYTES] = { 0U };
529 CW_Curve curve = CW_CURVE_SECP256R1;
530
531 bool keyOk = s_crypto.makeKey(clientPub, clientPriv, curve);
532 TEST_ASSERT_TRUE(keyOk);
533
534 CW_SecureSession session;
535 CW_SecureChannel channel(s_scriptedTransport, s_logger, s_crypto, s_platform);
536 bool ok = channel.mutuallyAuthenticate(session, salt, clientPub, clientPriv,
537 curve, cardEphemeralPub);
538
539 TEST_ASSERT_TRUE(ok);
540
541 /* Session IV must be the first 16 bytes of the scripted response */
542 TEST_ASSERT_EQUAL_HEX8_ARRAY(resp, session.iv, SC_IV_BYTES);
543
544 /* Session keys must be non-zero (ECDH + SHA-512 derivation ran) */
545 const uint8_t zeroKey[SC_AES_KEY_BYTES] = { 0U };
546 TEST_ASSERT_FALSE(CW_Utils::secure_compare(session.aesKey, zeroKey, SC_AES_KEY_BYTES));
547 TEST_ASSERT_FALSE(CW_Utils::secure_compare(session.macKey, zeroKey, SC_AES_KEY_BYTES));
548}
549
550/******************************************************************
551 * 9. Key derivation correctness test
552 *
553 * The secure channel derives Kenc and Kmac as:
554 * sha512( ecdh_secret || "Cryptnox Basic CommonPairingData" || salt )
555 * Kenc = output[0..31], Kmac = output[32..63].
556 *
557 * This test verifies the derivation is consistent by computing it
558 * independently and comparing with what SHA-512 over the same input
559 * produces directly through the crypto provider.
560 ******************************************************************/
561
562TEST_CASE("key derivation: ECDH + SHA-512 split yields distinct Kenc and Kmac",
563 "[secure_channel]")
564{
565 CW_Curve curve = CW_CURVE_SECP256R1;
566
567 /* Generate two ephemeral keypairs */
568 uint8_t pubA[SC_EC_PUBKEY_BYTES] = { 0U };
569 uint8_t privA[SC_EC_COORD_BYTES] = { 0U };
570 uint8_t pubB[SC_EC_PUBKEY_BYTES] = { 0U };
571 uint8_t privB[SC_EC_COORD_BYTES] = { 0U };
572
573 bool okA = s_crypto.makeKey(pubA, privA, curve);
574 bool okB = s_crypto.makeKey(pubB, privB, curve);
575 TEST_ASSERT_TRUE(okA);
576 TEST_ASSERT_TRUE(okB);
577
578 /* ECDH: both sides must yield the same shared secret */
579 uint8_t secretAB[SC_EC_COORD_BYTES] = { 0U };
580 uint8_t secretBA[SC_EC_COORD_BYTES] = { 0U };
581 bool ecdhAB = s_crypto.ecdh(pubB, privA, secretAB, curve);
582 bool ecdhBA = s_crypto.ecdh(pubA, privB, secretBA, curve);
583 TEST_ASSERT_TRUE(ecdhAB);
584 TEST_ASSERT_TRUE(ecdhBA);
585 TEST_ASSERT_EQUAL_HEX8_ARRAY(secretAB, secretBA, SC_EC_COORD_BYTES);
586
587 /* Manually derive Kenc/Kmac with a known salt */
588 static const uint8_t SALT[SC_SALT_BYTES] = {
589 0x11U, 0x22U, 0x33U, 0x44U, 0x55U, 0x66U, 0x77U, 0x88U,
590 0x99U, 0xaaU, 0xbbU, 0xccU, 0xddU, 0xeeU, 0xffU, 0x00U,
591 0x01U, 0x02U, 0x03U, 0x04U, 0x05U, 0x06U, 0x07U, 0x08U,
592 0x09U, 0x0aU, 0x0bU, 0x0cU, 0x0dU, 0x0eU, 0x0fU, 0x10U
593 };
594
595 uint8_t concat[SC_EC_COORD_BYTES + CW_PAIRING_DATA_BYTES + SC_SALT_BYTES] = { 0U };
596 (void)memcpy(concat, secretAB, SC_EC_COORD_BYTES);
597 (void)memcpy(concat + SC_EC_COORD_BYTES, CW_PAIRING_DATA, CW_PAIRING_DATA_BYTES);
598 (void)memcpy(concat + SC_EC_COORD_BYTES + CW_PAIRING_DATA_BYTES, SALT, SC_SALT_BYTES);
599
600 uint8_t sha512Out[SC_SHA512_OUT_BYTES] = { 0U };
601 s_crypto.sha512(concat, sizeof(concat), sha512Out);
602
603 /* Kenc = sha512[0..31], Kmac = sha512[32..63] — they must differ */
604 uint8_t* derivedKenc = sha512Out;
605 uint8_t* derivedKmac = sha512Out + SC_AES_KEY_BYTES;
606
607 TEST_ASSERT_FALSE(CW_Utils::secure_compare(derivedKenc, derivedKmac, SC_AES_KEY_BYTES));
608
609 /* Cross-check: running mutuallyAuthenticate with these inputs must set
610 * the same Kenc/Kmac in the session (one scripted APDU response). */
611 uint8_t mutualResp[SC_MUTUAL_AUTH_RESP_BYTES] = { 0U };
612 mutualResp[SC_MUTUAL_AUTH_RESP_BYTES - 2U] = 0x90U;
613 mutualResp[SC_MUTUAL_AUTH_RESP_BYTES - 1U] = 0x00U;
614
615 s_scriptedTransport.reset();
616 s_scriptedTransport.addScript(mutualResp,
617 static_cast<uint8_t>(sizeof(mutualResp)));
618
619 CW_SecureSession session;
620 CW_SecureChannel channel(s_scriptedTransport, s_logger, s_crypto, s_platform);
621
622 /* publicB acts as the "card" ephemeral key; privA is the client key.
623 * The ECDH inside mutuallyAuthenticate will compute secretAB. */
624 bool ok = channel.mutuallyAuthenticate(session, SALT, pubA, privA, curve, pubB);
625 TEST_ASSERT_TRUE(ok);
626
627 TEST_ASSERT_EQUAL_HEX8_ARRAY(derivedKenc, session.aesKey, SC_AES_KEY_BYTES);
628 TEST_ASSERT_EQUAL_HEX8_ARRAY(derivedKmac, session.macKey, SC_AES_KEY_BYTES);
629}
630
631/******************************************************************
632 * 10. AES-CBC-MAC round-trip: aesCbcEncrypt + aesCbcDecrypt
633 *
634 * Uses a manually set session (known Kenc/Kmac) and the
635 * ReflectiveMockNfcTransport which computes a valid card response.
636 * Verifies the decrypted payload matches K_CARD_RESP_PLAINTEXT[0..3].
637 ******************************************************************/
638
639TEST_CASE("aesCbcEncrypt/aesCbcDecrypt: round-trip via reflective mock returns card payload",
640 "[secure_channel]")
641{
642 /* Set up a session with known keys and a zero IV */
643 CW_SecureSession session;
644 (void)memcpy(session.aesKey, K_TEST_KENC, SC_AES_KEY_BYTES);
645 (void)memcpy(session.macKey, K_TEST_KMAC, SC_AES_KEY_BYTES);
646 (void)memset(session.iv, 0x00U, SC_IV_BYTES);
647
648 /* Wire the reflective mock to this session */
649 s_reflectiveTransport.session = &session;
651
652 CW_SecureChannel channel(s_reflectiveTransport, s_logger, s_crypto, s_platform);
653
654 /* Arbitrary plaintext command payload */
655 static const uint8_t plaintext[] = { 0xAAU, 0xBBU, 0xCCU };
656 static const uint8_t apduHeader[] = { 0x80U, 0x01U, 0x00U, 0x00U };
657
658 uint8_t decryptedOut[32U] = { 0U };
659 uint16_t decryptedLen = 0U;
660
661 bool ok = channel.aesCbcEncrypt(
662 session,
663 apduHeader,
664 static_cast<uint16_t>(sizeof(apduHeader)),
665 plaintext,
666 static_cast<uint16_t>(sizeof(plaintext)),
667 decryptedOut,
668 &decryptedLen);
669
670 TEST_ASSERT_TRUE(ok);
671
672 /* decryptedLen = card response payload without the two SW bytes */
673 TEST_ASSERT_EQUAL_UINT16(static_cast<uint16_t>(SC_CARD_RESP_PAYLOAD_BYTES),
674 decryptedLen);
675
676 /* Decrypted payload must match K_CARD_RESP_PLAINTEXT (excl. SW) */
677 TEST_ASSERT_EQUAL_HEX8_ARRAY(K_CARD_RESP_PLAINTEXT, decryptedOut,
679}
CW_CryptoProvider backed by mbedTLS and the ESP32 hardware TRNG.
void print(uint8_t, int=DEC) override
void println(const char *) override
void print(char) override
void println(uint8_t, int=DEC) override
bool begin(unsigned long=MOCK_UART_BAUD_RATE) override
void println(uint32_t, int=DEC) override
void println() override
void println(char) override
void println(const __FlashStringHelper *) override
void println(int, int=DEC) override
void print(uint32_t, int=DEC) override
void println(uint16_t, int=DEC) override
void print(const __FlashStringHelper *) override
void print(const char *) override
void print(uint16_t, int=DEC) override
void print(int, int=DEC) override
void sleep_ms(uint32_t) override
bool sendAPDU(const uint8_t *apdu, uint8_t apduLen, uint8_t *response, uint8_t &responseLen) override
bool sendAPDU(const uint8_t *apdu, uint8_t apduLen, uint8_t *response, uint8_t &responseLen) override
MockScriptEntry scripts[MOCK_MAX_SCRIPTS]
void addScript(const uint8_t *data, uint8_t len, bool succeed=true)
CW_CryptoProvider implementation for ESP32 using mbedTLS and the hardware TRNG.
uint8_t data[MOCK_MAX_RESP_BYTES]
#define SC_SW_BYTES
static const uint8_t K_CARD_RESP_PLAINTEXT[SC_CARD_RESP_TOTAL_BYTES]
#define SC_IV_BYTES
#define SC_CERT_KEY_OFFSET
#define SC_CERT_KEY65_BYTES
static ReflectiveMockNfcTransport s_reflectiveTransport
#define SC_AES_KEY_BYTES
#define SC_AES_BLOCK_BYTES
#define SC_GET_CERT_RESP_BYTES
#define MOCK_UART_BAUD_RATE
#define SC_CERT_MARKER_BYTES
#define SC_MUTUAL_AUTH_RESP_BYTES
static ESP32CryptoProvider s_crypto
#define SC_SELECT_RESP_BYTES
static const uint8_t K_TEST_KENC[SC_AES_KEY_BYTES]
#define SC_SALT_BYTES
static const uint8_t K_TEST_KMAC[SC_AES_KEY_BYTES]
#define SC_OPEN_SC_RESP_BYTES
static ScriptedMockNfcTransport s_scriptedTransport
static const uint8_t K_CARD_EPHEMERAL_PUB[SC_EC_PUBKEY_BYTES]
#define SC_EC_COORD_BYTES
#define SC_CERT_TOTAL_BYTES
#define SC_CARD_RESP_PAYLOAD_BYTES
#define SC_EC_PUBKEY_BYTES
#define SC_APDU_MAC_OFFSET
#define SC_CARD_RESP_TOTAL_BYTES
#define SC_SHA512_OUT_BYTES
TEST_CASE("checkStatusWord: SW 0x9000 returns true", "[secure_channel]")
#define MOCK_MAX_SCRIPTS
#define MOCK_MAX_RESP_BYTES
#define SC_CERT_NONCE_BYTES
#define SC_APDU_MAC_BYTES
static MockLogger s_logger
static MockPlatform s_platform